diff options
author | Øyvind Grønnesby <oyving@yahooinc.com> | 2022-06-14 15:36:45 +0200 |
---|---|---|
committer | Øyvind Grønnesby <oyving@yahooinc.com> | 2022-06-14 15:36:45 +0200 |
commit | 82cc6b485145067f281a2cf68a0e823b8cde6e13 (patch) | |
tree | c37f10c8fade516a37dc721ea01b36bfcac9661d /controller-api | |
parent | e5486a7ad29d64215c593b0062d9f622be9ce394 (diff) | |
parent | aaafe503173878c081aadaa91665cef162726a24 (diff) |
Merge remote-tracking branch 'origin/master' into ogronnesby/persist-scaling-events
Conflicts:
controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java
Diffstat (limited to 'controller-api')
18 files changed, 230 insertions, 110 deletions
diff --git a/controller-api/pom.xml b/controller-api/pom.xml index 42e96e0331a..f7057c93561 100644 --- a/controller-api/pom.xml +++ b/controller-api/pom.xml @@ -8,12 +8,12 @@ <parent> <groupId>com.yahoo.vespa</groupId> <artifactId>parent</artifactId> - <version>7-SNAPSHOT</version> + <version>8-SNAPSHOT</version> <relativePath>../parent/pom.xml</relativePath> </parent> <artifactId>controller-api</artifactId> <packaging>container-plugin</packaging> - <version>7-SNAPSHOT</version> + <version>8-SNAPSHOT</version> <dependencies> @@ -67,6 +67,13 @@ <scope>provided</scope> </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>security-utils</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <!-- compile --> <dependency> diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java index 33a87e82ec9..bf16913d05a 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java @@ -7,7 +7,6 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.ControllerVersion; import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveService; import com.yahoo.vespa.hosted.controller.api.integration.artifact.ArtifactRegistry; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AccessControlService; -import com.yahoo.vespa.hosted.controller.api.integration.aws.CloudEventFetcher; import com.yahoo.vespa.hosted.controller.api.integration.aws.ResourceTagger; import com.yahoo.vespa.hosted.controller.api.integration.aws.RoleService; import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController; @@ -30,6 +29,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipI import com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor; import com.yahoo.vespa.hosted.controller.api.integration.resource.CostReportConsumer; import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClient; +import com.yahoo.vespa.hosted.controller.api.integration.secrets.GcpSecretStore; import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretService; import com.yahoo.vespa.hosted.controller.api.integration.user.RoleMaintainer; import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestClient; @@ -74,8 +74,6 @@ public interface ServiceRegistry { CostReportConsumer costReportConsumer(); - CloudEventFetcher eventFetcherService(); - ArtifactRepository artifactRepository(); TesterCloud testerCloud(); @@ -113,4 +111,6 @@ public interface ServiceRegistry { PlanRegistry planRegistry(); RoleMaintainer roleMaintainer(); + + GcpSecretStore gcpSecretStore(); } 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 69eda662692..46e7fb48553 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 @@ -3,7 +3,9 @@ package com.yahoo.vespa.hosted.controller.api.integration.archive; 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.Set; @@ -17,7 +19,9 @@ public interface ArchiveService { ArchiveBucket createArchiveBucketFor(ZoneId zoneId); - void updateBucketPolicy(ZoneId zoneId, ArchiveBucket bucket, Map<TenantName, String> authorizeIamRoleByTenantName); + void updatePolicies(ZoneId zoneId, Set<ArchiveBucket> buckets, Map<TenantName,ArchiveAccess> authorizeAccessByTenantName); - void updateKeyPolicy(ZoneId zoneId, String keyArn, Set<String> tenantAuthorizedIamRoles); + boolean canAddTenantToBucket(ZoneId zoneId, ArchiveBucket bucket); + + URI bucketURI(ZoneId zoneId, String bucketName, TenantName tenantName); } 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 ce7b56ad1f6..a2847439ce7 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 @@ -3,8 +3,11 @@ package com.yahoo.vespa.hosted.controller.api.integration.archive; 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.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.TreeMap; @@ -15,8 +18,10 @@ import java.util.TreeMap; */ public class MockArchiveService implements ArchiveService { - public Map<ArchiveBucket, Map<TenantName, String>> authorizedIamRolesForBucket = new HashMap<>(); - public Map<String, Set<String>> authorizedIamRolesForKey = new TreeMap<>(); + + public Set<ArchiveBucket> archiveBuckets = new HashSet<>(); + public Map<TenantName, ArchiveAccess> authorizeAccessByTenantName = new HashMap<>(); + @Override public ArchiveBucket createArchiveBucketFor(ZoneId zoneId) { @@ -24,12 +29,18 @@ public class MockArchiveService implements ArchiveService { } @Override - public void updateBucketPolicy(ZoneId zoneId, ArchiveBucket bucket, Map<TenantName, String> authorizeIamRoleByTenantName) { - authorizedIamRolesForBucket.put(bucket, authorizeIamRoleByTenantName); + public void updatePolicies(ZoneId zoneId, Set<ArchiveBucket> buckets, Map<TenantName, ArchiveAccess> authorizeAccessByTenantName) { + this.archiveBuckets = new HashSet<>(buckets); + this.authorizeAccessByTenantName = new HashMap<>(authorizeAccessByTenantName); + } + + @Override + public boolean canAddTenantToBucket(ZoneId zoneId, ArchiveBucket bucket) { + return bucket.tenants().size() < 5; } @Override - public void updateKeyPolicy(ZoneId zoneId, String keyArn, Set<String> tenantAuthorizedIamRoles) { - authorizedIamRolesForKey.put(keyArn, tenantAuthorizedIamRoles); + public URI bucketURI(ZoneId zoneId, String bucketName, TenantName tenantName) { + return URI.create(String.format("s3://%s/%s/", bucketName, tenantName.value())); } } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/CloudEvent.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/CloudEvent.java deleted file mode 100644 index b5445791bf0..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/CloudEvent.java +++ /dev/null @@ -1,36 +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.aws; - -import java.util.Date; -import java.util.Optional; -import java.util.Set; - -/** - * A maintenance event in a cloud service. - * - * @author freva - */ -public class CloudEvent { - - public final String instanceEventId; - public final String code; - public final String description; - public final Optional<Date> notBefore; - public final Optional<Date> notBeforeDeadline; - public final Optional<Date> notAfter; - public final String awsRegionName; - public final Set<String> affectedInstances; - - public CloudEvent(String instanceEventId, String code, String description, Date notAfter, Date notBefore, - Date notBeforeDeadline, String awsRegionName, Set<String> affectedInstances) { - this.instanceEventId = instanceEventId; - this.code = code; - this.description = description; - this.notBefore = Optional.ofNullable(notBefore); - this.notBeforeDeadline = Optional.ofNullable(notBeforeDeadline); - this.notAfter = Optional.ofNullable(notAfter); - this.awsRegionName = awsRegionName; - this.affectedInstances = Set.copyOf(affectedInstances); - } - -} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/CloudEventFetcher.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/CloudEventFetcher.java deleted file mode 100644 index 0d08a5a8cb6..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/CloudEventFetcher.java +++ /dev/null @@ -1,13 +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.aws; - -import java.util.List; - -/** - * @author freva - */ -public interface CloudEventFetcher { - - List<CloudEvent> getEvents(String regionName); - -} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockCloudEventFetcher.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockCloudEventFetcher.java deleted file mode 100644 index 3300d8879ce..00000000000 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockCloudEventFetcher.java +++ /dev/null @@ -1,25 +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.aws; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * @author freva - */ -public class MockCloudEventFetcher implements CloudEventFetcher { - - private final Map<String, List<CloudEvent>> mockedEvents = new HashMap<>(); - - @Override - public List<CloudEvent> getEvents(String regionName) { - return mockedEvents.getOrDefault(regionName, new ArrayList<>()); - } - - public void addEvent(String regionName, CloudEvent cloudEvent) { - mockedEvents.computeIfAbsent(regionName, i -> new ArrayList<>()).add(cloudEvent); - } - -} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java index 23fb1f20fbc..6b267e538e7 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java @@ -1,6 +1,7 @@ // 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.billing; +import java.util.List; import java.util.Optional; /** @@ -16,6 +17,9 @@ public interface PlanRegistry { /** Get a plan given a plan ID */ Optional<Plan> plan(PlanId planId); + /** Get a set of all plans */ + List<Plan> all(); + /** Get a plan give a plan ID */ default Optional<Plan> plan(String planId) { if (planId == null || planId.isBlank()) diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java index 4757fa76224..723ffa383eb 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java @@ -6,6 +6,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceUsage; import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.List; import java.util.Optional; import java.util.stream.Stream; @@ -27,6 +28,11 @@ public class PlanRegistryMock implements PlanRegistry { .findAny(); } + @Override + public List<Plan> all() { + return List.of(freeTrial, paidPlan, nonePlan); + } + private static class MockPlan implements Plan { private final PlanId planId; private final String description; diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateMetadata.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateMetadata.java index 1b36a573bf1..b423fcb83f8 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateMetadata.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateMetadata.java @@ -84,6 +84,34 @@ public class EndpointCertificateMetadata { return lastRefreshed; } + public EndpointCertificateMetadata withKeyName(String keyName) { + return new EndpointCertificateMetadata( + keyName, + this.certName, + this.version, + this.lastRequested, + this.rootRequestId, + this.leafRequestId, + this.requestedDnsSans, + this.issuer, + this.expiry, + this.lastRefreshed); + } + + public EndpointCertificateMetadata withCertName(String certName) { + return new EndpointCertificateMetadata( + this.keyName, + certName, + this.version, + this.lastRequested, + this.rootRequestId, + this.leafRequestId, + this.requestedDnsSans, + this.issuer, + this.expiry, + this.lastRefreshed); + } + public EndpointCertificateMetadata withVersion(int version) { return new EndpointCertificateMetadata( this.keyName, diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobType.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobType.java index 9e14c4ae8dd..77879699ab9 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobType.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobType.java @@ -134,7 +134,7 @@ public final class JobType implements Comparable<JobType> { } public static List<JobType> allIn(ZoneRegistry zones) { - return zones.zones().controllerUpgraded().zones().stream() + return zones.zones().reachable().zones().stream() .flatMap(zone -> zone.getEnvironment().isProduction() ? Stream.of(deploymentTo(zone.getId()), productionTestOf(zone.getId())) : Stream.of(deploymentTo(zone.getId()))) .sorted(naturalOrder()) diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/GcpSecretStore.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/GcpSecretStore.java new file mode 100644 index 00000000000..312bec1fd98 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/GcpSecretStore.java @@ -0,0 +1,8 @@ +package com.yahoo.vespa.hosted.controller.api.integration.secrets; + +public interface GcpSecretStore { + + void createSecret(String secretName, String secret); + + String getSecret(String secretName, int version); +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/NoopGcpSecretStore.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/NoopGcpSecretStore.java new file mode 100644 index 00000000000..9335a814f6c --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/NoopGcpSecretStore.java @@ -0,0 +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.secrets; + +/** + * @author olaa + */ +public class NoopGcpSecretStore implements GcpSecretStore { + + @Override + public void createSecret(String secretName, String secret) { + + } + + @Override + public String getSecret(String secretName, int version) { + return ""; + } +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java index b5f03756777..a3c1a0315ec 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java @@ -55,6 +55,9 @@ enum PathGroup { tenantInfo(Matcher.tenant, "/application/v4/tenant/{tenant}/application/", "/application/v4/tenant/{tenant}/info/", + "/application/v4/tenant/{tenant}/info/profile", + "/application/v4/tenant/{tenant}/info/billing", + "/application/v4/tenant/{tenant}/info/contacts", "/application/v4/tenant/{tenant}/notifications", "/routing/v1/status/tenant/{tenant}/{*}"), @@ -62,7 +65,10 @@ enum PathGroup { "/application/v4/tenant/{tenant}/key/"), tenantArchiveAccess(Matcher.tenant, - "/application/v4/tenant/{tenant}/archive-access"), + "/application/v4/tenant/{tenant}/archive-access", + "/application/v4/tenant/{tenant}/archive-access/aws", + "/application/v4/tenant/{tenant}/archive-access/gcp"), + billingToken(Matcher.tenant, "/billing/v1/tenant/{tenant}/token"), @@ -228,7 +234,8 @@ enum PathGroup { /** Paths used for invoice management */ hostedAccountant("/billing/v1/invoice/{*}", - "/billing/v1/billing"), + "/billing/v1/billing", + "/billing/v1/plans"), /** Path used for listing endpoint certificate request and re-requesting endpoint certificates */ endpointCertificates("/endpointcertificates/"), diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java index a739a8e2b01..60950341a42 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java @@ -177,7 +177,7 @@ public class SystemFlagsDataArchive { if (!filenamesForSystem.isEmpty() && !filenamesForSystem.contains(filename)) { if (systemDefinition != null && filename.startsWith(systemDefinition.system().value() + '.')) { throw new IllegalArgumentException(String.format( - "Environment or zone in filename '%s' is does not exist", filename)); + "Environment or zone in filename '%s' does not exist", filename)); } return; // Ignore files irrelevant for system } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccess.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccess.java new file mode 100644 index 00000000000..fba361f9223 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccess.java @@ -0,0 +1,105 @@ +package com.yahoo.vespa.hosted.controller.tenant; + +import com.yahoo.text.Text; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class ArchiveAccess { + + private static final Pattern VALID_AWS_ARCHIVE_ACCESS_ROLE_PATTERN = Pattern.compile("arn:aws:iam::\\d{12}:.+"); + private static final Pattern VALID_GCP_ARCHIVE_ACCESS_MEMBER_PATTERN = Pattern.compile("(?<prefix>[a-zA-Z]+):.+"); + + private static final Set<String> gcpMemberPrefixes = Set.of("user", "serviceAccount", "group", "domain"); + + // AWS IAM Role + private final Optional<String> awsRole; + // GCP Member + private final Optional<String> gcpMember; + + public ArchiveAccess() { + this(Optional.empty(), Optional.empty()); + } + + private ArchiveAccess(Optional<String> awsRole, Optional<String> gcpMember) { + this.awsRole = awsRole; + this.gcpMember = gcpMember; + + awsRole.ifPresent(role -> validateAWSIAMRole(role)); + gcpMember.ifPresent(member -> validateGCPMember(member)); + } + + public ArchiveAccess withAWSRole(String role) { + return new ArchiveAccess(Optional.of(role), gcpMember()); + } + + public ArchiveAccess withGCPMember(String member) { + return new ArchiveAccess(awsRole(), Optional.of(member)); + } + + public ArchiveAccess withAWSRole(Optional<String> role) { + return new ArchiveAccess(role, gcpMember()); + } + + public ArchiveAccess withGCPMember(Optional<String> member) { + return new ArchiveAccess(awsRole(), member); + } + + public ArchiveAccess removeAWSRole() { + return new ArchiveAccess(Optional.empty(), gcpMember()); + } + + public ArchiveAccess removeGCPMember() { + return new ArchiveAccess(awsRole(), Optional.empty()); + } + + private void validateAWSIAMRole(String role) { + if (!VALID_AWS_ARCHIVE_ACCESS_ROLE_PATTERN.matcher(role).matches()) { + throw new IllegalArgumentException(Text.format("Invalid archive access role '%s': Must match expected pattern: '%s'", + awsRole.get(), VALID_AWS_ARCHIVE_ACCESS_ROLE_PATTERN.pattern())); + } + if (role.length() > 100) { + throw new IllegalArgumentException("Invalid archive access role too long, must be 100 or less characters"); + } + } + + private void validateGCPMember(String member) { + var matcher = VALID_GCP_ARCHIVE_ACCESS_MEMBER_PATTERN.matcher(member); + if (!matcher.matches()) { + throw new IllegalArgumentException(Text.format("Invalid GCP archive access member '%s': Must match expected pattern: '%s'", + gcpMember.get(), VALID_GCP_ARCHIVE_ACCESS_MEMBER_PATTERN.pattern())); + } + var prefix = matcher.group("prefix"); + if (!gcpMemberPrefixes.contains(prefix)) { + throw new IllegalArgumentException(Text.format("Invalid GCP member prefix '%s', must be one of '%s'", + prefix, gcpMemberPrefixes.stream().collect(Collectors.joining(", ")))); + } + if (!"domain".equals(prefix) && !member.contains("@")) { + throw new IllegalArgumentException(Text.format("Invalid GCP member '%s', prefix '%s' must be followed by an email id", member, prefix)); + } + } + + public Optional<String> awsRole() { + return awsRole; + } + + public Optional<String> gcpMember() { + return gcpMember; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ArchiveAccess that = (ArchiveAccess) o; + return awsRole.equals(that.awsRole) && gcpMember.equals(that.gcpMember); + } + + @Override + public int hashCode() { + return Objects.hash(awsRole, gcpMember); + } +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java index 9a4e04ebb3a..54924b9c456 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java @@ -4,7 +4,6 @@ package com.yahoo.vespa.hosted.controller.tenant; import com.google.common.collect.BiMap; import com.google.common.collect.ImmutableBiMap; import com.yahoo.config.provision.TenantName; -import com.yahoo.text.Text; import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore; import java.security.Principal; @@ -13,7 +12,6 @@ import java.time.Instant; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.regex.Pattern; /** * A paying tenant in a Vespa cloud service. @@ -22,29 +20,22 @@ import java.util.regex.Pattern; */ public class CloudTenant extends Tenant { - private static final Pattern VALID_ARCHIVE_ACCESS_ROLE_PATTERN = Pattern.compile("arn:aws:iam::\\d{12}:.+"); - private final Optional<Principal> creator; private final BiMap<PublicKey, Principal> developerKeys; private final TenantInfo info; private final List<TenantSecretStore> tenantSecretStores; - private final Optional<String> archiveAccessRole; + private final ArchiveAccess archiveAccess; /** Public for the serialization layer — do not use! */ public CloudTenant(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional<Principal> creator, BiMap<PublicKey, Principal> developerKeys, TenantInfo info, - List<TenantSecretStore> tenantSecretStores, Optional<String> archiveAccessRole) { + List<TenantSecretStore> tenantSecretStores, ArchiveAccess archiveAccess) { super(name, createdAt, lastLoginInfo, Optional.empty()); this.creator = creator; this.developerKeys = developerKeys; this.info = Objects.requireNonNull(info); this.tenantSecretStores = tenantSecretStores; - this.archiveAccessRole = archiveAccessRole; - if (!archiveAccessRole.map(role -> VALID_ARCHIVE_ACCESS_ROLE_PATTERN.matcher(role).matches()).orElse(true)) - throw new IllegalArgumentException(Text.format("Invalid archive access role '%s': Must match expected pattern: '%s'", - archiveAccessRole.get(), VALID_ARCHIVE_ACCESS_ROLE_PATTERN.pattern())); - if (archiveAccessRole.map(role -> role.length() > 100).orElse(false)) - throw new IllegalArgumentException("Invalid archive access role too long, must be 100 or less characters"); + this.archiveAccess = Objects.requireNonNull(archiveAccess); } /** Creates a tenant with the given name, provided it passes validation. */ @@ -53,7 +44,7 @@ public class CloudTenant extends Tenant { createdAt, LastLoginInfo.EMPTY, Optional.ofNullable(creator), - ImmutableBiMap.of(), TenantInfo.empty(), List.of(), Optional.empty()); + ImmutableBiMap.of(), TenantInfo.empty(), List.of(), new ArchiveAccess()); } /** The user that created the tenant */ @@ -66,11 +57,6 @@ public class CloudTenant extends Tenant { return info; } - /** An iam role which is allowed to access the S3 (log, dump) archive) */ - public Optional<String> archiveAccessRole() { - return archiveAccessRole; - } - /** Returns the set of developer keys and their corresponding developers for this tenant. */ public BiMap<PublicKey, Principal> developerKeys() { return developerKeys; } @@ -79,6 +65,16 @@ public class CloudTenant extends Tenant { return tenantSecretStores; } + /** + * Role or member that is allowed to access archive bucket (log, dump) + * + * For AWS is this the IAM role + * For GCP it is a GCP member + */ + public ArchiveAccess archiveAccess() { + return archiveAccess; + } + @Override public Type type() { return Type.cloud; diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java index e4c46cfed1a..ce2746f92e0 100644 --- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java +++ b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java @@ -135,7 +135,7 @@ public class SystemFlagsDataArchiveTest { Path directory = Paths.get("src/test/resources/system-flags-with-unknown-file-name/"); expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage( - "Environment or zone in filename 'main.prod.unknown-region.json' is does not exist"); + "Environment or zone in filename 'main.prod.unknown-region.json' does not exist"); SystemFlagsDataArchive.fromDirectoryAndSystem(directory, createZoneRegistryMock()); } |