diff options
author | Valerij Fredriksen <freva@users.noreply.github.com> | 2023-03-02 16:51:01 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-03-02 16:51:01 +0100 |
commit | fa987c7e8dfaa7293fc99459fd942d001fdef11f (patch) | |
tree | 1dc41dad02166dc624baf9af70f0fd33cd0d97d9 | |
parent | 8a0918e280bf7ebd8f92ec84f76a5d32aa573470 (diff) | |
parent | db840edf14445c07f991cf4a591aeb552f457962 (diff) |
Merge pull request #26279 from vespa-engine/freva/propagate-account-archive-uri
Propagate account archive URI to node-repo
16 files changed, 340 insertions, 159 deletions
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<VespaManagedArchiveBucket> vespaManaged, + Set<TenantManagedArchiveBucket> tenantManaged) { + public static final ArchiveBuckets EMPTY = new ArchiveBuckets(Set.of(), Set.of()); + + public ArchiveBuckets(Set<VespaManagedArchiveBucket> vespaManaged, Set<TenantManagedArchiveBucket> 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<VespaManagedArchiveBucket> 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<TenantManagedArchiveBucket> 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 46e7fb48553..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 @@ -1,12 +1,14 @@ // 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 com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import java.net.URI; import java.util.Map; +import java.util.Optional; import java.util.Set; /** @@ -17,11 +19,13 @@ import java.util.Set; */ public interface ArchiveService { - ArchiveBucket createArchiveBucketFor(ZoneId zoneId); + VespaManagedArchiveBucket createArchiveBucketFor(ZoneId zoneId); - void updatePolicies(ZoneId zoneId, Set<ArchiveBucket> buckets, Map<TenantName,ArchiveAccess> authorizeAccessByTenantName); + void updatePolicies(ZoneId zoneId, Set<VespaManagedArchiveBucket> buckets, Map<TenantName,ArchiveAccess> authorizeAccessByTenantName); - boolean canAddTenantToBucket(ZoneId zoneId, ArchiveBucket bucket); + boolean canAddTenantToBucket(ZoneId zoneId, VespaManagedArchiveBucket bucket); - URI bucketURI(ZoneId zoneId, String bucketName, TenantName tenantName); + Optional<String> findEnclaveArchiveBucket(ZoneId zoneId, CloudAccount cloudAccount); + + URI bucketURI(ZoneId zoneId, String bucketName); } 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 a2847439ce7..7461d3aa47e 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 @@ -1,16 +1,18 @@ // 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 com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import java.net.URI; +import java.time.Clock; import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; -import java.util.TreeMap; /** * @author freva @@ -18,29 +20,54 @@ import java.util.TreeMap; */ public class MockArchiveService implements ArchiveService { - - public Set<ArchiveBucket> archiveBuckets = new HashSet<>(); + private final Map<ZoneId, Set<TenantManagedArchiveBucket>> tenantArchiveBucketsByZone = new HashMap<>(); + public Set<VespaManagedArchiveBucket> archiveBuckets = new HashSet<>(); public Map<TenantName, ArchiveAccess> authorizeAccessByTenantName = new HashMap<>(); + private final Clock clock; + + public MockArchiveService(Clock clock) { + this.clock = clock; + } @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<ArchiveBucket> buckets, Map<TenantName, ArchiveAccess> authorizeAccessByTenantName) { + public void updatePolicies(ZoneId zoneId, Set<VespaManagedArchiveBucket> buckets, Map<TenantName, ArchiveAccess> 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; } @Override - public URI bucketURI(ZoneId zoneId, String bucketName, TenantName tenantName) { - return URI.create(String.format("s3://%s/%s/", bucketName, tenantName.value())); + public Optional<String> findEnclaveArchiveBucket(ZoneId zoneId, CloudAccount cloudAccount) { + return tenantArchiveBucketsByZone.getOrDefault(zoneId, Set.of()).stream() + .filter(bucket -> bucket.cloudAccount().equals(cloudAccount)) + .findFirst() + .map(TenantManagedArchiveBucket::bucketName); + } + + @Override + public URI bucketURI(ZoneId zoneId, String bucketName) { + return URI.create(String.format("s3://%s/", bucketName)); + } + + + public void setEnclaveArchiveBucket(ZoneId zoneId, CloudAccount cloudAccount, String bucketName) { + removeEnclaveArchiveBucket(zoneId, cloudAccount); + tenantArchiveBucketsByZone.computeIfAbsent(zoneId, z -> new HashSet<>()) + .add(new TenantManagedArchiveBucket(bucketName, cloudAccount, clock.instant())); + } + + public void removeEnclaveArchiveBucket(ZoneId zoneId, CloudAccount cloudAccount) { + Optional.ofNullable(tenantArchiveBucketsByZone.get(zoneId)) + .ifPresent(set -> set.removeIf(bucket -> bucket.cloudAccount().equals(cloudAccount))); } } 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/ArchiveBucket.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/VespaManagedArchiveBucket.java index be3b87ddc5c..c80e9b3780d 100644 --- 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/VespaManagedArchiveBucket.java @@ -8,20 +8,21 @@ import java.util.Objects; import java.util.Set; /** - * Represents an S3 bucket used to store archive data - logs, heap/core dumps, etc. + * 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 ArchiveBucket { +public class VespaManagedArchiveBucket { private final String bucketName; private final String keyArn; private final Set<TenantName> tenants; - public ArchiveBucket(String bucketName, String keyArn) { + public VespaManagedArchiveBucket(String bucketName, String keyArn) { this(bucketName, keyArn, Set.of()); } - private ArchiveBucket(String bucketName, String keyArn, Set<TenantName> tenants) { + private VespaManagedArchiveBucket(String bucketName, String keyArn, Set<TenantName> tenants) { this.bucketName = bucketName; this.keyArn = keyArn; this.tenants = Set.copyOf(tenants); @@ -39,19 +40,19 @@ public class ArchiveBucket { return tenants; } - public ArchiveBucket withTenant(TenantName tenant) { + public VespaManagedArchiveBucket withTenant(TenantName tenant) { return withTenants(Set.of(tenant)); } - public ArchiveBucket withTenants(Set<TenantName> tenants) { - return new ArchiveBucket(bucketName, keyArn, Sets.union(this.tenants, tenants)); + public VespaManagedArchiveBucket withTenants(Set<TenantName> 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; - ArchiveBucket that = (ArchiveBucket) o; + VespaManagedArchiveBucket that = (VespaManagedArchiveBucket) o; return bucketName.equals(that.bucketName) && keyArn.equals(that.keyArn) && tenants.equals(that.tenants); } 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 ac32fe5799d..962bd144a21 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 @@ -1,18 +1,22 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.archive; +import com.yahoo.config.provision.CloudAccount; 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.TenantManagedArchiveBucket; +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.time.Clock; +import java.time.Duration; +import java.time.Instant; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @@ -23,81 +27,100 @@ import java.util.stream.Collectors; */ public class CuratorArchiveBucketDb { + private static final Duration ENCLAVE_BUCKET_CACHE_LIFETIME = Duration.ofMinutes(60); + /** * Archive URIs are often requested because they are returned in /application/v4 API. Since they * never change, it's safe to cache them and only update on misses */ private final Map<ZoneId, Map<TenantName, String>> archiveUriCache = new ConcurrentHashMap<>(); + private final Map<ZoneId, Map<CloudAccount, TenantManagedArchiveBucket>> tenantArchiveCache = new ConcurrentHashMap<>(); private final ArchiveService archiveService; private final CuratorDb curatorDb; + private final Clock clock; public CuratorArchiveBucketDb(Controller controller) { this.archiveService = controller.serviceRegistry().archiveService(); this.curatorDb = controller.curator(); + this.clock = controller.clock(); } public Optional<URI> 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, tenant)); + .map(bucketName -> archiveService.bucketURI(zoneId, bucketName)); + } + + public Optional<URI> archiveUriFor(ZoneId zoneId, CloudAccount account, boolean searchIfMissing) { + Instant updatedAfter = searchIfMissing ? clock.instant().minus(ENCLAVE_BUCKET_CACHE_LIFETIME) : Instant.MIN; + return getBucketNameFromCache(zoneId, account, updatedAfter) + .or(() -> { + if (!searchIfMissing) return Optional.empty(); + try (var lock = curatorDb.lockArchiveBuckets(zoneId)) { + ArchiveBuckets archiveBuckets = buckets(zoneId); + updateArchiveUriCache(zoneId, archiveBuckets); + + return getBucketNameFromCache(zoneId, account, updatedAfter) + .or(() -> archiveService.findEnclaveArchiveBucket(zoneId, account) + .map(bucketName -> { + var bucket = new TenantManagedArchiveBucket(bucketName, account, clock.instant()); + ArchiveBuckets updated = archiveBuckets.with(bucket); + curatorDb.writeArchiveBuckets(zoneId, updated); + updateArchiveUriCache(zoneId, updated); + return bucket; + })); + } + }) + .map(TenantManagedArchiveBucket::bucketName) + .map(bucketName -> archiveService.bucketURI(zoneId, bucketName)); } private String assignToBucket(ZoneId zoneId, TenantName tenant) { try (var lock = curatorDb.lockArchiveBuckets(zoneId)) { - Set<ArchiveBucket> 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<ArchiveBucket> unfilledBucket = zoneBuckets.stream() + VespaManagedArchiveBucket bucketToAssignTo = archiveBuckets.vespaManaged().stream() .filter(bucket -> archiveService.canAddTenantToBucket(zoneId, bucket)) - .findAny(); - - // And place the tenant in that bucket. - if (unfilledBucket.isPresent()) { - var unfilled = unfilledBucket.get(); - - zoneBuckets.remove(unfilled); - zoneBuckets.add(unfilled.withTenant(tenant)); - curatorDb.writeArchiveBuckets(zoneId, zoneBuckets); + .findAny() + // Or create a new one + .orElseGet(() -> archiveService.createArchiveBucketFor(zoneId)); - return unfilled.bucketName(); - } + ArchiveBuckets updated = archiveBuckets.with(bucketToAssignTo.withTenant(tenant)); + curatorDb.writeArchiveBuckets(zoneId, updated); + updateArchiveUriCache(zoneId, updated); - // 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<ArchiveBucket> buckets(ZoneId zoneId) { + public ArchiveBuckets buckets(ZoneId zoneId) { return curatorDb.readArchiveBuckets(zoneId); } - private Optional<String> findAndUpdateArchiveUriCache(ZoneId zoneId, TenantName tenant, Set<ArchiveBucket> zoneBuckets) { - Optional<String> bucketName = zoneBuckets.stream() - .filter(bucket -> bucket.tenants().contains(tenant)) - .findAny() - .map(ArchiveBucket::bucketName); - if (bucketName.isPresent()) updateArchiveUriCache(zoneId, zoneBuckets); - return bucketName; - } - private Optional<String> getBucketNameFromCache(ZoneId zoneId, TenantName tenantName) { return Optional.ofNullable(archiveUriCache.get(zoneId)).map(map -> map.get(tenantName)); } - private void updateArchiveUriCache(ZoneId zoneId, Set<ArchiveBucket> zoneBuckets) { - Map<TenantName, String> bucketNameByTenant = zoneBuckets.stream() - .flatMap(bucket -> bucket.tenants().stream() - .map(tenant -> Map.entry(tenant, bucket.bucketName()))) + private Optional<TenantManagedArchiveBucket> getBucketNameFromCache(ZoneId zoneId, CloudAccount cloudAccount, Instant updatedAfter) { + return Optional.ofNullable(tenantArchiveCache.get(zoneId)) + .map(map -> map.get(cloudAccount)) + .filter(bucket -> bucket.updatedAt().isAfter(updatedAfter)); + } + + private void updateArchiveUriCache(ZoneId zoneId, ArchiveBuckets archiveBuckets) { + Map<TenantName, String> 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)); archiveUriCache.put(zoneId, bucketNameByTenant); + + Map<CloudAccount, TenantManagedArchiveBucket> bucketByAccount = archiveBuckets.tenantManaged().stream() + .collect(Collectors.toUnmodifiableMap(TenantManagedArchiveBucket::cloudAccount, bucket -> bucket)); + tenantArchiveCache.put(zoneId, bucketByAccount); } } 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/maintenance/ArchiveUriUpdater.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdater.java index ddb1365f2de..1083e545b33 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdater.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdater.java @@ -1,10 +1,12 @@ // 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.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.ApplicationController; import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveUriUpdate; import com.yahoo.vespa.hosted.controller.api.integration.configserver.ArchiveUris; import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository; @@ -18,6 +20,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.logging.Level; +import java.util.stream.Stream; /** * Updates archive URIs for tenants in all zones. @@ -42,14 +45,19 @@ public class ArchiveUriUpdater extends ControllerMaintainer { @Override protected double maintain() { Map<ZoneId, Set<TenantName>> tenantsByZone = new HashMap<>(); + Map<ZoneId, Set<CloudAccount>> accountsByZone = new HashMap<>(); - controller().zoneRegistry().zonesIncludingSystem().reachable().zones().forEach( - z -> tenantsByZone.put(z.getVirtualId(), new HashSet<>(INFRASTRUCTURE_TENANTS))); + controller().zoneRegistry().zonesIncludingSystem().reachable().zones().forEach(zone -> { + tenantsByZone.put(zone.getVirtualId(), new HashSet<>(INFRASTRUCTURE_TENANTS)); + accountsByZone.put(zone.getVirtualId(), new HashSet<>()); + }); for (var application : applications.asList()) { for (var instance : application.instances().values()) { for (var deployment : instance.deployments().values()) { tenantsByZone.get(deployment.zone()).add(instance.id().tenant()); + applications.decideCloudAccountOf(new DeploymentId(instance.id(), deployment.zone()), application.deploymentSpec()) + .ifPresent(account -> accountsByZone.get(deployment.zone()).add(account)); } } } @@ -59,17 +67,29 @@ public class ArchiveUriUpdater extends ControllerMaintainer { try { ArchiveUris zoneArchiveUris = nodeRepository.getArchiveUris(zone); - for (TenantName tenant : tenantsByZone.get(zone)) { - archiveBucketDb.archiveUriFor(zone, tenant, true) - .filter(uri -> !uri.equals(zoneArchiveUris.tenantArchiveUris().get(tenant))) - .ifPresent(uri -> nodeRepository.updateArchiveUri(zone, ArchiveUriUpdate.setArchiveUriFor(tenant, uri))); - } - - zoneArchiveUris.tenantArchiveUris().keySet().stream() - .filter(tenant -> !tenantsByZone.get(zone).contains(tenant)) - .forEach(tenant -> nodeRepository.updateArchiveUri(zone, ArchiveUriUpdate.deleteArchiveUriFor(tenant))); - - // TODO (freva): Update account archive URIs + Stream.of( + // Tenant URIs that need to be added or updated + tenantsByZone.get(zone).stream() + .flatMap(tenant -> archiveBucketDb.archiveUriFor(zone, tenant, true) + .filter(uri -> !uri.equals(zoneArchiveUris.tenantArchiveUris().get(tenant))) + .map(uri -> ArchiveUriUpdate.setArchiveUriFor(tenant, uri)) + .stream()), + // Account URIs that need to be added or updated + accountsByZone.get(zone).stream() + .flatMap(account -> archiveBucketDb.archiveUriFor(zone, account, true) + .filter(uri -> !uri.equals(zoneArchiveUris.accountArchiveUris().get(account))) + .map(uri -> ArchiveUriUpdate.setArchiveUriFor(account, uri)) + .stream()), + // Tenant URIs that need to be deleted + zoneArchiveUris.tenantArchiveUris().keySet().stream() + .filter(tenant -> !tenantsByZone.get(zone).contains(tenant)) + .map(ArchiveUriUpdate::deleteArchiveUriFor), + // Account URIs that need to be deleted + zoneArchiveUris.accountArchiveUris().keySet().stream() + .filter(account -> !accountsByZone.get(zone).contains(account)) + .map(ArchiveUriUpdate::deleteArchiveUriFor)) + .flatMap(s -> s) + .forEach(update -> nodeRepository.updateArchiveUri(zone, update)); } catch (Exception e) { log.log(Level.WARNING, "Failed to update archive URI in " + zone + ". Retrying in " + interval() + ". Error: " + Exceptions.toMessageString(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<ArchiveBucket> 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<ArchiveBucket> 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<TenantName> 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<ArchiveBucket> 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<ArchiveBucket> 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<ArchiveBucket> 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 081056e5184..e5571c0e0ca 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 @@ -1,15 +1,21 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.archive; +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.test.ManualClock; +import com.yahoo.vespa.hosted.controller.Controller; 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.MockArchiveService; +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; import java.net.URI; +import java.time.Duration; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -21,27 +27,27 @@ 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/default/")), bucketDb.archiveUriFor(ZoneId.defaultId(), TenantName.defaultName(), true)); + assertEquals(Optional.of(URI.create("s3://existingBucket/")), bucketDb.archiveUriFor(ZoneId.defaultId(), TenantName.defaultName(), true)); // Assigns to existing bucket while there is space IntStream.range(0, 4).forEach(i -> assertEquals( - Optional.of(URI.create("s3://existingBucket/tenant" + i + "/")), bucketDb + Optional.of(URI.create("s3://existingBucket/")), bucketDb .archiveUriFor(ZoneId.defaultId(), TenantName.from("tenant" + i), true))); // Creates new bucket when existing buckets are full - assertEquals(Optional.of(URI.create("s3://bucketName/lastDrop/")), bucketDb.archiveUriFor(ZoneId.defaultId(), TenantName.from("lastDrop"), true)); + assertEquals(Optional.of(URI.create("s3://bucketName/")), bucketDb.archiveUriFor(ZoneId.defaultId(), TenantName.from("lastDrop"), true)); // Creates new bucket when there are no existing buckets in zone - assertEquals(Optional.of(URI.create("s3://bucketName/firstInZone/")), bucketDb.archiveUriFor(ZoneId.from("prod.us-east-3"), TenantName.from("firstInZone"), true)); + assertEquals(Optional.of(URI.create("s3://bucketName/")), bucketDb.archiveUriFor(ZoneId.from("prod.us-east-3"), TenantName.from("firstInZone"), true)); // Does not create bucket if not required assertEquals(Optional.empty(), bucketDb.archiveUriFor(ZoneId.from("prod.us-east-3"), TenantName.from("newTenant"), false)); @@ -50,11 +56,36 @@ public class CuratorArchiveBucketDbTest { Set<TenantName> 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()); + } + + @Test + void archiveUriForAccount() { + Controller controller = new ControllerTester(SystemName.Public).controller(); + CuratorArchiveBucketDb bucketDb = new CuratorArchiveBucketDb(controller); + MockArchiveService service = (MockArchiveService) controller.serviceRegistry().archiveService(); + ManualClock clock = (ManualClock) controller.clock(); + + CloudAccount acc1 = CloudAccount.from("001122334455"); + ZoneId z1 = ZoneId.from("prod.us-east-3"); + + assertEquals(Optional.empty(), bucketDb.archiveUriFor(z1, acc1, true)); // Initially not set + service.setEnclaveArchiveBucket(z1, acc1, "bucket-1"); + assertEquals(Optional.empty(), bucketDb.archiveUriFor(z1, acc1, false)); + assertEquals(Optional.of(URI.create("s3://bucket-1/")), bucketDb.archiveUriFor(z1, acc1, true)); + assertEquals(Optional.of(URI.create("s3://bucket-1/")), bucketDb.archiveUriFor(z1, acc1, false)); + + service.setEnclaveArchiveBucket(z1, acc1, "bucket-2"); + assertEquals(Optional.of(URI.create("s3://bucket-1/")), bucketDb.archiveUriFor(z1, acc1, true)); // Returns old value even with search + + clock.advance(Duration.ofMinutes(61)); // After expiry the cache is expired, new search is performed + assertEquals(Optional.of(URI.create("s3://bucket-1/")), bucketDb.archiveUriFor(z1, acc1, false)); // When requesting without search, return previous value even if expired + assertEquals(Optional.of(URI.create("s3://bucket-2/")), bucketDb.archiveUriFor(z1, acc1, true)); + assertEquals(Optional.of(URI.create("s3://bucket-2/")), bucketDb.archiveUriFor(z1, acc1, false)); } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java index be257daa211..998b371bbf1 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java @@ -37,11 +37,11 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.MockIssueH import com.yahoo.vespa.hosted.controller.api.integration.resource.CostReportConsumerMock; import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClient; import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClientMock; +import com.yahoo.vespa.hosted.controller.api.integration.secrets.EndpointSecretManager; import com.yahoo.vespa.hosted.controller.api.integration.secrets.GcpSecretStore; -import com.yahoo.vespa.hosted.controller.api.integration.secrets.NoopGcpSecretStore; import com.yahoo.vespa.hosted.controller.api.integration.secrets.NoopEndpointSecretManager; +import com.yahoo.vespa.hosted.controller.api.integration.secrets.NoopGcpSecretStore; import com.yahoo.vespa.hosted.controller.api.integration.secrets.NoopTenantSecretService; -import com.yahoo.vespa.hosted.controller.api.integration.secrets.EndpointSecretManager; import com.yahoo.vespa.hosted.controller.api.integration.stubs.DummyOwnershipIssues; import com.yahoo.vespa.hosted.controller.api.integration.stubs.DummySystemMonitor; import com.yahoo.vespa.hosted.controller.api.integration.stubs.LoggingDeploymentIssues; @@ -89,7 +89,7 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg private final ArtifactRegistryMock containerRegistry = new ArtifactRegistryMock(); private final NoopTenantSecretService tenantSecretService = new NoopTenantSecretService(); private final NoopEndpointSecretManager secretManager = new NoopEndpointSecretManager(); - private final ArchiveService archiveService = new MockArchiveService(); + private final ArchiveService archiveService = new MockArchiveService(clock); private final MockChangeRequestClient changeRequestClient = new MockChangeRequestClient(); private final AccessControlService accessControlService = new MockAccessControlService(); private final HorizonClient horizonClient = new MockHorizonClient(); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneRegistryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneRegistryMock.java index 38ff9967ef6..de186109784 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneRegistryMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneRegistryMock.java @@ -266,7 +266,7 @@ public class ZoneRegistryMock extends AbstractComponent implements ZoneRegistry @Override public boolean hasZone(ZoneId zoneId, CloudAccount cloudAccount) { - return hasZone(zoneId) && cloudAccountZones.getOrDefault(cloudAccount, Set.of()).contains(zoneId); + return hasZone(zoneId) && (system.isPublic() || cloudAccountZones.getOrDefault(cloudAccount, Set.of()).contains(zoneId)); } @Override 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 de62a2fc48c..d6b59ef860f 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,23 +1,28 @@ // 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.flags.InMemoryFlagSource; +import com.yahoo.vespa.flags.PermanentFlags; 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.MockArchiveService; +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; -import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage; +import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder; import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext; import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester; import org.junit.jupiter.api.Test; import java.net.URI; import java.time.Duration; -import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -36,42 +41,49 @@ public class ArchiveUriUpdaterTest { var tenant1 = TenantName.from("tenant1"); var tenant2 = TenantName.from("tenant2"); + var account1 = CloudAccount.from("001122334455"); var tenantInfra = SystemApplication.TENANT; var application = tester.newDeploymentContext(tenant1.value(), "app1", "instance1"); ZoneId zone = ZoneId.from("prod", "aws-us-east-1c"); // Initially we should only is the bucket for hosted-vespa tenant updater.maintain(); - assertArchiveUris(Map.of(TenantName.from("hosted-vespa"), "s3://bucketName/hosted-vespa/"), zone); - assertArchiveUris(Map.of(TenantName.from("hosted-vespa"), "s3://bucketName/hosted-vespa/"), 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); + setAccountBucketNameInService(zone, account1, "bkt-1"); updater.maintain(); - assertArchiveUris(Map.of(TenantName.from("hosted-vespa"), "s3://bucketName/hosted-vespa/"), zone); + assertArchiveUris(zone, Map.of(TenantName.from("hosted-vespa"), "s3://bucketName/"), Map.of()); - deploy(application, zone); + ((InMemoryFlagSource) tester.controller().flagSource()) + .withListFlag(PermanentFlags.CLOUD_ACCOUNTS.id(), List.of(account1.value()), String.class); + deploy(application, zone, account1); updater.maintain(); - assertArchiveUris(Map.of(tenant1, "s3://uri-1/tenant1/", tenantInfra, "s3://bucketName/hosted-vespa/"), zone); + assertArchiveUris(zone, Map.of(tenant1, "s3://uri-1/", tenantInfra, "s3://bucketName/"), Map.of(account1, "s3://bkt-1/")); // 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/tenant1/", tenantInfra, "s3://bucketName/hosted-vespa/"), zone); + assertArchiveUris(zone, Map.of(tenant1, "s3://uri-1/", tenantInfra, "s3://bucketName/"), Map.of(account1, "s3://bkt-1/")); } - private void assertArchiveUris(Map<TenantName, String> expectedUris, ZoneId zone) { - Map<TenantName, String> 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<TenantName, String> expectedTenantUris, Map<CloudAccount, String> 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<TenantName, String> 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 setAccountBucketNameInService(ZoneId zone, CloudAccount cloudAccount, String bucketName) { + ((MockArchiveService) tester.controller().serviceRegistry().archiveService()).setEnclaveArchiveBucket(zone, cloudAccount, bucketName); } private void setArchiveUriInNodeRepo(Map<TenantName, String> archiveUris, ZoneId zone) { @@ -79,8 +91,11 @@ public class ArchiveUriUpdaterTest { archiveUris.forEach((tenant, uri) -> nodeRepository.updateArchiveUri(zone, ArchiveUriUpdate.setArchiveUriFor(tenant, URI.create(uri)))); } - private void deploy(DeploymentContext application, ZoneId zone) { - application.runJob(JobType.deploymentTo(zone), new ApplicationPackage(new byte[0])); + private void deploy(DeploymentContext application, ZoneId zone, CloudAccount cloudAccount) { + application.submit(new ApplicationPackageBuilder() + .cloudAccount(cloudAccount.value()) + .region(zone.region().value()) + .build()).deploy(); } } 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<TenantName>(); - 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<ArchiveBucket>(); - 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))); } } |