diff options
author | Andreas Eriksen <andreer@verizonmedia.com> | 2021-04-07 09:04:59 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-07 09:04:59 +0200 |
commit | bccd68f8f9a7eb0830d136f8b034ae4f40cc819c (patch) | |
tree | 94a641c4e2ca5041959479338ee5cbb1c813d4f3 /controller-server | |
parent | 5d89a774c5133332b4c6486f344b1543e1f04dce (diff) |
archive bucket db (#17073)
Diffstat (limited to 'controller-server')
13 files changed, 447 insertions, 13 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java index abc0784396c..c6dddc0f223 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java @@ -17,6 +17,7 @@ import com.yahoo.vespa.flags.FlagSource; import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry; import com.yahoo.vespa.hosted.controller.api.integration.maven.MavenRepository; import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry; +import com.yahoo.vespa.hosted.controller.archive.CuratorArchiveBucketDb; import com.yahoo.vespa.hosted.controller.auditlog.AuditLogger; import com.yahoo.vespa.hosted.controller.config.ControllerConfig; import com.yahoo.vespa.hosted.controller.deployment.JobController; @@ -80,6 +81,7 @@ public class Controller extends AbstractComponent { private final RoutingController routingController; private final ControllerConfig controllerConfig; private final SecretStore secretStore; + private final CuratorArchiveBucketDb archiveBucketDb; /** * Creates a controller @@ -115,6 +117,7 @@ public class Controller extends AbstractComponent { routingController = new RoutingController(this, Objects.requireNonNull(rotationsConfig, "RotationsConfig cannot be null")); auditLogger = new AuditLogger(curator, clock); jobControl = new JobControl(new JobControlFlags(curator, flagSource)); + archiveBucketDb = new CuratorArchiveBucketDb(this); this.controllerConfig = controllerConfig; this.secretStore = secretStore; @@ -302,4 +305,8 @@ public class Controller extends AbstractComponent { public JobControl jobControl() { return jobControl; } + + public CuratorArchiveBucketDb archiveBucketDb() { + return archiveBucketDb; + } } 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 new file mode 100644 index 00000000000..8c68acc7c37 --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDb.java @@ -0,0 +1,109 @@ +// Copyright 2021 Oath Inc. 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.google.inject.Inject; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.vespa.flags.FetchVector; +import com.yahoo.vespa.flags.Flags; +import com.yahoo.vespa.flags.StringFlag; +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.ArchiveBucketDb; +import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveService; +import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; +import org.jetbrains.annotations.NotNull; + +import java.net.URI; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +/** + * This class decides which tenant goes in what bucket, and creates new buckets when required. + * + * @author andreer + */ +public class CuratorArchiveBucketDb implements ArchiveBucketDb { + + /** + * Due to policy limits, we can't put data for more than this many tenants in a bucket. + * Policy size limit is 20kb, with approx. 500 bytes of policy required per tenant = 40 tenants. + * We set the maximum a bit lower to have a solid margin of error. + */ + private final static int TENANTS_PER_BUCKET = 30; + + private final ArchiveService archiveService; + private final CuratorDb curatorDb; + private final StringFlag bucketNameFlag; + + @Inject + public CuratorArchiveBucketDb(Controller controller) { + this.archiveService = controller.serviceRegistry().archiveService(); + this.curatorDb = controller.curator(); + this.bucketNameFlag = Flags.SYNC_HOST_LOGS_TO_S3_BUCKET.bindTo(controller.flagSource()); + } + + @Override + public Optional<URI> archiveUriFor(ZoneId zoneId, TenantName tenant) { + String bucketName = bucketNameFlag + .with(FetchVector.Dimension.ZONE_ID, zoneId.value()) + .with(FetchVector.Dimension.TENANT_ID, tenant.value()) + .value(); + + if (bucketName.isBlank()) return Optional.empty(); + + if ("auto".equals(bucketName)) bucketName = findOrAssignBucket(zoneId, tenant); + + return Optional.of(URI.create(String.format("s3://%s/%s/", bucketName, tenant.value()))); + } + + private String findOrAssignBucket(ZoneId zoneId, TenantName tenant) { + var zoneBuckets = curatorDb.readArchiveBuckets(zoneId); + return find(tenant, zoneBuckets).orElseGet(() -> assignToBucket(zoneId, tenant)); + } + + private String assignToBucket(ZoneId zoneId, TenantName tenant) { + try (var lock = curatorDb.lockArchiveBuckets(zoneId)) { + Set<ArchiveBucket> zoneBuckets = new HashSet<>(curatorDb.readArchiveBuckets(zoneId)); + + return find(tenant, zoneBuckets) // 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() + .filter(bucket -> bucket.tenants().size() < TENANTS_PER_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); + + 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); + return newBucket.bucketName(); + }); + } + } + + @NotNull + private Optional<String> find(TenantName tenant, Set<ArchiveBucket> zoneBuckets) { + return zoneBuckets.stream() + .filter(bucket -> bucket.tenants().contains(tenant)) + .findAny() + .map(ArchiveBucket::bucketName); + } + + @Override + public Set<ArchiveBucket> buckets(ZoneId zoneId) { + return curatorDb.readArchiveBuckets(zoneId); + } +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/archive/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/archive/package-info.java new file mode 100644 index 00000000000..c93eb56d294 --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/archive/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2021 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.vespa.hosted.controller.archive; + +import com.yahoo.osgi.annotation.ExportPackage; 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 new file mode 100644 index 00000000000..826b411df9e --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainer.java @@ -0,0 +1,52 @@ +// Copyright 2021 Oath Inc. 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.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBucketDb; +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.tenant.CloudTenant; +import com.yahoo.vespa.hosted.controller.tenant.Tenant; + +import java.time.Duration; +import java.util.stream.Collectors; + +/** + * Update archive access permissions with roles from tenants + * + * @author andreer + */ +public class ArchiveAccessMaintainer extends ControllerMaintainer { + + private final ArchiveBucketDb archiveBucketDb; + private final ArchiveService archiveService; + private final ZoneRegistry zoneRegistry; + + public ArchiveAccessMaintainer(Controller controller, Duration interval) { + super(controller, interval); + this.archiveBucketDb = controller.archiveBucketDb(); + this.archiveService = controller.serviceRegistry().archiveService(); + this.zoneRegistry = controller().zoneRegistry(); + } + + @Override + protected boolean maintain() { + var tenantArchiveAccessRoles = controller().tenants().asList().stream() + .filter(t -> t instanceof CloudTenant) + .map(t -> (CloudTenant) t) + .filter(t -> t.archiveAccessRole().isPresent()) + .collect(Collectors.toUnmodifiableMap( + Tenant::name, cloudTenant -> cloudTenant.archiveAccessRole().orElseThrow())); + + zoneRegistry.zones().controllerUpgraded().ids().forEach(zoneId -> + archiveBucketDb.buckets(zoneId).forEach(archiveBucket -> + archiveService.updateBucketAndKeyPolicy(zoneId, archiveBucket, + Maps.filterEntries(tenantArchiveAccessRoles, + entry -> archiveBucket.tenants().contains(entry.getKey()))) + ) + ); + + return true; + } +} 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 6b8f2c6b822..faa4813e6b0 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 @@ -6,7 +6,7 @@ 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.integration.archive.ArchiveService; +import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBucketDb; import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository; import com.yahoo.vespa.hosted.controller.application.SystemApplication; @@ -28,13 +28,13 @@ public class ArchiveUriUpdater extends ControllerMaintainer { private final ApplicationController applications; private final NodeRepository nodeRepository; - private final ArchiveService archiveService; + private final ArchiveBucketDb archiveBucketDb; public ArchiveUriUpdater(Controller controller, Duration duration) { super(controller, duration, ArchiveUriUpdater.class.getSimpleName(), SystemName.all()); this.applications = controller.applications(); this.nodeRepository = controller.serviceRegistry().configServer().nodeRepository(); - this.archiveService = controller.serviceRegistry().archiveService(); + this.archiveBucketDb = controller.archiveBucketDb(); } @Override @@ -53,13 +53,13 @@ public class ArchiveUriUpdater extends ControllerMaintainer { tenantsByZone.forEach((zone, tenants) -> { Map<TenantName, URI> zoneArchiveUris = nodeRepository.getArchiveUris(zone); for (TenantName tenant : tenants) { - archiveService.archiveUriFor(zone, tenant) + archiveBucketDb.archiveUriFor(zone, tenant) .filter(uri -> !uri.equals(zoneArchiveUris.get(tenant))) .ifPresent(uri -> nodeRepository.setArchiveUri(zone, tenant, uri)); } zoneArchiveUris.keySet().stream() - .filter(tenant -> ! tenants.contains(tenant)) + .filter(tenant -> !tenants.contains(tenant)) .forEach(tenant -> nodeRepository.removeArchiveUri(zone, tenant)); }); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java index 8433afaf006..ad41fc7c9e8 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java @@ -66,6 +66,7 @@ public class ControllerMaintenance extends AbstractComponent { maintainers.add(new EndpointCertificateMaintainer(controller, intervals.endpointCertificateMaintainer)); maintainers.add(new TrafficShareUpdater(controller, intervals.trafficFractionUpdater)); maintainers.add(new ArchiveUriUpdater(controller, intervals.archiveUriUpdater)); + maintainers.add(new ArchiveAccessMaintainer(controller, intervals.archiveAccessMaintainer)); maintainers.add(new TenantRoleMaintainer(controller, intervals.tenantRoleMaintainer)); maintainers.add(new ChangeRequestMaintainer(controller, intervals.changeRequestMaintainer)); } @@ -119,6 +120,7 @@ public class ControllerMaintenance extends AbstractComponent { private final Duration endpointCertificateMaintainer; private final Duration trafficFractionUpdater; private final Duration archiveUriUpdater; + private final Duration archiveAccessMaintainer; private final Duration tenantRoleMaintainer; private final Duration changeRequestMaintainer; @@ -149,6 +151,7 @@ public class ControllerMaintenance extends AbstractComponent { this.endpointCertificateMaintainer = duration(12, HOURS); this.trafficFractionUpdater = duration(5, MINUTES); this.archiveUriUpdater = duration(5, MINUTES); + this.archiveAccessMaintainer = duration(10, MINUTES); this.tenantRoleMaintainer = duration(5, MINUTES); this.changeRequestMaintainer = duration(12, HOURS); } 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 new file mode 100644 index 00000000000..3a625c5c42c --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializer.java @@ -0,0 +1,71 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.persistence; + +import com.yahoo.config.provision.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 java.util.Set; +import java.util.stream.Collectors; + +/** + * (de)serializes tenant/bucket mappings for a zone + * <p> + * + * @author andreer + */ +public class ArchiveBucketsSerializer { + + // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one + // (and rewrite all nodes on startup), changes to the serialized format must be made + // such that what is serialized on version N+1 can be read by version N: + // - ADDING FIELDS: Always ok + // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version. + // - CHANGING THE FORMAT OF A FIELD: Don't do it bro. + + private final static String bucketsFieldName = "buckets"; + private final static String bucketNameFieldName = "bucketName"; + private final static String keyArnFieldName = "keyArn"; + private final static String tenantsFieldName = "tenantIds"; + + public static Slime toSlime(Set<ArchiveBucket> 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())); + } + ); + + return slime; + } + + public static Set<ArchiveBucket> fromSlime(Inspector inspector) { + return SlimeUtils.entriesStream(inspector.field(bucketsFieldName)) + .map(ArchiveBucketsSerializer::fromInspector) + .collect(Collectors.toUnmodifiableSet()); + } + + private static ArchiveBucket fromInspector(Inspector inspector) { + Set<TenantName> tenants = SlimeUtils.entriesStream(inspector.field(tenantsFieldName)) + .map(i -> TenantName.from(i.asString())) + .collect(Collectors.toUnmodifiableSet()); + + return new ArchiveBucket( + inspector.field(bucketNameFieldName).asString(), + inspector.field(keyArnFieldName).asString()) + .withTenants(tenants); + } + + public static Set<ArchiveBucket> fromJsonString(String zkData) { + return fromSlime(SlimeUtils.jsonToSlime(zkData).get()); + } +} 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 010a2e3f8e4..34741bcaedf 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 @@ -15,6 +15,7 @@ import com.yahoo.slime.SlimeUtils; import com.yahoo.vespa.curator.Curator; import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.hosted.controller.Application; +import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBucket; 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; @@ -87,6 +88,7 @@ public class CuratorDb { private static final Path routingPoliciesRoot = root.append("routingPolicies"); private static final Path zoneRoutingPoliciesRoot = root.append("zoneRoutingPolicies"); private static final Path endpointCertificateRoot = root.append("applicationCertificates"); + private static final Path archiveBucketsRoot = root.append("archiveBuckets"); private final NodeVersionSerializer nodeVersionSerializer = new NodeVersionSerializer(); private final VersionStatusSerializer versionStatusSerializer = new VersionStatusSerializer(nodeVersionSerializer); @@ -198,6 +200,10 @@ public class CuratorDb { return tryLock(lockRoot.append("meteringRefreshTime")); } + public Lock lockArchiveBuckets(ZoneId zoneId) { + return curator.lock(lockRoot.append("archiveBuckets").append(zoneId.value()), defaultLockTimeout); + } + // -------------- Helpers ------------------------------------------ /** Try locking with a low timeout, meaning it is OK to fail lock acquisition. @@ -546,6 +552,17 @@ public class CuratorDb { .orElse(0L); } + // -------------- Archive buckets ----------------------------------------- + + public Set<ArchiveBucket> readArchiveBuckets(ZoneId zoneId) { + return curator.getData(archiveBucketsPath(zoneId)).map(String::new).map(ArchiveBucketsSerializer::fromJsonString) + .orElse(Set.of()); + } + + public void writeArchiveBuckets(ZoneId zoneid, Set<ArchiveBucket> archiveBuckets) { + curator.set(archiveBucketsPath(zoneid), asJson(ArchiveBucketsSerializer.toSlime(archiveBuckets))); + } + // -------------- Paths --------------------------------------------------- private Path lockPath(TenantName tenant) { @@ -667,4 +684,8 @@ public class CuratorDb { return root.append("meteringRefreshTime"); } + private static Path archiveBucketsPath(ZoneId zoneId) { + return archiveBucketsRoot.append(zoneId.value()); + } + } 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 new file mode 100644 index 00000000000..57fa7cc8e44 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDbTest.java @@ -0,0 +1,66 @@ +package com.yahoo.vespa.hosted.controller.archive; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.vespa.flags.Flags; +import com.yahoo.vespa.flags.InMemoryFlagSource; +import com.yahoo.vespa.hosted.controller.ControllerTester; +import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBucket; +import org.apache.curator.shaded.com.google.common.collect.Streams; +import org.junit.Test; + +import java.net.URI; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.junit.Assert.*; + +public class CuratorArchiveBucketDbTest { + + @Test + public void archiveUriFor() { + ControllerTester tester = new ControllerTester(); + InMemoryFlagSource flagSource = (InMemoryFlagSource) tester.controller().flagSource(); + CuratorArchiveBucketDb bucketDb = new CuratorArchiveBucketDb(tester.controller()); + + tester.curator().writeArchiveBuckets(ZoneId.defaultId(), + Set.of(new ArchiveBucket("existingBucket", "keyArn").withTenant(TenantName.defaultName()))); + + // Nothing when feature flag is not set. + assertEquals(Optional.empty(), bucketDb.archiveUriFor(ZoneId.defaultId(), TenantName.defaultName())); + + // Returns hardcoded name from feature flag + flagSource.withStringFlag(Flags.SYNC_HOST_LOGS_TO_S3_BUCKET.id(), "hardcoded"); + assertEquals(Optional.of(URI.create("s3://hardcoded/default/")), bucketDb.archiveUriFor(ZoneId.defaultId(), TenantName.defaultName())); + + // Finds existing bucket in db when set to "auto" + flagSource.withStringFlag(Flags.SYNC_HOST_LOGS_TO_S3_BUCKET.id(), "auto"); + assertEquals(Optional.of(URI.create("s3://existingBucket/default/")), bucketDb.archiveUriFor(ZoneId.defaultId(), TenantName.defaultName())); + + // Assigns to existing bucket while there is space + IntStream.range(0, 29).forEach(i -> + assertEquals( + Optional.of(URI.create("s3://existingBucket/tenant" + i + "/")), bucketDb + .archiveUriFor(ZoneId.defaultId(), TenantName.from("tenant" + i)))); + + // Creates new bucket when existing buckets are full + assertEquals(Optional.of(URI.create("s3://bucketName/lastDrop/")), bucketDb.archiveUriFor(ZoneId.defaultId(), TenantName.from("lastDrop"))); + + // 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"))); + + // Lists all buckets by zone + Set<TenantName> existingBucketTenants = Streams.concat(Stream.of(TenantName.defaultName()), IntStream.range(0, 29).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())); + assertEquals( + Set.of(new ArchiveBucket("bucketName", "keyArn").withTenant(TenantName.from("firstInZone"))), + bucketDb.buckets(ZoneId.from("prod.us-east-3"))); + } +}
\ No newline at end of file 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 new file mode 100644 index 00000000000..89072519c7d --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java @@ -0,0 +1,60 @@ +// Copyright 2020 Oath Inc. 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.TenantName; +import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.vespa.flags.Flags; +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.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.restapi.ContainerTester; +import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerCloudTest; +import com.yahoo.vespa.hosted.controller.tenant.Tenant; +import org.junit.Test; + +import java.time.Duration; +import java.util.Map; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author andreer + */ +public class ArchiveAccessMaintainerTest extends ControllerContainerCloudTest { + + @Test + public void grantsRoleAccess() { + var containerTester = new ContainerTester(container, ""); + ((InMemoryFlagSource) containerTester.controller().flagSource()) + .withBooleanFlag(PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id(), true) + .withStringFlag(Flags.SYNC_HOST_LOGS_TO_S3_BUCKET.id(), "auto"); + var tester = new ControllerTester(containerTester); + + String tenant1role = "arn:aws:iam::123456789012:role/my-role"; + String tenant2role = "arn:aws:iam::210987654321:role/my-role"; + var tenant1 = createTenantWithAccessRole(tester, "tenant1", tenant1role); + createTenantWithAccessRole(tester, "tenant2", tenant2role); + + tester.controller().archiveBucketDb().archiveUriFor(ZoneId.from("prod.us-east-3"), tenant1); + var testBucket = new ArchiveBucket("bucketName", "keyArn").withTenant(tenant1); + + MockArchiveService archiveService = (MockArchiveService) tester.controller().serviceRegistry().archiveService(); + assertNull(archiveService.authorizedIamRoles.get(testBucket)); + new ArchiveAccessMaintainer(containerTester.controller(), Duration.ofMinutes(10)).maintain(); + assertEquals(Map.of(tenant1, tenant1role), archiveService.authorizedIamRoles.get(testBucket)); + } + + private TenantName createTenantWithAccessRole(ControllerTester tester, String tenantName, String role) { + var tenant = tester.createTenant(tenantName, Tenant.Type.cloud); + tester.controller().tenants().lockOrThrow(tenant, LockedTenant.Cloud.class, lockedTenant -> { + lockedTenant = lockedTenant.withArchiveAccessRole(Optional.of(role)); + tester.controller().tenants().store(lockedTenant); + }); + return tenant; + } +} 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 379331265e5..505536558ab 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 @@ -5,7 +5,9 @@ import com.yahoo.component.Version; 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.api.integration.archive.MockArchiveService; +import com.yahoo.vespa.flags.Flags; +import com.yahoo.vespa.flags.InMemoryFlagSource; +import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBucket; 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.ApplicationPackage; @@ -16,7 +18,9 @@ import org.junit.Test; import java.net.URI; import java.time.Duration; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; @@ -32,6 +36,9 @@ public class ArchiveUriUpdaterTest { public void archive_uri_test() { var updater = new ArchiveUriUpdater(tester.controller(), Duration.ofDays(1)); + ((InMemoryFlagSource) tester.controller().flagSource()) + .withStringFlag(Flags.SYNC_HOST_LOGS_TO_S3_BUCKET.id(), "auto"); + var tenant1 = TenantName.from("tenant1"); var tenant2 = TenantName.from("tenant2"); var tenantInfra = SystemApplication.TENANT; @@ -43,19 +50,19 @@ public class ArchiveUriUpdaterTest { assertArchiveUris(Map.of(), zone); // Archive service now has URI for tenant1, but tenant1 is not deployed in zone - setArchiveUriInService(Map.of(tenant1, "uri-1"), zone); - setArchiveUriInService(Map.of(tenantInfra, "uri-3"), zone); + setBucketNameInService(Map.of(tenant1, "uri-1"), zone); + setBucketNameInService(Map.of(tenantInfra, "uri-3"), zone); updater.maintain(); assertArchiveUris(Map.of(), zone); deploy(application, zone); updater.maintain(); - assertArchiveUris(Map.of(tenant1, "uri-1", tenantInfra, "uri-3"), zone); + assertArchiveUris(Map.of(tenant1, "s3://uri-1/tenant1/", tenantInfra, "s3://uri-3/hosted-vespa/"), zone); // 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, "uri-1", tenantInfra, "uri-3"), zone); + assertArchiveUris(Map.of(tenant1, "s3://uri-1/tenant1/", tenantInfra, "s3://uri-3/hosted-vespa/"), zone); } private void assertArchiveUris(Map<TenantName, String> expectedUris, ZoneId zone) { @@ -65,9 +72,11 @@ public class ArchiveUriUpdaterTest { assertEquals(expectedUris, actualUris); } - private void setArchiveUriInService(Map<TenantName, String> archiveUris, ZoneId zone) { - MockArchiveService archiveService = (MockArchiveService) tester.controller().serviceRegistry().archiveService(); - archiveUris.forEach((tenant, uri) -> archiveService.setArchiveUri(zone, tenant, URI.create(uri))); + 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); } private void setArchiveUriInNodeRepo(Map<TenantName, String> 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 new file mode 100644 index 00000000000..17814b12a09 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializerTest.java @@ -0,0 +1,28 @@ +package com.yahoo.vespa.hosted.controller.persistence; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBucket; +import org.junit.Test; + +import java.util.LinkedHashSet; + +import static org.junit.Assert.assertEquals; + +public class ArchiveBucketsSerializerTest { + + @Test + public void serdes() { + var testTenants = new LinkedHashSet<TenantName>(); + testTenants.add(TenantName.from("tenant1")); + testTenants.add(TenantName.from("tenant2")); + + 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())); + } +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json index 08741e7f38a..17c93c070fb 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json @@ -7,6 +7,9 @@ "name": "ApplicationOwnershipConfirmer" }, { + "name": "ArchiveAccessMaintainer" + }, + { "name": "ArchiveUriUpdater" }, { |