aboutsummaryrefslogtreecommitdiffstats
path: root/configserver
diff options
context:
space:
mode:
authorMorten Tokle <mortent@yahooinc.com>2023-06-09 08:34:31 +0200
committerMorten Tokle <mortent@yahooinc.com>2023-06-12 22:53:33 +0200
commite4a5a20eb19ff2e2d5759f6fef83da5611d1a94e (patch)
tree1cef0d16766e852e36d9f44b7b2e788b17d9d8b2 /configserver
parent5f25e0ba346c04ccc27c60cc410c0ed2fdb6b06b (diff)
Persist tokens on deploy
Diffstat (limited to 'configserver')
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java11
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java3
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/PreparedModelsBuilder.java3
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java21
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java9
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java8
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java20
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializer.java62
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java3
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java20
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializerTest.java29
11 files changed, 180 insertions, 9 deletions
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java
index efdcaeec3aa..36f09f989a7 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java
@@ -21,6 +21,7 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.AthenzDomain;
import com.yahoo.config.provision.CloudAccount;
import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.DataplaneToken;
import com.yahoo.config.provision.DockerImage;
import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.TenantName;
@@ -385,6 +386,7 @@ public class ModelContextImpl implements ModelContext {
private final List<String> zoneDnsSuffixes;
private final List<String> environmentVariables;
private final Optional<CloudAccount> cloudAccount;
+ private final List<DataplaneToken> dataplaneTokens;
private final boolean allowUserFilters;
private final Duration endpointConnectionTtl;
@@ -402,7 +404,8 @@ public class ModelContextImpl implements ModelContext {
List<TenantSecretStore> tenantSecretStores,
SecretStore secretStore,
List<X509Certificate> operatorCertificates,
- Optional<CloudAccount> cloudAccount) {
+ Optional<CloudAccount> cloudAccount,
+ List<DataplaneToken> dataplaneTokens) {
this.featureFlags = new FeatureFlags(flagSource, applicationId, modelVersion);
this.applicationId = applicationId;
this.multitenant = configserverConfig.multitenant() || configserverConfig.hostedVespa() || Boolean.getBoolean("multitenant");
@@ -436,6 +439,7 @@ public class ModelContextImpl implements ModelContext {
this.endpointConnectionTtl = Duration.ofSeconds(
PermanentFlags.ENDPOINT_CONNECTION_TTL.bindTo(flagSource)
.with(FetchVector.Dimension.APPLICATION_ID, applicationId.serializedForm()).value());
+ this.dataplaneTokens = dataplaneTokens;
}
@Override public ModelContext.FeatureFlags featureFlags() { return featureFlags; }
@@ -525,6 +529,11 @@ public class ModelContextImpl implements ModelContext {
return cloudAccount;
}
+ @Override
+ public List<DataplaneToken> dataplaneTokens() {
+ return dataplaneTokens;
+ }
+
@Override public boolean allowUserFilters() { return allowUserFilters; }
@Override public Duration endpointConnectionTtl() { return endpointConnectionTtl; }
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java
index 9497a298a33..0e45d42efcf 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java
@@ -156,7 +156,8 @@ public class ActivatedModelsBuilder extends ModelsBuilder<Application> {
zkClient.readTenantSecretStores(),
secretStore,
zkClient.readOperatorCertificates(),
- zkClient.readCloudAccount());
+ zkClient.readCloudAccount(),
+ zkClient.readDataplaneTokens());
}
}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/PreparedModelsBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/PreparedModelsBuilder.java
index 1bfa94a1355..fc86629125f 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/PreparedModelsBuilder.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/PreparedModelsBuilder.java
@@ -209,7 +209,8 @@ public class PreparedModelsBuilder extends ModelsBuilder<PreparedModelsBuilder.P
params.tenantSecretStores(),
secretStore,
params.operatorCertificates(),
- params.cloudAccount());
+ params.cloudAccount(),
+ params.dataplaneTokens());
}
/** The result of preparing a single model version */
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java
index 239026249e5..f00e37e0ee8 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java
@@ -9,6 +9,7 @@ import com.yahoo.config.model.api.TenantSecretStore;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.AthenzDomain;
import com.yahoo.config.provision.CloudAccount;
+import com.yahoo.config.provision.DataplaneToken;
import com.yahoo.config.provision.DockerImage;
import com.yahoo.config.provision.TenantName;
import com.yahoo.container.jdisc.HttpRequest;
@@ -20,6 +21,7 @@ import com.yahoo.vespa.config.server.TimeoutBudget;
import com.yahoo.vespa.config.server.http.SessionHandler;
import com.yahoo.vespa.config.server.tenant.CloudAccountSerializer;
import com.yahoo.vespa.config.server.tenant.ContainerEndpointSerializer;
+import com.yahoo.vespa.config.server.tenant.DataplaneTokenSerializer;
import com.yahoo.vespa.config.server.tenant.EndpointCertificateMetadataSerializer;
import com.yahoo.vespa.config.server.tenant.TenantSecretStoreSerializer;
@@ -55,6 +57,7 @@ public final class PrepareParams {
static final String WAIT_FOR_RESOURCES_IN_PREPARE = "waitForResourcesInPrepare";
static final String OPERATOR_CERTIFICATES = "operatorCertificates";
static final String CLOUD_ACCOUNT = "cloudAccount";
+ static final String DATAPLANE_TOKENS = "dataplaneTokens";
private final ApplicationId applicationId;
private final TimeoutBudget timeoutBudget;
@@ -73,6 +76,7 @@ public final class PrepareParams {
private final List<TenantSecretStore> tenantSecretStores;
private final List<X509Certificate> operatorCertificates;
private final Optional<CloudAccount> cloudAccount;
+ private final List<DataplaneToken> dataplaneTokens;
private PrepareParams(ApplicationId applicationId,
TimeoutBudget timeoutBudget,
@@ -90,7 +94,8 @@ public final class PrepareParams {
boolean force,
boolean waitForResourcesInPrepare,
List<X509Certificate> operatorCertificates,
- Optional<CloudAccount> cloudAccount) {
+ Optional<CloudAccount> cloudAccount,
+ List<DataplaneToken> dataplaneTokens) {
this.timeoutBudget = timeoutBudget;
this.applicationId = Objects.requireNonNull(applicationId);
this.ignoreValidationErrors = ignoreValidationErrors;
@@ -108,6 +113,7 @@ public final class PrepareParams {
this.waitForResourcesInPrepare = waitForResourcesInPrepare;
this.operatorCertificates = operatorCertificates;
this.cloudAccount = Objects.requireNonNull(cloudAccount);
+ this.dataplaneTokens = dataplaneTokens;
}
public static class Builder {
@@ -129,6 +135,7 @@ public final class PrepareParams {
private List<TenantSecretStore> tenantSecretStores = List.of();
private List<X509Certificate> operatorCertificates = List.of();
private Optional<CloudAccount> cloudAccount = Optional.empty();
+ private List<DataplaneToken> dataplaneTokens = List.of();
public Builder() { }
@@ -266,6 +273,11 @@ public final class PrepareParams {
return this;
}
+ public Builder dataplaneTokens(List<DataplaneToken> dataplaneTokens) {
+ this.dataplaneTokens = List.copyOf(dataplaneTokens);
+ return this;
+ }
+
public PrepareParams build() {
return new PrepareParams(applicationId,
timeoutBudget,
@@ -283,7 +295,8 @@ public final class PrepareParams {
force,
waitForResourcesInPrepare,
operatorCertificates,
- cloudAccount);
+ cloudAccount,
+ dataplaneTokens);
}
}
@@ -327,6 +340,7 @@ public final class PrepareParams {
.waitForResourcesInPrepare(booleanValue(params, WAIT_FOR_RESOURCES_IN_PREPARE))
.operatorCertificates(deserialize(params.field(OPERATOR_CERTIFICATES), PrepareParams::readOperatorCertificates, List.of()))
.cloudAccount(deserialize(params.field(CLOUD_ACCOUNT), CloudAccountSerializer::fromSlime, null))
+ .dataplaneTokens(deserialize(params.field(DATAPLANE_TOKENS), DataplaneTokenSerializer::fromSlime, List.of()))
.build();
}
@@ -444,4 +458,7 @@ public final class PrepareParams {
return cloudAccount;
}
+ public List<DataplaneToken> dataplaneTokens() {
+ return dataplaneTokens;
+ }
}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java
index 94b3bd96620..b627fe9ba3b 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java
@@ -12,6 +12,7 @@ import com.yahoo.config.provision.AllocatedHosts;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.AthenzDomain;
import com.yahoo.config.provision.CloudAccount;
+import com.yahoo.config.provision.DataplaneToken;
import com.yahoo.config.provision.DockerImage;
import com.yahoo.config.provision.TenantName;
import com.yahoo.path.Path;
@@ -151,6 +152,10 @@ public abstract class Session implements Comparable<Session> {
sessionZooKeeperClient.writeCloudAccount(cloudAccount);
}
+ public void setDataplaneTokens(List<DataplaneToken> dataplaneTokens) {
+ sessionZooKeeperClient.writeDataplaneTokens(dataplaneTokens);
+ }
+
/** Returns application id read from ZooKeeper. Will throw RuntimeException if not found */
public ApplicationId getApplicationId() { return sessionZooKeeperClient.readApplicationId(); }
@@ -193,6 +198,10 @@ public abstract class Session implements Comparable<Session> {
return sessionZooKeeperClient.readCloudAccount();
}
+ public List<DataplaneToken> getDataplaneTokens() {
+ return sessionZooKeeperClient.readDataplaneTokens();
+ }
+
private Transaction createSetStatusTransaction(Status status) {
return sessionZooKeeperClient.createWriteStatusTransaction(status);
}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java
index d33dcea5c69..ae87a0dd182 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java
@@ -24,6 +24,7 @@ import com.yahoo.config.provision.AllocatedHosts;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.AthenzDomain;
import com.yahoo.config.provision.CloudAccount;
+import com.yahoo.config.provision.DataplaneToken;
import com.yahoo.config.provision.DockerImage;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.Tags;
@@ -344,7 +345,8 @@ public class SessionPreparer {
params.quota(),
params.tenantSecretStores(),
params.operatorCertificates(),
- params.cloudAccount());
+ params.cloudAccount(),
+ params.dataplaneTokens());
checkTimeout("write state to zookeeper");
}
@@ -385,7 +387,8 @@ public class SessionPreparer {
Optional<Quota> quota,
List<TenantSecretStore> tenantSecretStores,
List<X509Certificate> operatorCertificates,
- Optional<CloudAccount> cloudAccount) {
+ Optional<CloudAccount> cloudAccount,
+ List<DataplaneToken> dataplaneTokens) {
ZooKeeperDeployer zkDeployer = zooKeeperClient.createDeployer(deployLogger);
try {
zkDeployer.deploy(applicationPackage, fileRegistryMap, allocatedHosts);
@@ -399,6 +402,7 @@ public class SessionPreparer {
zooKeeperClient.writeTenantSecretStores(tenantSecretStores);
zooKeeperClient.writeOperatorCertificates(operatorCertificates);
zooKeeperClient.writeCloudAccount(cloudAccount);
+ zooKeeperClient.writeDataplaneTokens(dataplaneTokens);
} catch (RuntimeException | IOException e) {
zkDeployer.cleanup();
throw new RuntimeException("Error preparing session", e);
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java
index 6284ecad33e..23b6fe075fa 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java
@@ -14,6 +14,7 @@ import com.yahoo.config.provision.AllocatedHosts;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.AthenzDomain;
import com.yahoo.config.provision.CloudAccount;
+import com.yahoo.config.provision.DataplaneToken;
import com.yahoo.config.provision.DockerImage;
import com.yahoo.config.provision.TenantName;
import com.yahoo.path.Path;
@@ -27,6 +28,7 @@ import com.yahoo.vespa.config.server.deploy.ZooKeeperDeployer;
import com.yahoo.vespa.config.server.filedistribution.AddFileInterface;
import com.yahoo.vespa.config.server.filedistribution.MockFileManager;
import com.yahoo.vespa.config.server.tenant.CloudAccountSerializer;
+import com.yahoo.vespa.config.server.tenant.DataplaneTokenSerializer;
import com.yahoo.vespa.config.server.tenant.OperatorCertificateSerializer;
import com.yahoo.vespa.config.server.tenant.TenantRepository;
import com.yahoo.vespa.config.server.tenant.TenantSecretStoreSerializer;
@@ -69,6 +71,7 @@ public class SessionZooKeeperClient {
private static final String TENANT_SECRET_STORES_PATH = "tenantSecretStores";
private static final String OPERATOR_CERTIFICATES_PATH = "operatorCertificates";
private static final String CLOUD_ACCOUNT_PATH = "cloudAccount";
+ private static final String DATAPLANE_TOKENS_PATH = "dataplaneTokens";
private final Curator curator;
private final TenantName tenantName;
@@ -216,6 +219,10 @@ public class SessionZooKeeperClient {
return sessionPath.append(CLOUD_ACCOUNT_PATH);
}
+ private Path dataplaneTokensPath() {
+ return sessionPath.append(DATAPLANE_TOKENS_PATH);
+ }
+
public void writeVespaVersion(Version version) {
curator.set(versionPath(), Utf8.toBytes(version.toString()));
}
@@ -335,6 +342,18 @@ public class SessionZooKeeperClient {
return curator.getData(cloudAccountPath()).map(SlimeUtils::jsonToSlime).map(slime -> CloudAccountSerializer.fromSlime(slime.get()));
}
+ public void writeDataplaneTokens(List<DataplaneToken> dataplaneTokens) {
+ byte[] data = uncheck(() -> SlimeUtils.toJsonBytes(DataplaneTokenSerializer.toSlime(dataplaneTokens)));
+ curator.set(dataplaneTokensPath(), data);
+ }
+
+ public List<DataplaneToken> readDataplaneTokens() {
+ return curator.getData(dataplaneTokensPath())
+ .map(SlimeUtils::jsonToSlime)
+ .map(slime -> DataplaneTokenSerializer.fromSlime(slime.get()))
+ .orElse(List.of());
+ }
+
/**
* Create necessary paths atomically for a new session.
*
@@ -352,5 +371,4 @@ public class SessionZooKeeperClient {
private static Path getSessionPath(TenantName tenantName, long sessionId) {
return TenantRepository.getSessionsPath(tenantName).append(String.valueOf(sessionId));
}
-
}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializer.java
new file mode 100644
index 00000000000..be6bf7164ad
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializer.java
@@ -0,0 +1,62 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.tenant;
+
+import com.yahoo.config.provision.DataplaneToken;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Inspector;
+import com.yahoo.slime.Slime;
+import com.yahoo.slime.SlimeUtils;
+
+import java.util.List;
+
+/**
+ * Serialize/deserialize dataplane tokens
+ *
+ * @author mortent
+ */
+public class DataplaneTokenSerializer {
+
+ private static final String ID_FIELD = "id";
+ private static final String VALUES_FIELD = "values";
+ private static final String FINGERPRINT_FIELD = "fingerPrint";
+ private static final String CHECKACCESSHASH_FIELD = "checkAccessHash";
+
+ private DataplaneTokenSerializer() {}
+
+ public static List<DataplaneToken> fromSlime(Inspector object) {
+ return SlimeUtils.entriesStream(object)
+ .map(DataplaneTokenSerializer::tokenFromSlime)
+ .toList();
+ }
+
+ private static DataplaneToken tokenFromSlime(Inspector object) {
+ String id = object.field(ID_FIELD).asString();
+ List<DataplaneToken.TokenValue> values = SlimeUtils.entriesStream(object.field(VALUES_FIELD))
+ .filter(Inspector::valid)
+ .map(DataplaneTokenSerializer::tokenValue)
+ .toList();
+ return new DataplaneToken(id, values);
+ }
+
+ private static DataplaneToken.TokenValue tokenValue(Inspector inspector) {
+ return new DataplaneToken.TokenValue(
+ inspector.field(FINGERPRINT_FIELD).asString(),
+ inspector.field(CHECKACCESSHASH_FIELD).asString());
+ }
+
+ public static Slime toSlime(List<DataplaneToken> dataplaneTokens) {
+ Slime slime = new Slime();
+ Cursor root = slime.setArray();
+ for (DataplaneToken token : dataplaneTokens) {
+ Cursor cursor = root.addObject();
+ cursor.setString(ID_FIELD, token.tokenId());
+ Cursor values = cursor.setArray(VALUES_FIELD);
+ token.tokenValues().forEach(v -> {
+ Cursor val = values.addObject();
+ val.setString(FINGERPRINT_FIELD, v.fingerprint());
+ val.setString(CHECKACCESSHASH_FIELD, v.checkAccessHash());
+ });
+ }
+ return slime;
+ }
+}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java
index 2ef9a41a967..f5cd56707b3 100644
--- a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java
@@ -75,7 +75,8 @@ public class ModelContextImplTest {
List.of(),
new SecretStoreProvider().get(),
List.of(),
- Optional.empty()),
+ Optional.empty(),
+ List.of()),
Optional.empty(),
Optional.empty(),
new Version(7),
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java
index 4af0881e200..19f24f8e817 100644
--- a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java
@@ -16,6 +16,7 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.CertificateNotReadyException;
import com.yahoo.config.provision.CloudAccount;
+import com.yahoo.config.provision.DataplaneToken;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.Zone;
@@ -360,6 +361,25 @@ public class SessionPreparerTest {
assertEquals(Optional.of(expected), accountFromModel);
}
+ @Test
+ public void require_that_dataplane_tokens_are_written() throws Exception {
+ TestModelFactory modelFactory = new TestModelFactory(version123);
+ preparer = createPreparer(new ModelFactoryRegistry(List.of(modelFactory)), HostProvisionerProvider.empty());
+ ApplicationId applicationId = applicationId("test");
+ List<DataplaneToken> expected = List.of(new DataplaneToken("id", List.of(new DataplaneToken.TokenValue("f1", "ch1"))));
+ PrepareParams params = new PrepareParams.Builder().applicationId(applicationId)
+ .dataplaneTokens(expected)
+ .build();
+ prepare(new File("src/test/resources/deploy/hosted-app"), params);
+
+ SessionZooKeeperClient zkClient = createSessionZooKeeperClient();
+ assertEquals(expected, zkClient.readDataplaneTokens());
+
+ ModelContext modelContext = modelFactory.getModelContext();
+ List<DataplaneToken> tokensFromModel = modelContext.properties().dataplaneTokens();
+ assertEquals(expected, tokensFromModel);
+ }
+
private List<ContainerEndpoint> readContainerEndpoints(ApplicationId applicationId) {
Path tenantPath = TenantRepository.getTenantPath(applicationId.tenant());
return new ContainerEndpointsCache(tenantPath, curator).read(applicationId);
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializerTest.java
new file mode 100644
index 00000000000..cb9e29e6517
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/tenant/DataplaneTokenSerializerTest.java
@@ -0,0 +1,29 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.tenant;
+
+import com.yahoo.config.provision.DataplaneToken;
+import com.yahoo.slime.Slime;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author mortent
+ */
+public class DataplaneTokenSerializerTest {
+
+ @Test
+ public void testSerialization() {
+ List<DataplaneToken> tokens = List.of(
+ new DataplaneToken("id1",
+ List.of(new DataplaneToken.TokenValue("id1_fingerPrint1", "id1_checkaccesshash1"))),
+ new DataplaneToken("id2",
+ List.of(new DataplaneToken.TokenValue("id2_fingerPrint1", "id2_checkaccesshash1"),
+ new DataplaneToken.TokenValue("id3_fingerPrint1", "id3_checkaccesshash1"))));
+ Slime slime = DataplaneTokenSerializer.toSlime(tokens);
+ List<DataplaneToken> deserialized = DataplaneTokenSerializer.fromSlime(slime.get());
+ assertEquals(tokens, deserialized);
+ }
+}