From 51dd527f03628a0fe37babdb9a105111a7c7d40a Mon Sep 17 00:00:00 2001 From: Valerij Fredriksen Date: Thu, 2 Mar 2023 14:15:28 +0100 Subject: Store tenant managed archive buckets in ZK --- .../api/integration/archive/ArchiveBucket.java | 71 --------------------- .../api/integration/archive/ArchiveBuckets.java | 34 ++++++++++ .../api/integration/archive/ArchiveService.java | 6 +- .../integration/archive/MockArchiveService.java | 12 ++-- .../archive/TenantManagedArchiveBucket.java | 15 +++++ .../archive/VespaManagedArchiveBucket.java | 72 ++++++++++++++++++++++ .../controller/archive/CuratorArchiveBucketDb.java | 52 +++++----------- .../maintenance/ArchiveAccessMaintainer.java | 9 +-- .../persistence/ArchiveBucketsSerializer.java | 61 ++++++++++++------ .../hosted/controller/persistence/CuratorDb.java | 10 +-- .../archive/CuratorArchiveBucketDbTest.java | 17 ++--- .../maintenance/ArchiveAccessMaintainerTest.java | 4 -- .../maintenance/ArchiveUriUpdaterTest.java | 33 +++++----- .../persistence/ArchiveBucketsSerializerTest.java | 25 ++++---- 14 files changed, 231 insertions(+), 190 deletions(-) delete mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveBucket.java create mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveBuckets.java create mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/TenantManagedArchiveBucket.java create mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/VespaManagedArchiveBucket.java diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveBucket.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveBucket.java deleted file mode 100644 index be3b87ddc5c..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveBucket.java +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.controller.api.integration.archive; - -import com.google.common.collect.Sets; -import com.yahoo.config.provision.TenantName; - -import java.util.Objects; -import java.util.Set; - -/** - * Represents an S3 bucket used to store archive data - logs, heap/core dumps, etc. - * - * @author andreer - */ -public class ArchiveBucket { - private final String bucketName; - private final String keyArn; - private final Set tenants; - - public ArchiveBucket(String bucketName, String keyArn) { - this(bucketName, keyArn, Set.of()); - } - - private ArchiveBucket(String bucketName, String keyArn, Set tenants) { - this.bucketName = bucketName; - this.keyArn = keyArn; - this.tenants = Set.copyOf(tenants); - } - - public String bucketName() { - return bucketName; - } - - public String keyArn() { - return keyArn; - } - - public Set tenants() { - return tenants; - } - - public ArchiveBucket withTenant(TenantName tenant) { - return withTenants(Set.of(tenant)); - } - - public ArchiveBucket withTenants(Set tenants) { - return new ArchiveBucket(bucketName, keyArn, Sets.union(this.tenants, tenants)); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ArchiveBucket that = (ArchiveBucket) o; - return bucketName.equals(that.bucketName) && keyArn.equals(that.keyArn) && tenants.equals(that.tenants); - } - - @Override - public int hashCode() { - return Objects.hash(bucketName, keyArn, tenants); - } - - @Override - public String toString() { - return "ArchiveBucket{" + - "bucketName='" + bucketName + '\'' + - ", keyArn='" + keyArn + '\'' + - ", tenants=" + tenants + - '}'; - } -} \ No newline at end of file diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveBuckets.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveBuckets.java new file mode 100644 index 00000000000..62e341c674c --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveBuckets.java @@ -0,0 +1,34 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.archive; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author freva + */ +public record ArchiveBuckets(Set vespaManaged, + Set tenantManaged) { + public static final ArchiveBuckets EMPTY = new ArchiveBuckets(Set.of(), Set.of()); + + public ArchiveBuckets(Set vespaManaged, Set tenantManaged) { + this.vespaManaged = Set.copyOf(vespaManaged); + this.tenantManaged = Set.copyOf(tenantManaged); + } + + /** Adds or replaces a VespaManagedArchive bucket with the given archive bucket */ + public ArchiveBuckets with(VespaManagedArchiveBucket vespaManagedArchiveBucket) { + Set updated = new HashSet<>(vespaManaged); + updated.removeIf(bucket -> bucket.bucketName().equals(vespaManagedArchiveBucket.bucketName())); + updated.add(vespaManagedArchiveBucket); + return new ArchiveBuckets(updated, tenantManaged); + } + + /** Adds or replaces a TenantManagedArchive bucket with the given archive bucket */ + public ArchiveBuckets with(TenantManagedArchiveBucket tenantManagedArchiveBucket) { + Set updated = new HashSet<>(tenantManaged); + updated.removeIf(bucket -> bucket.cloudAccount().equals(tenantManagedArchiveBucket.cloudAccount())); + updated.add(tenantManagedArchiveBucket); + return new ArchiveBuckets(vespaManaged, updated); + } +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveService.java index 71425ca4b79..ed965f4331e 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveService.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveService.java @@ -19,11 +19,11 @@ import java.util.Set; */ public interface ArchiveService { - ArchiveBucket createArchiveBucketFor(ZoneId zoneId); + VespaManagedArchiveBucket createArchiveBucketFor(ZoneId zoneId); - void updatePolicies(ZoneId zoneId, Set buckets, Map authorizeAccessByTenantName); + void updatePolicies(ZoneId zoneId, Set buckets, Map authorizeAccessByTenantName); - boolean canAddTenantToBucket(ZoneId zoneId, ArchiveBucket bucket); + boolean canAddTenantToBucket(ZoneId zoneId, VespaManagedArchiveBucket bucket); Optional findEnclaveArchiveBucket(ZoneId zoneId, CloudAccount cloudAccount); diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/MockArchiveService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/MockArchiveService.java index 17f90f13988..4bed7c5177f 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/MockArchiveService.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/MockArchiveService.java @@ -19,24 +19,22 @@ import java.util.Set; */ public class MockArchiveService implements ArchiveService { - - public Set archiveBuckets = new HashSet<>(); + public Set archiveBuckets = new HashSet<>(); public Map authorizeAccessByTenantName = new HashMap<>(); - @Override - public ArchiveBucket createArchiveBucketFor(ZoneId zoneId) { - return new ArchiveBucket("bucketName", "keyArn"); + public VespaManagedArchiveBucket createArchiveBucketFor(ZoneId zoneId) { + return new VespaManagedArchiveBucket("bucketName", "keyArn"); } @Override - public void updatePolicies(ZoneId zoneId, Set buckets, Map authorizeAccessByTenantName) { + public void updatePolicies(ZoneId zoneId, Set buckets, Map authorizeAccessByTenantName) { this.archiveBuckets = new HashSet<>(buckets); this.authorizeAccessByTenantName = new HashMap<>(authorizeAccessByTenantName); } @Override - public boolean canAddTenantToBucket(ZoneId zoneId, ArchiveBucket bucket) { + public boolean canAddTenantToBucket(ZoneId zoneId, VespaManagedArchiveBucket bucket) { return bucket.tenants().size() < 5; } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/TenantManagedArchiveBucket.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/TenantManagedArchiveBucket.java new file mode 100644 index 00000000000..80e9762f84b --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/TenantManagedArchiveBucket.java @@ -0,0 +1,15 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.archive; + +import com.yahoo.config.provision.CloudAccount; + +import java.time.Instant; + +/** + * Represents a cloud storage bucket (e.g. AWS S3 or Google Storage) used to store archive data - logs, heap/core dumps, etc. + * that is managed by the tenant directly. + * + * @author freva + */ +public record TenantManagedArchiveBucket(String bucketName, CloudAccount cloudAccount, Instant updatedAt) { +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/VespaManagedArchiveBucket.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/VespaManagedArchiveBucket.java new file mode 100644 index 00000000000..c80e9b3780d --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/VespaManagedArchiveBucket.java @@ -0,0 +1,72 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.archive; + +import com.google.common.collect.Sets; +import com.yahoo.config.provision.TenantName; + +import java.util.Objects; +import java.util.Set; + +/** + * Represents a cloud storage bucket (e.g. AWS S3 or Google Storage) used to store archive data - logs, heap/core dumps, etc. + * that is managed by the Vespa controller. + * + * @author andreer + */ +public class VespaManagedArchiveBucket { + private final String bucketName; + private final String keyArn; + private final Set tenants; + + public VespaManagedArchiveBucket(String bucketName, String keyArn) { + this(bucketName, keyArn, Set.of()); + } + + private VespaManagedArchiveBucket(String bucketName, String keyArn, Set tenants) { + this.bucketName = bucketName; + this.keyArn = keyArn; + this.tenants = Set.copyOf(tenants); + } + + public String bucketName() { + return bucketName; + } + + public String keyArn() { + return keyArn; + } + + public Set tenants() { + return tenants; + } + + public VespaManagedArchiveBucket withTenant(TenantName tenant) { + return withTenants(Set.of(tenant)); + } + + public VespaManagedArchiveBucket withTenants(Set tenants) { + return new VespaManagedArchiveBucket(bucketName, keyArn, Sets.union(this.tenants, tenants)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + VespaManagedArchiveBucket that = (VespaManagedArchiveBucket) o; + return bucketName.equals(that.bucketName) && keyArn.equals(that.keyArn) && tenants.equals(that.tenants); + } + + @Override + public int hashCode() { + return Objects.hash(bucketName, keyArn, tenants); + } + + @Override + public String toString() { + return "ArchiveBucket{" + + "bucketName='" + bucketName + '\'' + + ", keyArn='" + keyArn + '\'' + + ", tenants=" + tenants + + '}'; + } +} \ No newline at end of file diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDb.java index 1c930b04989..e9734359647 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDb.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDb.java @@ -4,15 +4,14 @@ package com.yahoo.vespa.hosted.controller.archive; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.Controller; -import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBucket; +import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBuckets; import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveService; +import com.yahoo.vespa.hosted.controller.api.integration.archive.VespaManagedArchiveBucket; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import java.net.URI; -import java.util.HashSet; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @@ -39,62 +38,43 @@ public class CuratorArchiveBucketDb { public Optional archiveUriFor(ZoneId zoneId, TenantName tenant, boolean createIfMissing) { return getBucketNameFromCache(zoneId, tenant) - .or(() -> findAndUpdateArchiveUriCache(zoneId, tenant, buckets(zoneId))) .or(() -> createIfMissing ? Optional.of(assignToBucket(zoneId, tenant)) : Optional.empty()) .map(bucketName -> archiveService.bucketURI(zoneId, bucketName)); } private String assignToBucket(ZoneId zoneId, TenantName tenant) { try (var lock = curatorDb.lockArchiveBuckets(zoneId)) { - Set zoneBuckets = new HashSet<>(buckets(zoneId)); + ArchiveBuckets archiveBuckets = buckets(zoneId); + updateArchiveUriCache(zoneId, archiveBuckets); - return findAndUpdateArchiveUriCache(zoneId, tenant, zoneBuckets) // Some other thread might have assigned it before we grabbed the lock + return getBucketNameFromCache(zoneId, tenant) // Some other thread might have assigned it before we grabbed the lock .orElseGet(() -> { // If not, find an existing bucket with space - Optional unfilledBucket = zoneBuckets.stream() + VespaManagedArchiveBucket bucketToAssignTo = archiveBuckets.vespaManaged().stream() .filter(bucket -> archiveService.canAddTenantToBucket(zoneId, bucket)) - .findAny(); + .findAny() + // Or create a new one + .orElseGet(() -> archiveService.createArchiveBucketFor(zoneId)); - // And place the tenant in that bucket. - if (unfilledBucket.isPresent()) { - var unfilled = unfilledBucket.get(); + ArchiveBuckets updated = archiveBuckets.with(bucketToAssignTo.withTenant(tenant)); + curatorDb.writeArchiveBuckets(zoneId, updated); + updateArchiveUriCache(zoneId, updated); - zoneBuckets.remove(unfilled); - zoneBuckets.add(unfilled.withTenant(tenant)); - curatorDb.writeArchiveBuckets(zoneId, zoneBuckets); - - return unfilled.bucketName(); - } - - // We'll have to create a new bucket - var newBucket = archiveService.createArchiveBucketFor(zoneId).withTenant(tenant); - zoneBuckets.add(newBucket); - curatorDb.writeArchiveBuckets(zoneId, zoneBuckets); - updateArchiveUriCache(zoneId, zoneBuckets); - return newBucket.bucketName(); + return bucketToAssignTo.bucketName(); }); } } - public Set buckets(ZoneId zoneId) { + public ArchiveBuckets buckets(ZoneId zoneId) { return curatorDb.readArchiveBuckets(zoneId); } - private Optional findAndUpdateArchiveUriCache(ZoneId zoneId, TenantName tenant, Set zoneBuckets) { - Optional bucketName = zoneBuckets.stream() - .filter(bucket -> bucket.tenants().contains(tenant)) - .findAny() - .map(ArchiveBucket::bucketName); - if (bucketName.isPresent()) updateArchiveUriCache(zoneId, zoneBuckets); - return bucketName; - } - private Optional getBucketNameFromCache(ZoneId zoneId, TenantName tenantName) { return Optional.ofNullable(archiveUriCache.get(zoneId)).map(map -> map.get(tenantName)); } - private void updateArchiveUriCache(ZoneId zoneId, Set zoneBuckets) { - Map bucketNameByTenant = zoneBuckets.stream() + private void updateArchiveUriCache(ZoneId zoneId, ArchiveBuckets archiveBuckets) { + Map bucketNameByTenant = archiveBuckets.vespaManaged().stream() .flatMap(bucket -> bucket.tenants().stream() .map(tenant -> Map.entry(tenant, bucket.bucketName()))) .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainer.java index eed4fd0245d..b2ed0941c8e 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainer.java @@ -1,12 +1,10 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.maintenance; -import com.google.common.collect.Maps; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.jdisc.Metric; import com.yahoo.vespa.hosted.controller.Controller; -import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBucket; import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveService; import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry; import com.yahoo.vespa.hosted.controller.archive.CuratorArchiveBucketDb; @@ -17,11 +15,8 @@ import com.yahoo.vespa.hosted.controller.tenant.Tenant; import java.time.Duration; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; -import static java.util.stream.Collectors.groupingBy; - /** * Update archive access permissions with roles from tenants * @@ -48,7 +43,7 @@ public class ArchiveAccessMaintainer extends ControllerMaintainer { protected double maintain() { // Count buckets - so we can alert if we get close to the AWS account limit of 1000 zoneRegistry.zonesIncludingSystem().all().zones().forEach(z -> - metric.set(bucketCountMetricName, archiveBucketDb.buckets(z.getVirtualId()).size(), + metric.set(bucketCountMetricName, archiveBucketDb.buckets(z.getVirtualId()).vespaManaged().size(), metric.createContext(Map.of( "zone", z.getVirtualId().value(), "cloud", z.getCloudName().value())))); @@ -57,7 +52,7 @@ public class ArchiveAccessMaintainer extends ControllerMaintainer { ZoneId zoneId = z.getVirtualId(); try { var tenantArchiveAccessRoles = cloudTenantArchiveExternalAccessRoles(); - var buckets = archiveBucketDb.buckets(zoneId); + var buckets = archiveBucketDb.buckets(zoneId).vespaManaged(); archiveService.updatePolicies(zoneId, buckets, tenantArchiveAccessRoles); } catch (Exception e) { throw new RuntimeException("Failed to maintain archive access in " + zoneId.value(), e); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializer.java index a4c7c50085c..f40193510ce 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializer.java @@ -1,12 +1,15 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.persistence; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.TenantName; 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.controller.api.integration.archive.ArchiveBucket; +import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBuckets; +import com.yahoo.vespa.hosted.controller.api.integration.archive.TenantManagedArchiveBucket; +import com.yahoo.vespa.hosted.controller.api.integration.archive.VespaManagedArchiveBucket; import java.util.Set; import java.util.stream.Collectors; @@ -25,46 +28,64 @@ public class ArchiveBucketsSerializer { // - 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 final static String bucketsFieldName = "buckets"; + private final static String vespaManagedBucketsFieldName = "buckets"; + private final static String tenantManagedBucketsFieldName = "tenantManagedBuckets"; private final static String bucketNameFieldName = "bucketName"; private final static String keyArnFieldName = "keyArn"; private final static String tenantsFieldName = "tenantIds"; + private final static String accountFieldName = "account"; + private final static String updatedAtFieldName = "updatedAt"; - public static Slime toSlime(Set archiveBuckets) { + public static Slime toSlime(ArchiveBuckets archiveBuckets) { Slime slime = new Slime(); Cursor rootObject = slime.setObject(); - Cursor bucketsArray = rootObject.setArray(bucketsFieldName); - archiveBuckets.forEach(bucket -> { - Cursor cursor = bucketsArray.addObject(); - cursor.setString(bucketNameFieldName, bucket.bucketName()); - cursor.setString(keyArnFieldName, bucket.keyArn()); - Cursor tenants = cursor.setArray(tenantsFieldName); - bucket.tenants().forEach(tenantName -> tenants.addString(tenantName.value())); - } - ); + Cursor vespaBucketsArray = rootObject.setArray(vespaManagedBucketsFieldName); + archiveBuckets.vespaManaged().forEach(bucket -> { + Cursor cursor = vespaBucketsArray.addObject(); + cursor.setString(bucketNameFieldName, bucket.bucketName()); + cursor.setString(keyArnFieldName, bucket.keyArn()); + Cursor tenants = cursor.setArray(tenantsFieldName); + bucket.tenants().forEach(tenantName -> tenants.addString(tenantName.value())); + }); + + Cursor tenantBucketsArray = rootObject.setArray(tenantManagedBucketsFieldName); + archiveBuckets.tenantManaged().forEach(bucket -> { + Cursor cursor = tenantBucketsArray.addObject(); + cursor.setString(bucketNameFieldName, bucket.bucketName()); + cursor.setString(accountFieldName, bucket.cloudAccount().value()); + cursor.setLong(updatedAtFieldName, bucket.updatedAt().toEpochMilli()); + }); return slime; } - public static Set fromSlime(Inspector inspector) { - return SlimeUtils.entriesStream(inspector.field(bucketsFieldName)) - .map(ArchiveBucketsSerializer::fromInspector) - .collect(Collectors.toUnmodifiableSet()); + public static ArchiveBuckets fromSlime(Slime slime) { + Inspector inspector = slime.get(); + return new ArchiveBuckets( + SlimeUtils.entriesStream(inspector.field(vespaManagedBucketsFieldName)) + .map(ArchiveBucketsSerializer::vespaManagedArchiveBucketFromInspector) + .collect(Collectors.toUnmodifiableSet()), + SlimeUtils.entriesStream(inspector.field(tenantManagedBucketsFieldName)) + .map(ArchiveBucketsSerializer::tenantManagedArchiveBucketFromInspector) + .collect(Collectors.toUnmodifiableSet())); } - private static ArchiveBucket fromInspector(Inspector inspector) { + private static VespaManagedArchiveBucket vespaManagedArchiveBucketFromInspector(Inspector inspector) { Set tenants = SlimeUtils.entriesStream(inspector.field(tenantsFieldName)) .map(i -> TenantName.from(i.asString())) .collect(Collectors.toUnmodifiableSet()); - return new ArchiveBucket( + return new VespaManagedArchiveBucket( inspector.field(bucketNameFieldName).asString(), inspector.field(keyArnFieldName).asString()) .withTenants(tenants); } - public static Set fromJsonString(String zkData) { - return fromSlime(SlimeUtils.jsonToSlime(zkData).get()); + private static TenantManagedArchiveBucket tenantManagedArchiveBucketFromInspector(Inspector inspector) { + return new TenantManagedArchiveBucket( + inspector.field(bucketNameFieldName).asString(), + CloudAccount.from(inspector.field(accountFieldName).asString()), + SlimeUtils.instant(inspector.field(updatedAtFieldName))); } } 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 f4980073d6c..d4e6d7af4b4 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 @@ -19,7 +19,7 @@ import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId; import com.yahoo.vespa.hosted.controller.api.identifiers.ControllerVersion; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; -import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBucket; +import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBuckets; import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId; @@ -632,12 +632,12 @@ public class CuratorDb { // -------------- Archive buckets ----------------------------------------- - public Set readArchiveBuckets(ZoneId zoneId) { - return curator.getData(archiveBucketsPath(zoneId)).map(String::new).map(ArchiveBucketsSerializer::fromJsonString) - .orElseGet(Set::of); + public ArchiveBuckets readArchiveBuckets(ZoneId zoneId) { + return readSlime(archiveBucketsPath(zoneId)).map(ArchiveBucketsSerializer::fromSlime) + .orElse(ArchiveBuckets.EMPTY); } - public void writeArchiveBuckets(ZoneId zoneid, Set archiveBuckets) { + public void writeArchiveBuckets(ZoneId zoneid, ArchiveBuckets archiveBuckets) { curator.set(archiveBucketsPath(zoneid), asJson(ArchiveBucketsSerializer.toSlime(archiveBuckets))); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDbTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDbTest.java index 8bad8efc322..9770133dcfb 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDbTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDbTest.java @@ -5,7 +5,8 @@ import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.ControllerTester; -import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBucket; +import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBuckets; +import com.yahoo.vespa.hosted.controller.api.integration.archive.VespaManagedArchiveBucket; import org.apache.curator.shaded.com.google.common.collect.Streams; import org.junit.jupiter.api.Test; @@ -21,12 +22,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class CuratorArchiveBucketDbTest { @Test - void archiveUriFor() { + void archiveUriForTenant() { ControllerTester tester = new ControllerTester(SystemName.Public); CuratorArchiveBucketDb bucketDb = new CuratorArchiveBucketDb(tester.controller()); tester.curator().writeArchiveBuckets(ZoneId.defaultId(), - Set.of(new ArchiveBucket("existingBucket", "keyArn").withTenant(TenantName.defaultName()))); + ArchiveBuckets.EMPTY.with(new VespaManagedArchiveBucket("existingBucket", "keyArn").withTenant(TenantName.defaultName()))); // Finds existing bucket in db assertEquals(Optional.of(URI.create("s3://existingBucket/")), bucketDb.archiveUriFor(ZoneId.defaultId(), TenantName.defaultName(), true)); @@ -50,11 +51,11 @@ public class CuratorArchiveBucketDbTest { Set existingBucketTenants = Streams.concat(Stream.of(TenantName.defaultName()), IntStream.range(0, 4).mapToObj(i -> TenantName.from("tenant" + i))).collect(Collectors.toUnmodifiableSet()); assertEquals( Set.of( - new ArchiveBucket("existingBucket", "keyArn").withTenants(existingBucketTenants), - new ArchiveBucket("bucketName", "keyArn").withTenant(TenantName.from("lastDrop"))), - bucketDb.buckets(ZoneId.defaultId())); + new VespaManagedArchiveBucket("existingBucket", "keyArn").withTenants(existingBucketTenants), + new VespaManagedArchiveBucket("bucketName", "keyArn").withTenant(TenantName.from("lastDrop"))), + bucketDb.buckets(ZoneId.defaultId()).vespaManaged()); assertEquals( - Set.of(new ArchiveBucket("bucketName", "keyArn").withTenant(TenantName.from("firstInZone"))), - bucketDb.buckets(ZoneId.from("prod.us-east-3"))); + Set.of(new VespaManagedArchiveBucket("bucketName", "keyArn").withTenant(TenantName.from("firstInZone"))), + bucketDb.buckets(ZoneId.from("prod.us-east-3")).vespaManaged()); } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java index c10b77d853a..0490a9bdcc5 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java @@ -7,7 +7,6 @@ import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.jdisc.test.MockMetric; import com.yahoo.vespa.hosted.controller.ControllerTester; import com.yahoo.vespa.hosted.controller.LockedTenant; -import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBucket; import com.yahoo.vespa.hosted.controller.api.integration.archive.MockArchiveService; import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import com.yahoo.vespa.hosted.controller.tenant.Tenant; @@ -15,8 +14,6 @@ import org.junit.jupiter.api.Test; import java.time.Duration; import java.util.Map; -import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -37,7 +34,6 @@ public class ArchiveAccessMaintainerTest { ZoneId testZone = ZoneId.from("prod.aws-us-east-1c"); tester.controller().archiveBucketDb().archiveUriFor(testZone, tenant1, true); - var testBucket = new ArchiveBucket("bucketName", "keyArn").withTenant(tenant1); MockArchiveService archiveService = (MockArchiveService) tester.controller().serviceRegistry().archiveService(); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdaterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdaterTest.java index 7c487e7249b..ad37c4a985d 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdaterTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdaterTest.java @@ -1,12 +1,15 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.maintenance; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.ControllerTester; -import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBucket; +import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBuckets; import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveUriUpdate; +import com.yahoo.vespa.hosted.controller.api.integration.archive.VespaManagedArchiveBucket; +import com.yahoo.vespa.hosted.controller.api.integration.configserver.ArchiveUris; import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.application.SystemApplication; @@ -17,7 +20,6 @@ import org.junit.jupiter.api.Test; import java.net.URI; import java.time.Duration; -import java.util.LinkedHashSet; import java.util.Map; import java.util.stream.Collectors; @@ -42,36 +44,35 @@ public class ArchiveUriUpdaterTest { // Initially we should only is the bucket for hosted-vespa tenant updater.maintain(); - assertArchiveUris(Map.of(TenantName.from("hosted-vespa"), "s3://bucketName/"), zone); - assertArchiveUris(Map.of(TenantName.from("hosted-vespa"), "s3://bucketName/"), ZoneId.from("prod", "controller")); + assertArchiveUris(zone, Map.of(TenantName.from("hosted-vespa"), "s3://bucketName/"), Map.of()); + assertArchiveUris(ZoneId.from("prod", "controller"), Map.of(TenantName.from("hosted-vespa"), "s3://bucketName/"), Map.of()); // Archive service now has URI for tenant1, but tenant1 is not deployed in zone setBucketNameInService(Map.of(tenant1, "uri-1"), zone); updater.maintain(); - assertArchiveUris(Map.of(TenantName.from("hosted-vespa"), "s3://bucketName/"), zone); + assertArchiveUris(zone, Map.of(TenantName.from("hosted-vespa"), "s3://bucketName/"), Map.of()); deploy(application, zone); updater.maintain(); - assertArchiveUris(Map.of(tenant1, "s3://uri-1/", tenantInfra, "s3://bucketName/"), zone); + assertArchiveUris(zone, Map.of(tenant1, "s3://uri-1/", tenantInfra, "s3://bucketName/"), Map.of()); // URI for tenant1 should be updated and removed for tenant2 setArchiveUriInNodeRepo(Map.of(tenant1, "wrong-uri", tenant2, "uri-2"), zone); updater.maintain(); - assertArchiveUris(Map.of(tenant1, "s3://uri-1/", tenantInfra, "s3://bucketName/"), zone); + assertArchiveUris(zone, Map.of(tenant1, "s3://uri-1/", tenantInfra, "s3://bucketName/"), Map.of()); } - private void assertArchiveUris(Map expectedUris, ZoneId zone) { - Map actualUris = tester.controller().serviceRegistry().configServer().nodeRepository() - .getArchiveUris(zone).tenantArchiveUris().entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString())); - assertEquals(expectedUris, actualUris); + private void assertArchiveUris(ZoneId zone, Map expectedTenantUris, Map expectedAccountUris) { + ArchiveUris archiveUris = tester.controller().serviceRegistry().configServer().nodeRepository().getArchiveUris(zone); + assertEquals(expectedTenantUris, archiveUris.tenantArchiveUris().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString()))); + assertEquals(expectedAccountUris, archiveUris.accountArchiveUris().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString()))); } private void setBucketNameInService(Map bucketNames, ZoneId zone) { - var archiveBuckets = new LinkedHashSet<>(tester.controller().curator().readArchiveBuckets(zone)); - bucketNames.forEach((tenantName, bucketName) -> - archiveBuckets.add(new ArchiveBucket(bucketName, "keyArn").withTenant(tenantName))); - tester.controller().curator().writeArchiveBuckets(zone, archiveBuckets); + ArchiveBuckets buckets = tester.controller().curator().readArchiveBuckets(zone); + for (var entry : bucketNames.entrySet()) + buckets = buckets.with(new VespaManagedArchiveBucket(entry.getValue(), "keyArn").withTenant(entry.getKey())); + tester.controller().curator().writeArchiveBuckets(zone, buckets); } private void setArchiveUriInNodeRepo(Map archiveUris, ZoneId zone) { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializerTest.java index 82c5a6fc0c1..1d1b1124d22 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializerTest.java @@ -1,11 +1,15 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.persistence; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.TenantName; -import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBucket; +import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBuckets; +import com.yahoo.vespa.hosted.controller.api.integration.archive.TenantManagedArchiveBucket; +import com.yahoo.vespa.hosted.controller.api.integration.archive.VespaManagedArchiveBucket; import org.junit.jupiter.api.Test; -import java.util.LinkedHashSet; +import java.time.Instant; +import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -13,17 +17,12 @@ public class ArchiveBucketsSerializerTest { @Test void serdes() { - var testTenants = new LinkedHashSet(); - testTenants.add(TenantName.from("tenant1")); - testTenants.add(TenantName.from("tenant2")); + ArchiveBuckets archiveBuckets = new ArchiveBuckets( + Set.of(new VespaManagedArchiveBucket("bucket1Name", "key1Arn").withTenants(Set.of(TenantName.from("t1"), TenantName.from("t2"))), + new VespaManagedArchiveBucket("bucket2Name", "key2Arn").withTenant(TenantName.from("t3"))), + Set.of(new TenantManagedArchiveBucket("bucket3Name", CloudAccount.from("acct-1"), Instant.ofEpochMilli(1234)), + new TenantManagedArchiveBucket("bucket4Name", CloudAccount.from("acct-2"), Instant.ofEpochMilli(5678)))); - var testBuckets = new LinkedHashSet(); - testBuckets.add(new ArchiveBucket("bucket1Name", "key1Arn").withTenants(testTenants)); - testBuckets.add(new ArchiveBucket("bucket2Name", "key2Arn")); - - String zkData = "{\"buckets\":[{\"bucketName\":\"bucket1Name\",\"keyArn\":\"key1Arn\",\"tenantIds\":[\"tenant1\",\"tenant2\"]},{\"bucketName\":\"bucket2Name\",\"keyArn\":\"key2Arn\",\"tenantIds\":[]}]}"; - - assertEquals(testBuckets, ArchiveBucketsSerializer.fromJsonString(zkData)); - assertEquals(testBuckets, ArchiveBucketsSerializer.fromJsonString(ArchiveBucketsSerializer.toSlime(testBuckets).toString())); + assertEquals(archiveBuckets, ArchiveBucketsSerializer.fromSlime(ArchiveBucketsSerializer.toSlime(archiveBuckets))); } } -- cgit v1.2.3