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-server | |
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-server')
91 files changed, 1180 insertions, 1098 deletions
diff --git a/controller-server/pom.xml b/controller-server/pom.xml index 5cf53929a98..773d63202b6 100644 --- a/controller-server/pom.xml +++ b/controller-server/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-server</artifactId> <packaging>container-plugin</packaging> - <version>7-SNAPSHOT</version> + <version>8-SNAPSHOT</version> <dependencies> @@ -102,12 +102,6 @@ </dependency> <dependency> - <groupId>javax.ws.rs</groupId> - <artifactId>javax.ws.rs-api</artifactId> - <scope>provided</scope> - </dependency> - - <dependency> <groupId>com.yahoo.vespa</groupId> <artifactId>flags</artifactId> <version>${project.version}</version> 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 1c4f0994f8c..af56666c6eb 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 @@ -251,6 +251,19 @@ public class Controller extends AbstractComponent { } } + /** Clear the target OS version for given cloud in this system */ + public void cancelOsUpgradeIn(CloudName cloudName) { + try (Mutex lock = curator.lockOsVersions()) { + Map<CloudName, OsVersionTarget> targets = curator.readOsVersionTargets().stream() + .collect(Collectors.toMap(t -> t.osVersion().cloud(), + Function.identity())); + if (targets.remove(cloudName) == null) { + throw new IllegalArgumentException("Cloud '" + cloudName.value() + " has no OS upgrade target"); + } + curator.writeOsVersionTargets(new TreeSet<>(targets.values())); + } + } + /** Returns the current OS version status */ public OsVersionStatus osVersionStatus() { return curator.readOsVersionStatus(); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java index 7a0e60aacb4..4f58e87035b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java @@ -8,11 +8,11 @@ import com.yahoo.config.provision.TenantName; import com.yahoo.security.KeyUtils; import com.yahoo.transaction.Mutex; import com.yahoo.vespa.athenz.api.AthenzDomain; -import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.hosted.controller.api.identifiers.Property; import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact; import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant; @@ -128,26 +128,26 @@ public abstract class LockedTenant { private final BiMap<PublicKey, Principal> developerKeys; private final TenantInfo info; private final List<TenantSecretStore> tenantSecretStores; - private final Optional<String> archiveAccessRole; + private final ArchiveAccess archiveAccess; private Cloud(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); this.developerKeys = ImmutableBiMap.copyOf(developerKeys); this.creator = creator; this.info = info; this.tenantSecretStores = tenantSecretStores; - this.archiveAccessRole = archiveAccessRole; + this.archiveAccess = archiveAccess; } private Cloud(CloudTenant tenant) { - this(tenant.name(), tenant.createdAt(), tenant.lastLoginInfo(), tenant.creator(), tenant.developerKeys(), tenant.info(), tenant.tenantSecretStores(), tenant.archiveAccessRole()); + this(tenant.name(), tenant.createdAt(), tenant.lastLoginInfo(), tenant.creator(), tenant.developerKeys(), tenant.info(), tenant.tenantSecretStores(), tenant.archiveAccess()); } @Override public CloudTenant get() { - return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccessRole); + return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess); } public Cloud withDeveloperKey(PublicKey key, Principal principal) { @@ -155,38 +155,38 @@ public abstract class LockedTenant { if (keys.containsKey(key)) throw new IllegalArgumentException("Key " + KeyUtils.toPem(key) + " is already owned by " + keys.get(key)); keys.put(key, principal); - return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info, tenantSecretStores, archiveAccessRole); + return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info, tenantSecretStores, archiveAccess); } public Cloud withoutDeveloperKey(PublicKey key) { BiMap<PublicKey, Principal> keys = HashBiMap.create(developerKeys); keys.remove(key); - return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info, tenantSecretStores, archiveAccessRole); + return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info, tenantSecretStores, archiveAccess); } public Cloud withInfo(TenantInfo newInfo) { - return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, newInfo, tenantSecretStores, archiveAccessRole); + return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, newInfo, tenantSecretStores, archiveAccess); } @Override public LockedTenant with(LastLoginInfo lastLoginInfo) { - return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccessRole); + return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess); } public Cloud withSecretStore(TenantSecretStore tenantSecretStore) { ArrayList<TenantSecretStore> secretStores = new ArrayList<>(tenantSecretStores); secretStores.add(tenantSecretStore); - return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, secretStores, archiveAccessRole); + return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, secretStores, archiveAccess); } public Cloud withoutSecretStore(TenantSecretStore tenantSecretStore) { ArrayList<TenantSecretStore> secretStores = new ArrayList<>(tenantSecretStores); secretStores.remove(tenantSecretStore); - return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, secretStores, archiveAccessRole); + return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, secretStores, archiveAccess); } - public Cloud withArchiveAccessRole(Optional<String> role) { - return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, role); + public Cloud withArchiveAccess(ArchiveAccess archiveAccess) { + return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess); } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java index c0829adc7ae..790f54b5e8c 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java @@ -213,7 +213,7 @@ public class RoutingController { .map(com.yahoo.config.application.api.Endpoint.Target::region) .distinct() .map(region -> new DeploymentId(deployment.applicationId(), ZoneId.from(Environment.prod, region))) - .collect(Collectors.toUnmodifiableList()); + .toList(); TenantAndApplicationId application = TenantAndApplicationId.from(deployment.applicationId()); for (var targetDeployment : deploymentTargets) { builders.add(Endpoint.of(application).targetApplication(EndpointId.defaultId(), targetDeployment)); @@ -413,19 +413,19 @@ public class RoutingController { /** Create a common name based on a hash of given application. This must be less than 64 characters long. */ private static String commonNameHashOf(ApplicationId application, SystemName system) { + @SuppressWarnings("deprecation") // for Hashing.sha1() HashCode sha1 = Hashing.sha1().hashString(application.serializedForm(), StandardCharsets.UTF_8); String base32 = BaseEncoding.base32().omitPadding().lowerCase().encode(sha1.asBytes()); return 'v' + base32 + Endpoint.internalDnsSuffix(system); } private static String asString(Endpoint.Scope scope) { - switch (scope) { - case application: return "application"; - case global: return "global"; - case weighted: return "weighted"; - case zone: return "zone"; - } - throw new IllegalArgumentException("Unknown scope " + scope); + return switch (scope) { + case application -> "application"; + case global -> "global"; + case weighted -> "weighted"; + case zone -> "zone"; + }; } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java index 8de72893a7c..88366466289 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java @@ -103,7 +103,7 @@ public class Endpoint { /** Returns the deployments(s) to which this routes traffic */ public List<DeploymentId> deployments() { - return targets.stream().map(Target::deployment).collect(Collectors.toUnmodifiableList()); + return targets.stream().map(Target::deployment).toList(); } /** Returns the scope of this */ @@ -202,20 +202,19 @@ public class Endpoint { private static String scopeSymbol(Scope scope, SystemName system) { if (system.isPublic()) { - switch (scope) { - case zone: return "z"; - case weighted: return "w"; - case global: return "g"; - case application: return "r"; - } - } - switch (scope) { - case zone: return ""; - case weighted: return "w"; - case global: return "global"; - case application: return "r"; + return switch (scope) { + case zone -> "z"; + case weighted -> "w"; + case global -> "g"; + case application -> "r"; + }; } - throw new IllegalArgumentException("No scope symbol defined for " + scope + " in " + system); + return switch (scope) { + case zone -> ""; + case weighted -> "w"; + case global -> "global"; + case application -> "r"; + }; } private static String instancePart(Optional<InstanceName> instance, String separator) { @@ -233,17 +232,19 @@ public class Endpoint { /** Returns the DNS suffix used for endpoints in given system */ private static String dnsSuffix(SystemName system, boolean legacy) { switch (system) { - case cd: - case main: + case cd, main -> { if (legacy) return YAHOO_DNS_SUFFIX; return OATH_DNS_SUFFIX; - case Public: + } + case Public -> { if (legacy) throw new IllegalArgumentException("No legacy DNS suffix declared for system " + system); return PUBLIC_DNS_SUFFIX; - case PublicCd: + } + case PublicCd -> { if (legacy) throw new IllegalArgumentException("No legacy DNS suffix declared for system " + system); return PUBLIC_CD_DNS_SUFFIX; - default: throw new IllegalArgumentException("No DNS suffix declared for system " + system); + } + default -> throw new IllegalArgumentException("No DNS suffix declared for system " + system); } } @@ -284,11 +285,12 @@ public class Endpoint { /** Returns the given region without availability zone */ private static RegionName effectiveRegion(RegionName region) { - if (region.value().isEmpty()) return region; + if (region.value().length() < 2) return region; String value = region.value(); char lastChar = value.charAt(value.length() - 1); if (lastChar >= 'a' && lastChar <= 'z') { // Remove availability zone - value = value.substring(0, value.length() - 1); + int skip = value.charAt(value.length() - 2) == '-' ? 2 : 1; + value = value.substring(0, value.length() - skip); } return RegionName.from(value); } @@ -422,20 +424,6 @@ public class Endpoint { return new EndpointBuilder(application, Optional.empty()); } - /** Create an endpoint for given system application */ - public static Endpoint of(SystemApplication systemApplication, ZoneId zone, URI url) { - if (!systemApplication.hasEndpoint()) throw new IllegalArgumentException(systemApplication + " has no endpoint"); - RoutingMethod routingMethod = RoutingMethod.exclusive; - Port port = url.getPort() == -1 ? Port.tls() : Port.tls(url.getPort()); // System application endpoints are always TLS - return new Endpoint(TenantAndApplicationId.from(systemApplication.id()), - Optional.of(systemApplication.id().instance()), - null, - ClusterSpec.Id.from("admin"), - url, - List.of(new Target(new DeploymentId(systemApplication.id(), zone))), - Scope.zone, port, false, routingMethod, false); - } - /** A target of an endpoint */ public static class Target { @@ -502,7 +490,7 @@ public class Endpoint { public EndpointBuilder target(EndpointId endpointId, ClusterSpec.Id cluster, List<DeploymentId> deployments) { this.endpointId = endpointId; this.cluster = cluster; - this.targets = deployments.stream().map(Target::new).collect(Collectors.toUnmodifiableList()); + this.targets = deployments.stream().map(Target::new).toList(); this.scope = requireUnset(Scope.global); return this; } @@ -538,7 +526,7 @@ public class Endpoint { this.cluster = cluster; this.targets = deployments.entrySet().stream() .map(kv -> new Target(kv.getKey(), kv.getValue())) - .collect(Collectors.toUnmodifiableList()); + .toList(); this.scope = Scope.application; return this; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java index 613422b2749..f4a1001d9ff 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java @@ -11,7 +11,6 @@ import com.yahoo.vespa.applicationmodel.InfrastructureApplication; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; import com.yahoo.vespa.hosted.controller.api.integration.configserver.ServiceConvergence; -import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry; import java.util.Arrays; import java.util.EnumSet; @@ -76,17 +75,6 @@ public enum SystemApplication { return nodeType().isHost(); } - /** Returns whether this has an endpoint */ - public boolean hasEndpoint() { - return this == configServer; - } - - /** Returns the endpoint of this, if any */ - public Optional<Endpoint> endpointIn(ZoneId zone, ZoneRegistry zoneRegistry) { - if (!hasEndpoint()) return Optional.empty(); - return Optional.of(Endpoint.of(this, zone, zoneRegistry.getConfigServerVipUri(zone))); - } - /** All system applications that are not the controller */ public static List<SystemApplication> notController() { return List.copyOf(EnumSet.complementOf(EnumSet.of(SystemApplication.controllerHost))); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java index 7b3b90d1458..4bc9aeb00e4 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java @@ -20,7 +20,6 @@ import com.yahoo.security.X509CertificateUtils; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; import com.yahoo.slime.SlimeUtils; -import com.yahoo.slime.Type; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.deployment.ZipBuilder; import com.yahoo.yolean.Exceptions; @@ -98,6 +97,7 @@ public class ApplicationPackage { * it must not be further changed by the caller. * If 'requireFiles' is true, files needed by deployment orchestration must be present. */ + @SuppressWarnings("deprecation") // for Hashing.sha1() public ApplicationPackage(byte[] zippedContent, boolean requireFiles) { this.zippedContent = Objects.requireNonNull(zippedContent, "The application package content cannot be null"); this.contentHash = Hashing.sha1().hashBytes(zippedContent).toString(); @@ -240,6 +240,7 @@ public class ApplicationPackage { } // Hashes all files and settings that require a deployment to be forwarded to configservers + @SuppressWarnings("deprecation") // for Hashing.sha1() private String calculateBundleHash(byte[] zippedContent) { Predicate<String> entryMatcher = name -> ! name.endsWith(deploymentFile) && ! name.endsWith(buildMetaFile); SortedMap<String, Long> crcByEntry = new TreeMap<>(); @@ -261,6 +262,7 @@ public class ApplicationPackage { .hash().toString(); } + @SuppressWarnings("deprecation") // for Hashing.sha1() public static String calculateHash(byte[] bytes) { return Hashing.sha1().newHasher() .putBytes(bytes) @@ -274,14 +276,6 @@ public class ApplicationPackage { /** Max size of each extracted file */ private static final int maxSize = 10 << 20; // 10 Mb - // TODO: Vespa 8: Remove application/ directory support - private static final String applicationDir = "application/"; - - private static String withoutLegacyDir(String name) { - if (name.startsWith(applicationDir)) return name.substring(applicationDir.length()); - return name; - } - private final byte[] zip; private final Map<Path, Optional<byte[]>> cache; @@ -307,11 +301,11 @@ public class ApplicationPackage { private Map<Path, Optional<byte[]>> read(Collection<String> names) { var entries = ZipEntries.from(zip, - name -> names.contains(withoutLegacyDir(name)), + name -> names.contains(name), maxSize, true) .asList().stream() - .collect(toMap(entry -> Paths.get(withoutLegacyDir(entry.name())).normalize(), + .collect(toMap(entry -> Paths.get(entry.name()).normalize(), ZipEntries.ZipEntryWithContent::content)); names.stream().map(Paths::get).forEach(path -> entries.putIfAbsent(path.normalize(), Optional.empty())); return entries; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java index ccad4fe92ad..8546eb5a971 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.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.application.pkg; +import com.yahoo.component.Version; import com.yahoo.config.application.api.DeploymentInstanceSpec; import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.Endpoint; @@ -20,6 +21,7 @@ import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.application.EndpointId; import com.yahoo.vespa.hosted.controller.deployment.DeploymentSteps; +import com.yahoo.vespa.hosted.controller.versions.VespaVersion; import java.time.Instant; import java.util.ArrayList; @@ -59,6 +61,21 @@ public class ApplicationPackageValidator { validateEndpointChange(application, applicationPackage, instant); validateCompactedEndpoint(applicationPackage); validateSecurityClientsPem(applicationPackage); + validateDeprecatedElements(applicationPackage); + } + + /** Verify that deployment spec does not use elements deprecated on a major version older than wanted major version */ + private void validateDeprecatedElements(ApplicationPackage applicationPackage) { + int wantedMajor = applicationPackage.compileVersion().map(Version::getMajor) + .or(() -> applicationPackage.deploymentSpec().majorVersion()) + .or(() -> controller.readVersionStatus().controllerVersion() + .map(VespaVersion::versionNumber) + .map(Version::getMajor)) + .orElseThrow(() -> new IllegalArgumentException("Could not determine wanted major version")); + for (var deprecatedElement : applicationPackage.deploymentSpec().deprecatedElements()) { + if (deprecatedElement.majorVersion() >= wantedMajor) continue; + throw new IllegalArgumentException(deprecatedElement.humanReadableString()); + } } /** Verify that we have the security/clients.pem file for public systems */ 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 21914b87818..0a0adcfc252 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 @@ -46,7 +46,7 @@ public class CuratorArchiveBucketDb { return getBucketNameFromCache(zoneId, tenant) .or(() -> findAndUpdateArchiveUriCache(zoneId, tenant, buckets(zoneId))) .or(() -> createIfMissing ? Optional.of(assignToBucket(zoneId, tenant)) : Optional.empty()) - .map(bucketName -> URI.create(Text.format("s3://%s/%s/", bucketName, tenant.value()))); + .map(bucketName -> archiveService.bucketURI(zoneId, bucketName, tenant)); } private String assignToBucket(ZoneId zoneId, TenantName tenant) { @@ -57,7 +57,7 @@ public class CuratorArchiveBucketDb { .orElseGet(() -> { // If not, find an existing bucket with space Optional<ArchiveBucket> unfilledBucket = zoneBuckets.stream() - .filter(bucket -> bucket.tenants().size() < tenantsPerBucket().orElse(Integer.MAX_VALUE)) + .filter(bucket -> archiveService.canAddTenantToBucket(zoneId, bucket)) .findAny(); // And place the tenant in that bucket. @@ -94,23 +94,6 @@ public class CuratorArchiveBucketDb { return bucketName; } - private OptionalInt tenantsPerBucket() { - if (system.isPublic()) { - /* - * Due to policy limits, we can't put data for more than this many tenants in a bucket. - * Policy size limit is 20kb, about 550 bytes for non-tenant related policies. Each tenant - * needs about 500 + len(role_arn) bytes, we limit role_arn to 100 characters, so we can - * fit about (20k - 550) / 600 ~ 32 tenants per bucket. - */ - return OptionalInt.of(30); - } else { - /* - * The S3 policies in main/cd have a fixed size. - */ - return OptionalInt.empty(); - } - } - private Optional<String> getBucketNameFromCache(ZoneId zoneId, TenantName tenantName) { return Optional.ofNullable(archiveUriCache.get(zoneId)).map(map -> map.get(tenantName)); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java index e4869cc9bf4..3bc2aec95ff 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java @@ -8,6 +8,7 @@ import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.restapi.RestApiException; import com.yahoo.text.Text; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.athenz.api.AthenzIdentity; @@ -33,7 +34,6 @@ import com.yahoo.vespa.hosted.controller.security.TenantSpec; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; import com.yahoo.vespa.hosted.controller.tenant.Tenant; -import javax.ws.rs.ForbiddenException; import java.time.Instant; import java.util.Arrays; import java.util.List; @@ -197,7 +197,7 @@ public class AthenzFacade implements AccessControl { } catch (ZmsClientException e) { if (e.getErrorCode() == com.yahoo.jdisc.Response.Status.FORBIDDEN) - throw new ForbiddenException("Not authorized to create application", e); + throw new RestApiException.Forbidden("Not authorized to create application", e); else throw e; } @@ -289,7 +289,7 @@ public class AthenzFacade implements AccessControl { private void verifyIsDomainAdmin(AthenzIdentity identity, AthenzDomain domain) { log("getMembership(domain=%s, role=%s, principal=%s)", domain, "admin", identity); if ( ! zmsClient.getMembership(new AthenzRole(domain, "admin"), identity)) - throw new ForbiddenException( + throw new RestApiException.Forbidden( Text.format("The user '%s' is not admin in Athenz domain '%s'", identity.getFullName(), domain.getName())); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java index 996b53cc6f5..33aeda5e011 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.certificate; import com.yahoo.config.application.api.DeploymentInstanceSpec; import com.yahoo.config.application.api.DeploymentSpec; +import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.text.Text; import com.yahoo.vespa.hosted.controller.Controller; @@ -11,6 +12,7 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata; import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProvider; import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidator; +import com.yahoo.vespa.hosted.controller.api.integration.secrets.GcpSecretStore; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import java.time.Clock; @@ -60,6 +62,22 @@ public class EndpointCertificates { Duration duration = Duration.between(start, clock.instant()); if (duration.toSeconds() > 30) log.log(Level.INFO, Text.format("Getting endpoint certificate metadata for %s took %d seconds!", instance.id().serializedForm(), duration.toSeconds())); + + if (controller.zoneRegistry().zones().ofCloud(CloudName.from("gcp")).ids().contains(zone)) { // Until CKMS is available from GCP + if(metadata.isPresent()) { + var m = metadata.get(); + GcpSecretStore gcpSecretStore = controller.serviceRegistry().gcpSecretStore(); + String mangledCertName = "endpointCert_" + m.certName().replace('.', '_') + "-v" + m.version(); // Google cloud does not accept dots in secrets, but they accept underscores + String mangledKeyName = "endpointCert_" + m.keyName().replace('.', '_') + "-v" + m.version(); // Google cloud does not accept dots in secrets, but they accept underscores + if (gcpSecretStore.getSecret(mangledCertName, m.version()) == null) + gcpSecretStore.createSecret(mangledCertName, controller.secretStore().getSecret(m.certName(), m.version())); + if (gcpSecretStore.getSecret(mangledKeyName, m.version()) == null) + gcpSecretStore.createSecret(mangledKeyName, controller.secretStore().getSecret(m.keyName(), m.version())); + + return Optional.of(m.withVersion(1).withKeyName(mangledKeyName).withCertName(mangledCertName)); + } + } + return metadata; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java index be07a2b0cb1..6e2ae0da46d 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java @@ -7,9 +7,7 @@ import com.yahoo.config.application.api.DeploymentInstanceSpec; import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.InstanceName; -import com.yahoo.text.Text; import com.yahoo.transaction.Mutex; -import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.ApplicationController; import com.yahoo.vespa.hosted.controller.Controller; @@ -148,9 +146,11 @@ public class DeploymentTrigger { return; } - applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> + applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> { + if (application.get().deploymentSpec().instance(id.instance()).isPresent()) applications().store(application.with(id.instance(), - instance -> withRemainingChange(instance, instance.change(), jobs.deploymentStatus(application.get()))))); + instance -> withRemainingChange(instance, instance.change(), jobs.deploymentStatus(application.get())))); + }); } /** @@ -248,7 +248,7 @@ public class DeploymentTrigger { .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); jobs.forEach((jobId, versionsList) -> { - trigger(deploymentJob(instance, versionsList.get(0).versions(), jobId.type(), status.jobs().get(jobId).get(), clock.instant()), reason); + trigger(deploymentJob(application.require(job.application().instance()), versionsList.get(0).versions(), jobId.type(), status.jobs().get(jobId).get(), clock.instant()), reason); }); return List.copyOf(jobs.keySet()); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java index fb7a3fc4ba5..813e3454e80 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java @@ -157,7 +157,7 @@ public class InternalStepRunner implements StepRunner { } private Optional<RunStatus> deployInitialReal(RunId id, DualLogger logger) { - Versions versions = controller.jobController().run(id).get().versions(); + Versions versions = controller.jobController().run(id).versions(); logger.log("Deploying platform version " + versions.sourcePlatform().orElse(versions.targetPlatform()) + " and application " + @@ -166,16 +166,16 @@ public class InternalStepRunner implements StepRunner { } private Optional<RunStatus> deployReal(RunId id, DualLogger logger) { - Versions versions = controller.jobController().run(id).get().versions(); + Versions versions = controller.jobController().run(id).versions(); logger.log("Deploying platform version " + versions.targetPlatform() + " and application " + versions.targetRevision() + " ..."); return deployReal(id, false, logger); } private Optional<RunStatus> deployReal(RunId id, boolean setTheStage, DualLogger logger) { - Optional<X509Certificate> testerCertificate = controller.jobController().run(id).get().testerCertificate(); + Optional<X509Certificate> testerCertificate = controller.jobController().run(id).testerCertificate(); return deploy(() -> controller.applications().deploy(id.job(), setTheStage), - controller.jobController().run(id).get() + controller.jobController().run(id) .stepInfo(setTheStage ? deployInitialReal : deployReal).get() .startTime().get(), logger) @@ -184,8 +184,8 @@ public class InternalStepRunner implements StepRunner { if ( ! useTesterCertificate(id) || result != running) return true; // If tester cert, ensure real is deployed with the tester cert whose key was successfully deployed. - return controller.jobController().run(id).get().stepStatus(deployTester).get() == succeeded - && testerCertificate.equals(controller.jobController().run(id).get().testerCertificate()); + return controller.jobController().run(id).stepStatus(deployTester).get() == succeeded + && testerCertificate.equals(controller.jobController().run(id).testerCertificate()); }); } @@ -196,7 +196,7 @@ public class InternalStepRunner implements StepRunner { testerPackage(id), id.type().zone(), platform), - controller.jobController().run(id).get() + controller.jobController().run(id) .stepInfo(deployTester).get() .startTime().get(), logger); @@ -287,10 +287,10 @@ public class InternalStepRunner implements StepRunner { return Optional.of(installationFailed); } - Versions versions = controller.jobController().run(id).get().versions(); + Versions versions = controller.jobController().run(id).versions(); Version platform = setTheStage ? versions.sourcePlatform().orElse(versions.targetPlatform()) : versions.targetPlatform(); - Run run = controller.jobController().run(id).get(); + Run run = controller.jobController().run(id); Optional<ServiceConvergence> services = controller.serviceRegistry().configServer().serviceConvergence(new DeploymentId(id.application(), id.type().zone()), Optional.of(platform)); if (services.isEmpty()) { @@ -409,13 +409,14 @@ public class InternalStepRunner implements StepRunner { } private Version testerPlatformVersion(RunId id) { - return application(id.application()).change().isPinned() - ? controller.jobController().run(id).get().versions().targetPlatform() - : controller.readSystemVersion(); + Version targetPlatform = controller.jobController().run(id).versions().targetPlatform(); + Version systemVersion = controller.readSystemVersion(); + boolean incompatible = controller.applications().versionCompatibility(id.application()).refuse(targetPlatform, systemVersion); + return incompatible || application(id.application()).change().isPinned() ? targetPlatform : systemVersion; } private Optional<RunStatus> installTester(RunId id, DualLogger logger) { - Run run = controller.jobController().run(id).get(); + Run run = controller.jobController().run(id); Version platform = testerPlatformVersion(id); ZoneId zone = id.type().zone(); ApplicationId testerId = id.tester().id(); @@ -607,6 +608,9 @@ public class InternalStepRunner implements StepRunner { byte[] config = testConfigSerializer.configJson(id.application(), id.type(), true, + deployment.get().version(), + deployment.get().revision(), + deployment.get().at(), endpoints, controller.applications().reachableContentClustersByZone(deployments)); controller.jobController().cloud().startTests(getTesterDeploymentId(id), suite, config); @@ -621,7 +625,7 @@ public class InternalStepRunner implements StepRunner { return Optional.of(error); } - Optional<X509Certificate> testerCertificate = controller.jobController().run(id).get().testerCertificate(); + Optional<X509Certificate> testerCertificate = controller.jobController().run(id).testerCertificate(); if (testerCertificate.isPresent()) { try { testerCertificate.get().checkValidity(Date.from(controller.clock().instant())); @@ -662,7 +666,10 @@ public class InternalStepRunner implements StepRunner { "or a Java test bundle under 'components/' with at least one test with the annotation " + "for this suite. See docs.vespa.ai/en/testing.html for details."); controller.jobController().updateTestReport(id); - return Optional.of(noTests); + + DeploymentSpec spec = controller.applications().requireApplication(TenantAndApplicationId.from(id.application())).deploymentSpec(); + boolean requireTests = spec.steps().stream().anyMatch(step -> step.concerns(id.type().environment())); + return Optional.of(requireTests ? testFailure : noTests); } case SUCCESS: logger.log("Tests completed successfully."); @@ -680,7 +687,7 @@ public class InternalStepRunner implements StepRunner { } // Hitting a config server which doesn't have this particular app loaded causes a 404. catch (ConfigServerException e) { - Instant doom = controller.jobController().run(id).get().stepInfo(copyVespaLogs).get().startTime().get() + Instant doom = controller.jobController().run(id).stepInfo(copyVespaLogs).get().startTime().get() .plus(Duration.ofMinutes(3)); if (e.code() == ConfigServerException.ErrorCode.NOT_FOUND && controller.clock().instant().isBefore(doom)) { logger.log(INFO, "Found no logs, but will retry"); @@ -706,7 +713,7 @@ public class InternalStepRunner implements StepRunner { } catch (RuntimeException e) { logger.log(WARNING, "Failed deleting application " + id.application(), e); - Instant startTime = controller.jobController().run(id).get().stepInfo(deactivateReal).get().startTime().get(); + Instant startTime = controller.jobController().run(id).stepInfo(deactivateReal).get().startTime().get(); return startTime.isBefore(controller.clock().instant().minus(Duration.ofHours(1))) ? Optional.of(error) : Optional.empty(); @@ -721,7 +728,7 @@ public class InternalStepRunner implements StepRunner { } catch (RuntimeException e) { logger.log(WARNING, "Failed deleting tester of " + id.application(), e); - Instant startTime = controller.jobController().run(id).get().stepInfo(deactivateTester).get().startTime().get(); + Instant startTime = controller.jobController().run(id).stepInfo(deactivateTester).get().startTime().get(); return startTime.isBefore(controller.clock().instant().minus(Duration.ofHours(1))) ? Optional.of(error) : Optional.empty(); @@ -745,7 +752,7 @@ public class InternalStepRunner implements StepRunner { return Optional.of(error); } catch (RuntimeException e) { - Instant start = controller.jobController().run(id).get().stepInfo(report).get().startTime().get(); + Instant start = controller.jobController().run(id).stepInfo(report).get().startTime().get(); return (controller.clock().instant().isAfter(start.plusSeconds(180))) ? Optional.empty() : Optional.of(error); @@ -867,7 +874,7 @@ public class InternalStepRunner implements StepRunner { private boolean timedOut(RunId id, Deployment deployment, Duration defaultTimeout) { // TODO jonmv: This is a workaround for new deployment writes not yet being visible in spite of Curator locking. // TODO Investigate what's going on here, and remove this workaround. - Run run = controller.jobController().run(id).get(); + Run run = controller.jobController().run(id); if ( ! controller.system().isCd() && run.start().isAfter(deployment.at())) return false; @@ -883,7 +890,7 @@ public class InternalStepRunner implements StepRunner { /** Returns the application package for the tester application, assembled from a generated config, fat-jar and services.xml. */ private ApplicationPackage testerPackage(RunId id) { - RevisionId revision = controller.jobController().run(id).get().versions().targetRevision(); + RevisionId revision = controller.jobController().run(id).versions().targetRevision(); DeploymentSpec spec = controller.applications().requireApplication(TenantAndApplicationId.from(id.application())).deploymentSpec(); byte[] testZip = controller.applications().applicationStore().getTester(id.application().tenant(), id.application().application(), revision); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java index 30f16acf77d..3d4a2f40303 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java @@ -47,6 +47,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.NavigableMap; +import java.util.NoSuchElementException; import java.util.Optional; import java.util.OptionalLong; import java.util.Set; @@ -69,8 +70,12 @@ import static com.yahoo.vespa.hosted.controller.deployment.Step.copyVespaLogs; import static com.yahoo.vespa.hosted.controller.deployment.Step.deactivateTester; import static com.yahoo.vespa.hosted.controller.deployment.Step.endStagingSetup; import static com.yahoo.vespa.hosted.controller.deployment.Step.endTests; +import static com.yahoo.vespa.hosted.controller.deployment.Step.installInitialReal; +import static com.yahoo.vespa.hosted.controller.deployment.Step.installReal; +import static com.yahoo.vespa.hosted.controller.deployment.Step.installTester; import static com.yahoo.vespa.hosted.controller.deployment.Step.report; import static java.time.temporal.ChronoUnit.SECONDS; +import static java.util.Comparator.comparing; import static java.util.Comparator.naturalOrder; import static java.util.function.Predicate.not; import static java.util.logging.Level.INFO; @@ -179,11 +184,27 @@ public class JobController { if (deployment.isEmpty() || deployment.get().at().isBefore(run.start())) return run; - Instant from = run.lastVespaLogTimestamp().isAfter(deployment.get().at()) ? run.lastVespaLogTimestamp() : deployment.get().at(); + Instant deployedAt = run.stepInfo(installInitialReal).or(() -> run.stepInfo(installReal)).flatMap(StepInfo::startTime).orElseThrow(); + Instant from = run.lastVespaLogTimestamp().isAfter(run.start()) ? run.lastVespaLogTimestamp() : deployedAt.minusSeconds(10); List<LogEntry> log = LogEntry.parseVespaLog(controller.serviceRegistry().configServer() .getLogs(new DeploymentId(id.application(), zone), Map.of("from", Long.toString(from.toEpochMilli()))), from); + + if (run.hasStep(installTester) && run.versions().targetPlatform().isAfter(new Version("7.590"))) { // todo jonmv: remove + deployedAt = run.stepInfo(installTester).flatMap(StepInfo::startTime).orElseThrow(); + from = run.lastVespaLogTimestamp().isAfter(run.start()) ? run.lastVespaLogTimestamp() : deployedAt.minusSeconds(10); + List<LogEntry> testerLog = LogEntry.parseVespaLog(controller.serviceRegistry().configServer() + .getLogs(new DeploymentId(id.tester().id(), zone), + Map.of("from", Long.toString(from.toEpochMilli()))), + from); + + Instant justNow = controller.clock().instant().minusSeconds(2); + log = Stream.concat(log.stream(), testerLog.stream()) + .filter(entry -> entry.at().isBefore(justNow)) + .sorted(comparing(LogEntry::at)) + .collect(toUnmodifiableList()); + } if (log.isEmpty()) return run; @@ -278,11 +299,12 @@ public class JobController { return runs.build(); } - /** Returns the run with the given id, if it exists. */ - public Optional<Run> run(RunId id) { + /** Returns the run with the given id, or throws if no such run exists. */ + public Run run(RunId id) { return runs(id.application(), id.type()).values().stream() .filter(run -> run.id().equals(id)) - .findAny(); + .findAny() + .orElseThrow(() -> new NoSuchElementException("no run with id '" + id + "' exists")); } /** Returns the last run of the given type, for the given application, if one has been run. */ @@ -392,7 +414,7 @@ public class JobController { Deque<Mutex> locks = new ArrayDeque<>(); try { // Ensure no step is still running before we finish the run — report depends transitively on all the other steps. - Run unlockedRun = run(id).get(); + Run unlockedRun = run(id); locks.push(curator.lock(id.application(), id.type(), report)); for (Step step : report.allPrerequisites(unlockedRun.steps().keySet())) locks.push(curator.lock(id.application(), id.type(), step)); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java index 03cc6c6ba8d..7ffaaabb1a7 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java @@ -45,8 +45,9 @@ public class Run { // For deserialisation only -- do not use! public Run(RunId id, Map<Step, StepInfo> steps, Versions versions, boolean isRedeployment, Instant start, Optional<Instant> end, - Optional<Instant> sleepUntil, RunStatus status, long lastTestRecord, Instant lastVespaLogTimestamp, Optional<Instant> noNodesDownSince, - Optional<ConvergenceSummary> convergenceSummary, Optional<X509Certificate> testerCertificate, boolean dryRun, Optional<String> reason) { + Optional<Instant> sleepUntil, RunStatus status, long lastTestRecord, Instant lastVespaLogTimestamp, + Optional<Instant> noNodesDownSince, Optional<ConvergenceSummary> convergenceSummary, + Optional<X509Certificate> testerCertificate, boolean dryRun, Optional<String> reason) { this.id = id; this.steps = Collections.unmodifiableMap(new EnumMap<>(steps)); this.versions = versions; @@ -67,8 +68,9 @@ public class Run { public static Run initial(RunId id, Versions versions, boolean isRedeployment, Instant now, JobProfile profile, Optional<String> triggeredBy) { EnumMap<Step, StepInfo> steps = new EnumMap<>(Step.class); profile.steps().forEach(step -> steps.put(step, StepInfo.initial(step))); - return new Run(id, steps, requireNonNull(versions), isRedeployment, requireNonNull(now), Optional.empty(), Optional.empty(), running, - -1, Instant.EPOCH, Optional.empty(), Optional.empty(), Optional.empty(), profile == JobProfile.developmentDryRun, triggeredBy); + return new Run(id, steps, requireNonNull(versions), isRedeployment, requireNonNull(now), Optional.empty(), + Optional.empty(), running, -1, Instant.EPOCH, Optional.empty(), Optional.empty(), + Optional.empty(), profile == JobProfile.developmentDryRun, triggeredBy); } /** Returns a new Run with the status of the given completed step set accordingly. */ diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Step.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Step.java index 379cc9c4f0a..ec4d138c44f 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Step.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Step.java @@ -65,7 +65,7 @@ public enum Step { deactivateReal(true, deployInitialReal, deployReal, endTests, copyVespaLogs), /** Deactivate the tester. */ - deactivateTester(true, deployTester, endTests), + deactivateTester(true, deployTester, endTests, copyVespaLogs), /** Report completion to the deployment orchestration machinery. */ report(true, installReal, deactivateReal, deactivateTester); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializer.java index 1680e064234..2394f293170 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializer.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.deployment; +import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.zone.ZoneId; @@ -8,10 +9,12 @@ import com.yahoo.slime.Cursor; import com.yahoo.slime.Slime; import com.yahoo.slime.SlimeUtils; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId; import com.yahoo.vespa.hosted.controller.application.Endpoint; import java.io.IOException; import java.io.UncheckedIOException; +import java.time.Instant; import java.util.List; import java.util.Map; @@ -31,6 +34,9 @@ public class TestConfigSerializer { public Slime configSlime(ApplicationId id, JobType type, boolean isCI, + Version platform, + RevisionId revision, + Instant deployedAt, Map<ZoneId, List<Endpoint>> deployments, Map<ZoneId, List<String>> clusters) { Slime slime = new Slime(); @@ -40,6 +46,9 @@ public class TestConfigSerializer { root.setString("zone", type.zone().value()); root.setString("system", system.value()); root.setBool("isCI", isCI); + root.setString("platform", platform.toFullString()); + root.setLong("revision", revision.number()); + root.setLong("deployedAt", deployedAt.toEpochMilli()); // TODO jvenstad: remove when clients can be updated Cursor endpointsObject = root.setObject("endpoints"); @@ -72,10 +81,13 @@ public class TestConfigSerializer { public byte[] configJson(ApplicationId id, JobType type, boolean isCI, + Version platform, + RevisionId revision, + Instant deployedAt, Map<ZoneId, List<Endpoint>> deployments, Map<ZoneId, List<String>> clusters) { try { - return SlimeUtils.toJsonBytes(configSlime(id, type, isCI, deployments, clusters)); + return SlimeUtils.toJsonBytes(configSlime(id, type, isCI, platform, revision, deployedAt, deployments, clusters)); } catch (IOException e) { throw new UncheckedIOException(e); 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 bd69ea41b05..788360996ff 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 @@ -10,6 +10,7 @@ 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; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.Tenant; @@ -45,47 +46,34 @@ public class ArchiveAccessMaintainer extends ControllerMaintainer { @Override protected double maintain() { - // Count buckets - so we can alert if we get close to the account limit of 1000 - zoneRegistry.zonesIncludingSystem().all().ids().forEach(zoneId -> - metric.set(bucketCountMetricName, archiveBucketDb.buckets(zoneId).size(), - metric.createContext(Map.of("zone", zoneId.value())))); + // 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.createContext(Map.of( + "zone", z.getVirtualId().value(), + "cloud", z.getCloudName().value())))); zoneRegistry.zonesIncludingSystem().controllerUpgraded().zones().forEach(z -> { - ZoneId zoneId = z.getVirtualId(); - try { - var tenantArchiveAccessRoles = cloudTenantArchiveExternalAccessRoles(); - archiveBucketDb.buckets(zoneId).forEach(archiveBucket -> - archiveService.updateBucketPolicy(zoneId, archiveBucket, - Maps.filterEntries(tenantArchiveAccessRoles, - entry -> archiveBucket.tenants().contains(entry.getKey()))) - ); - Map<String, List<ArchiveBucket>> bucketsPerKey = archiveBucketDb.buckets(zoneId).stream() - .collect(groupingBy(ArchiveBucket::keyArn)); - bucketsPerKey.forEach((keyArn, buckets) -> { - Set<String> authorizedIamRolesForKey = buckets.stream() - .flatMap(b -> b.tenants().stream()) - .filter(tenantArchiveAccessRoles::containsKey) - .map(tenantArchiveAccessRoles::get) - .collect(Collectors.toSet()); - archiveService.updateKeyPolicy(zoneId, keyArn, authorizedIamRolesForKey); - }); - } catch (Exception e) { - throw new RuntimeException("Failed to maintain archive access in " + zoneId.value(), e); - } - } - ); + ZoneId zoneId = z.getVirtualId(); + try { + var tenantArchiveAccessRoles = cloudTenantArchiveExternalAccessRoles(); + var buckets = archiveBucketDb.buckets(zoneId); + archiveService.updatePolicies(zoneId, buckets, tenantArchiveAccessRoles); + } catch (Exception e) { + throw new RuntimeException("Failed to maintain archive access in " + zoneId.value(), e); + } + }); return 1.0; } - private Map<TenantName, String> cloudTenantArchiveExternalAccessRoles() { + private Map<TenantName, ArchiveAccess> cloudTenantArchiveExternalAccessRoles() { List<Tenant> tenants = controller().tenants().asList(); return tenants.stream() .filter(t -> t instanceof CloudTenant) .map(t -> (CloudTenant) t) - .filter(t -> t.archiveAccessRole().isPresent()) .collect(Collectors.toUnmodifiableMap( - Tenant::name, cloudTenant -> cloudTenant.archiveAccessRole().orElseThrow())); + Tenant::name, cloudTenant -> cloudTenant.archiveAccess())); } } 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 36ab2e6f384..42821ea8fe2 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 @@ -40,15 +40,13 @@ public class ArchiveUriUpdater extends ControllerMaintainer { protected double maintain() { Map<ZoneId, Set<TenantName>> tenantsByZone = new HashMap<>(); - tenantsByZone.put(controller().zoneRegistry().systemZone().getVirtualId(), - new HashSet<>(INFRASTRUCTURE_TENANTS)); + controller().zoneRegistry().zonesIncludingSystem().reachable().zones().forEach( + z -> tenantsByZone.put(z.getVirtualId(), new HashSet<>(INFRASTRUCTURE_TENANTS))); for (var application : applications.asList()) { for (var instance : application.instances().values()) { for (var deployment : instance.deployments().values()) { - tenantsByZone - .computeIfAbsent(deployment.zone(), zone -> new HashSet<>(INFRASTRUCTURE_TENANTS)) - .add(instance.id().tenant()); + tenantsByZone.get(deployment.zone()).add(instance.id().tenant()); } } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudEventTracker.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudEventTracker.java deleted file mode 100644 index 021c02fb6a0..00000000000 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudEventTracker.java +++ /dev/null @@ -1,77 +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.maintenance; - -import com.yahoo.config.provision.CloudName; -import com.yahoo.config.provision.zone.ZoneApi; -import com.yahoo.vespa.hosted.controller.Controller; -import com.yahoo.vespa.hosted.controller.api.integration.aws.CloudEvent; -import com.yahoo.vespa.hosted.controller.api.integration.aws.CloudEventFetcher; -import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node; -import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter; -import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository; - -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -/** - * This tracks maintenance events from cloud providers and deprovisions any affected hosts. - * - * @author mgimle - */ -public class CloudEventTracker extends ControllerMaintainer { - - private static final Logger log = Logger.getLogger(CloudEventTracker.class.getName()); - - private final CloudEventFetcher eventFetcher; - private final Map<String, List<ZoneApi>> zonesByCloudNativeRegion; - private final NodeRepository nodeRepository; - - CloudEventTracker(Controller controller, Duration interval) { - super(controller, interval); - this.eventFetcher = controller.serviceRegistry().eventFetcherService(); - this.nodeRepository = controller.serviceRegistry().configServer().nodeRepository(); - this.zonesByCloudNativeRegion = supportedZonesByRegion(); - } - - @Override - protected double maintain() { - for (var region : zonesByCloudNativeRegion.keySet()) { - for (var event : eventFetcher.getEvents(region)) { - deprovisionAffectedHosts(region, event); - } - } - return 1.0; - } - - /** Deprovision any host affected by given event */ - private void deprovisionAffectedHosts(String region, CloudEvent event) { - for (var zone : zonesByCloudNativeRegion.get(region)) { - for (var node : nodeRepository.list(zone.getId(), NodeFilter.all())) { - if (!deprovision(node, event)) continue; - log.info("Retiring and deprovisioning " + node.hostname().value() + " in " + zone.getId() + - ": Affected by maintenance event " + event.instanceEventId); - nodeRepository.retire(zone.getId(), node.hostname().value(), true, true); - } - } - } - - private static boolean deprovision(Node node, CloudEvent event) { - if (!node.type().isHost()) return false; // Non-hosts are never affected - if (node.wantToRetire() && node.wantToDeprovision()) return false; // Already deprovisioning - return event.affectedInstances.stream() - .anyMatch(instance -> node.hostname().value().contains(instance)); - } - - /** Returns zones supported by this, grouped by their native region name */ - private Map<String, List<ZoneApi>> supportedZonesByRegion() { - return controller().zoneRegistry().zones() - .ofCloud(CloudName.from("aws")) - .reachable() - .zones().stream() - .collect(Collectors.groupingBy(ZoneApi::getCloudNativeRegionName)); - } - -} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java index d482dedfdaa..b1b7e80e9a0 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java @@ -28,7 +28,8 @@ import java.util.stream.Collectors; public class CloudTrialExpirer extends ControllerMaintainer { private static final Logger log = Logger.getLogger(CloudTrialExpirer.class.getName()); - private static final Duration loginExpiry = Duration.ofDays(14); + private static final Duration nonePlanAfter = Duration.ofDays(14); + private static final Duration tombstoneAfter = Duration.ofDays(365); private final ListFlag<String> extendedTrialTenants; public CloudTrialExpirer(Controller controller, Duration interval) { @@ -38,24 +39,20 @@ public class CloudTrialExpirer extends ControllerMaintainer { @Override protected double maintain() { - if (controller().system().equals(SystemName.PublicCd)) { - tombstoneNonePlanTenants(); - } + tombstoneNonePlanTenants(); moveInactiveTenantsToNonePlan(); return 1.0; } private void moveInactiveTenantsToNonePlan() { - var predicate = tenantReadersNotLoggedIn(loginExpiry) + var predicate = tenantReadersNotLoggedIn(nonePlanAfter) .and(this::tenantHasTrialPlan); forTenant("'none' plan", predicate, this::setPlanNone); } private void tombstoneNonePlanTenants() { - // tombstone tenants that are inactive 14 days after being set as 'none' - var expiry = loginExpiry.plus(loginExpiry); - var predicate = tenantReadersNotLoggedIn(expiry).and(this::tenantHasNonePlan); + var predicate = tenantReadersNotLoggedIn(tombstoneAfter).and(this::tenantHasNonePlan); forTenant("tombstoned", predicate, this::tombstoneTenants); } 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 3beb4149938..8c52ae0560a 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 @@ -1,8 +1,8 @@ // 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.component.annotation.Inject; import com.yahoo.component.AbstractComponent; +import com.yahoo.component.annotation.Inject; import com.yahoo.concurrent.maintenance.Maintainer; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.zone.ZoneApi; @@ -59,9 +59,7 @@ public class ControllerMaintenance extends AbstractComponent { maintainers.add(new NameServiceDispatcher(controller, intervals.nameServiceDispatcher)); maintainers.add(new CostReportMaintainer(controller, intervals.costReportMaintainer, controller.serviceRegistry().costReportConsumer())); maintainers.add(new ResourceMeterMaintainer(controller, intervals.resourceMeterMaintainer, metric, controller.serviceRegistry().resourceDatabase())); - maintainers.add(new CloudEventTracker(controller, intervals.cloudEventReporter)); maintainers.add(new ResourceTagMaintainer(controller, intervals.resourceTagMaintainer, controller.serviceRegistry().resourceTagger())); - maintainers.add(new SystemRoutingPolicyMaintainer(controller, intervals.systemRoutingPolicyMaintainer)); maintainers.add(new ApplicationMetaDataGarbageCollector(controller, intervals.applicationMetaDataGarbageCollector)); maintainers.add(new ArtifactExpirer(controller, intervals.containerImageExpirer)); maintainers.add(new HostInfoUpdater(controller, intervals.hostInfoUpdater)); @@ -118,9 +116,7 @@ public class ControllerMaintenance extends AbstractComponent { private final Duration nameServiceDispatcher; private final Duration costReportMaintainer; private final Duration resourceMeterMaintainer; - private final Duration cloudEventReporter; private final Duration resourceTagMaintainer; - private final Duration systemRoutingPolicyMaintainer; private final Duration applicationMetaDataGarbageCollector; private final Duration containerImageExpirer; private final Duration hostInfoUpdater; @@ -153,9 +149,7 @@ public class ControllerMaintenance extends AbstractComponent { this.nameServiceDispatcher = duration(10, SECONDS); this.costReportMaintainer = duration(2, HOURS); this.resourceMeterMaintainer = duration(3, MINUTES); - this.cloudEventReporter = duration(30, MINUTES); this.resourceTagMaintainer = duration(30, MINUTES); - this.systemRoutingPolicyMaintainer = duration(15, MINUTES); this.applicationMetaDataGarbageCollector = duration(12, HOURS); this.containerImageExpirer = duration(12, HOURS); this.hostInfoUpdater = duration(12, HOURS); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunner.java index 369699eb3a3..94ec4129744 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunner.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunner.java @@ -81,9 +81,8 @@ public class JobRunner extends ControllerMaintainer { && controller().clock().instant().isAfter(run.sleepUntil().orElse(run.start()).plus(jobTimeout))) executors.execute(() -> { jobs.abort(run.id(), "job timeout of " + jobTimeout + " reached"); - advance(jobs.run(run.id()).get()); + advance(jobs.run(run.id())); }); - else if (run.readySteps().isEmpty()) executors.execute(() -> finish(run.id())); else if (run.hasFailed() || run.sleepUntil().map(sleepUntil -> ! sleepUntil.isAfter(controller().clock().instant())).orElse(true)) @@ -93,7 +92,8 @@ public class JobRunner extends ControllerMaintainer { private void finish(RunId id) { try { jobs.finish(id); - controller().applications().deploymentTrigger().notifyOfCompletion(id.application()); + if ( ! id.type().environment().isManuallyDeployed()) + controller().applications().deploymentTrigger().notifyOfCompletion(id.application()); } catch (TimeoutException e) { // One of the steps are still being run — that's ok, we'll try to finish the run again later. diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/SystemRoutingPolicyMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/SystemRoutingPolicyMaintainer.java deleted file mode 100644 index 5acb21917eb..00000000000 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/SystemRoutingPolicyMaintainer.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.maintenance; - -import com.yahoo.config.application.api.DeploymentSpec; -import com.yahoo.vespa.hosted.controller.Controller; -import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; -import com.yahoo.vespa.hosted.controller.application.SystemApplication; -import com.yahoo.vespa.hosted.controller.routing.RoutingPolicy; - -import java.time.Duration; - -/** - * This maintains {@link RoutingPolicy}'s for {@link SystemApplication}s. In contrast to regular applications, this - * refreshes policies at an interval, not on deployment. - * - * @author mpolden - */ -public class SystemRoutingPolicyMaintainer extends ControllerMaintainer { - - public SystemRoutingPolicyMaintainer(Controller controller, Duration interval) { - super(controller, interval); - } - - @Override - protected double maintain() { - for (var zone : controller().zoneRegistry().zones().reachable().ids()) { - for (var application : SystemApplication.values()) { - if (!application.hasEndpoint()) continue; - DeploymentId deployment = new DeploymentId(application.id(), zone); - controller().routing().of(deployment).configure(DeploymentSpec.empty); - } - } - return 1.0; - } - -} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java index 36a254f5c4b..49c819548fe 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.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.notification; +import com.google.common.annotations.VisibleForTesting; import com.yahoo.config.provision.Environment; import com.yahoo.text.Text; import com.yahoo.vespa.flags.FetchVector; @@ -19,6 +20,7 @@ import java.util.List; import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; import java.util.stream.Collectors; /** @@ -34,6 +36,9 @@ public class Notifier { private static final Logger log = Logger.getLogger(Notifier.class.getName()); + // Minimal url pattern matcher to detect hardcoded URLs in Notification messages + private static final Pattern urlPattern = Pattern.compile("https://[\\w\\d./]+"); + public Notifier(CuratorDb curatorDb, ZoneRegistry zoneRegistry, Mailer mailer, FlagSource flagSource) { this.curatorDb = Objects.requireNonNull(curatorDb); this.mailer = Objects.requireNonNull(mailer); @@ -103,16 +108,31 @@ public class Notifier { } } - private Mail mailOf(FormattedNotification content, Collection<String> recipients) { + public Mail mailOf(FormattedNotification content, Collection<String> recipients) { var notification = content.notification(); var subject = Text.format("[%s] %s Vespa Notification for %s", notification.level().toString().toUpperCase(), content.prettyType(), applicationIdSource(notification.source())); var body = new StringBuilder(); - body.append(content.messagePrefix()).append("\n\n") + body.append(content.messagePrefix()).append("\n") .append(notification.messages().stream().map(m -> " * " + m).collect(Collectors.joining("\n"))).append("\n") .append("\n") .append("Vespa Console link:\n") .append(content.uri().toString()); - return new Mail(recipients, subject, body.toString()); + var html = new StringBuilder(); + html.append(content.messagePrefix()).append("<br>\n") + .append("<ul>\n") + .append(notification.messages().stream() + .map(Notifier::linkify) + .map(m -> "<li>" + m + "</li>") + .collect(Collectors.joining("<br>\n"))) + .append("</ul>\n") + .append("<br>\n") + .append("<a href=\"" + content.uri() + "\">Vespa Console</a>"); + return new Mail(recipients, subject, body.toString(), html.toString()); + } + + @VisibleForTesting + static String linkify(String text) { + return urlPattern.matcher(text).replaceAll((res) -> String.format("<a href=\"%s\">%s</a>", res.group(), res.group())); } private String applicationIdSource(NotificationSource source) { 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 269864ad641..a4c7c50085c 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 @@ -13,7 +13,6 @@ import java.util.stream.Collectors; /** * (de)serializes tenant/bucket mappings for a zone - * <p> * * @author andreer */ diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java index dd28978d948..fcc6d99aec2 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java @@ -3,7 +3,6 @@ package com.yahoo.vespa.hosted.controller.persistence; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.SystemName; import com.yahoo.security.X509CertificateUtils; import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Cursor; @@ -214,7 +213,7 @@ class RunSerializer { run.sleepUntil().ifPresent(end -> runObject.setLong(sleepingUntilField, end.toEpochMilli())); runObject.setString(statusField, valueOf(run.status())); runObject.setLong(lastTestRecordField, run.lastTestLogEntry()); - runObject.setLong(lastVespaLogTimestampField, Instant.EPOCH.until(run.lastVespaLogTimestamp(), ChronoUnit.MICROS)); + if (run.lastVespaLogTimestamp().isAfter(Instant.EPOCH)) runObject.setLong(lastVespaLogTimestampField, Instant.EPOCH.until(run.lastVespaLogTimestamp(), ChronoUnit.MICROS)); run.noNodesDownSince().ifPresent(noNodesDownSince -> runObject.setLong(noNodesDownSinceField, noNodesDownSince.toEpochMilli())); run.convergenceSummary().ifPresent(convergenceSummary -> toSlime(convergenceSummary, runObject.setArray(convergenceSummaryField))); run.testerCertificate().ifPresent(certificate -> runObject.setString(testerCertificateField, X509CertificateUtils.toPem(certificate))); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java index 00e38abcba7..e7cf0c34511 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java @@ -17,6 +17,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInf import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact; import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore; import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant; @@ -77,6 +78,10 @@ public class TenantSerializer { private static final String lastLoginInfoField = "lastLoginInfo"; private static final String secretStoresField = "secretStores"; private static final String archiveAccessRoleField = "archiveAccessRole"; + private static final String archiveAccessField = "archiveAccess"; + private static final String awsArchiveAccessRoleField = "awsArchiveAccessRole"; + private static final String gcpArchiveAccessMemberField = "gcpArchiveAccessMember"; + private static final String awsIdField = "awsId"; private static final String roleField = "role"; @@ -117,7 +122,13 @@ public class TenantSerializer { toSlime(legacyBillingInfo, root.setObject(billingInfoField)); toSlime(tenant.info(), root); toSlime(tenant.tenantSecretStores(), root); - tenant.archiveAccessRole().ifPresent(role -> root.setString(archiveAccessRoleField, role)); + toSlime(tenant.archiveAccess(), root); + } + + private void toSlime(ArchiveAccess archiveAccess, Cursor root) { + Cursor object = root.setObject(archiveAccessField); + archiveAccess.awsRole().ifPresent(role -> object.setString(awsArchiveAccessRoleField, role)); + archiveAccess.gcpMember().ifPresent(member -> object.setString(gcpArchiveAccessMemberField, member)); } private void toSlime(DeletedTenant tenant, Cursor root) { @@ -175,8 +186,8 @@ public class TenantSerializer { BiMap<PublicKey, Principal> developerKeys = developerKeysFromSlime(tenantObject.field(pemDeveloperKeysField)); TenantInfo info = tenantInfoFromSlime(tenantObject.field(tenantInfoField)); List<TenantSecretStore> tenantSecretStores = secretStoresFromSlime(tenantObject.field(secretStoresField)); - Optional<String> archiveAccessRole = SlimeUtils.optionalString(tenantObject.field(archiveAccessRoleField)); - return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccessRole); + ArchiveAccess archiveAccess = archiveAccessFromSlime(tenantObject); + return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess); } private DeletedTenant deletedTenantFrom(Inspector tenantObject) { @@ -195,6 +206,22 @@ public class TenantSerializer { return keys.build(); } + ArchiveAccess archiveAccessFromSlime(Inspector tenantObject) { + // TODO(enygaard, 2022-05-24): Remove when all tenants have been rewritten to use ArchiveAccess object + Optional<String> archiveAccessRole = SlimeUtils.optionalString(tenantObject.field(archiveAccessRoleField)); + if (archiveAccessRole.isPresent()) { + return new ArchiveAccess().withAWSRole(archiveAccessRole.get()); + } + Inspector object = tenantObject.field(archiveAccessField); + if (!object.valid()) { + return new ArchiveAccess(); + } + Optional<String> awsArchiveAccessRole = SlimeUtils.optionalString(object.field(awsArchiveAccessRoleField)); + Optional<String> gcpArchiveAccessMember = SlimeUtils.optionalString(object.field(gcpArchiveAccessMemberField)); + return new ArchiveAccess() + .withAWSRole(awsArchiveAccessRole) + .withGCPMember(gcpArchiveAccessMember); + } TenantInfo tenantInfoFromSlime(Inspector infoObject) { if (!infoObject.valid()) return TenantInfo.empty(); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponse.java index 886dc27b404..c57133d8efd 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponse.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponse.java @@ -29,7 +29,9 @@ public class ProxyResponse extends HttpResponse { super(statusResponse); this.contentType = contentType; - String configServerPrefix = HttpURL.from(configServer).withPath(Path.empty()).asURI().toString(); + // Configserver always serves from 4443, therefore all responses will have port 4443 in them, + // but the request URI (loadbalancer/VIP) is not always 4443 + String configServerPrefix = HttpURL.from(configServer).withPort(4443).withPath(Path.empty()).asURI().toString(); String controllerRequestPrefix = controllerRequest.getControllerPrefixUri().toString(); bodyResponseRewritten = bodyResponse.replace(configServerPrefix, controllerRequestPrefix); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index 91b76ac8d05..63f33540721 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java @@ -35,6 +35,7 @@ import com.yahoo.restapi.ErrorResponse; import com.yahoo.restapi.MessageResponse; import com.yahoo.restapi.Path; import com.yahoo.restapi.ResourceResponse; +import com.yahoo.restapi.RestApiException; import com.yahoo.restapi.SlimeJsonResponse; import com.yahoo.security.KeyUtils; import com.yahoo.slime.Cursor; @@ -108,6 +109,7 @@ import com.yahoo.vespa.hosted.controller.routing.rotation.RotationStatus; import com.yahoo.vespa.hosted.controller.security.AccessControlRequests; import com.yahoo.vespa.hosted.controller.security.Credentials; import com.yahoo.vespa.hosted.controller.support.access.SupportAccess; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant; @@ -122,9 +124,6 @@ import com.yahoo.vespa.hosted.controller.versions.VersionStatus; import com.yahoo.vespa.hosted.controller.versions.VespaVersion; import com.yahoo.yolean.Exceptions; -import javax.ws.rs.ForbiddenException; -import javax.ws.rs.InternalServerErrorException; -import javax.ws.rs.NotAuthorizedException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -150,6 +149,7 @@ import java.util.Optional; import java.util.OptionalInt; import java.util.Scanner; import java.util.StringJoiner; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.logging.Level; import java.util.stream.Collectors; @@ -208,10 +208,10 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { default: return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported"); } } - catch (ForbiddenException e) { + catch (RestApiException.Forbidden e) { return ErrorResponse.forbidden(Exceptions.toMessageString(e)); } - catch (NotAuthorizedException e) { + catch (RestApiException.Unauthorized e) { return ErrorResponse.unauthorized(Exceptions.toMessageString(e)); } catch (NotExistsException e) { @@ -245,6 +245,9 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}")) return tenant(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/access/request/operator")) return accessRequests(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/info")) return tenantInfo(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/info/profile")) return withCloudTenant(path.get("tenant"), this::tenantInfoProfile); + if (path.matches("/application/v4/tenant/{tenant}/info/billing")) return withCloudTenant(path.get("tenant"), this::tenantInfoBilling); + if (path.matches("/application/v4/tenant/{tenant}/info/contacts")) return withCloudTenant(path.get("tenant"), this::tenantInfoContacts); if (path.matches("/application/v4/tenant/{tenant}/notifications")) return notifications(request, Optional.of(path.get("tenant")), false); if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}/validate")) return validateSecretStore(path.get("tenant"), path.get("name"), request); if (path.matches("/application/v4/tenant/{tenant}/application")) return applications(path.get("tenant"), Optional.empty(), request); @@ -298,7 +301,12 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}/access/approve/operator")) return approveAccessRequest(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/access/managed/operator")) return addManagedAccess(path.get("tenant")); if (path.matches("/application/v4/tenant/{tenant}/info")) return updateTenantInfo(path.get("tenant"), request); - if (path.matches("/application/v4/tenant/{tenant}/archive-access")) return allowArchiveAccess(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/info/profile")) return withCloudTenant(path.get("tenant"), request, this::putTenantInfoProfile); + if (path.matches("/application/v4/tenant/{tenant}/info/billing")) return withCloudTenant(path.get("tenant"), request, this::putTenantInfoBilling); + if (path.matches("/application/v4/tenant/{tenant}/info/contacts")) return withCloudTenant(path.get("tenant"), request, this::putTenantInfoContacts); + if (path.matches("/application/v4/tenant/{tenant}/archive-access")) return allowAwsArchiveAccess(path.get("tenant"), request); // TODO(enygaard, 2022-05-25) Remove when no longer used by console + if (path.matches("/application/v4/tenant/{tenant}/archive-access/aws")) return allowAwsArchiveAccess(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/archive-access/gcp")) return allowGcpArchiveAccess(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}")) return addSecretStore(path.get("tenant"), path.get("name"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/global-rotation/override")) return setGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), false, request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/global-rotation/override")) return setGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), false, request); @@ -346,7 +354,9 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}")) return deleteTenant(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/access/managed/operator")) return removeManagedAccess(path.get("tenant")); if (path.matches("/application/v4/tenant/{tenant}/key")) return removeDeveloperKey(path.get("tenant"), request); - if (path.matches("/application/v4/tenant/{tenant}/archive-access")) return removeArchiveAccess(path.get("tenant")); + if (path.matches("/application/v4/tenant/{tenant}/archive-access")) return removeAwsArchiveAccess(path.get("tenant")); // TODO(enygaard, 2022-05-25) Remove when no longer used by console + if (path.matches("/application/v4/tenant/{tenant}/archive-access/aws")) return removeAwsArchiveAccess(path.get("tenant")); + if (path.matches("/application/v4/tenant/{tenant}/archive-access/gcp")) return removeGcpArchiveAccess(path.get("tenant")); if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}")) return deleteSecretStore(path.get("tenant"), path.get("name"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return deleteApplication(path.get("tenant"), path.get("application"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deployment")) return removeAllProdDeployments(path.get("tenant"), path.get("application")); @@ -500,6 +510,13 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist or does not support this")); } + private HttpResponse withCloudTenant(String tenantName, Function<CloudTenant, SlimeJsonResponse> handler) { + return controller.tenants().get(TenantName.from(tenantName)) + .filter(tenant -> tenant.type() == Tenant.Type.cloud) + .map(tenant -> handler.apply((CloudTenant) tenant)) + .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist or does not support this")); + } + private SlimeJsonResponse tenantInfo(TenantInfo info, HttpRequest request) { Slime slime = new Slime(); Cursor infoCursor = slime.setObject(); @@ -517,6 +534,141 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { return new SlimeJsonResponse(slime); } + private SlimeJsonResponse tenantInfoProfile(CloudTenant cloudTenant) { + var slime = new Slime(); + var root = slime.setObject(); + var info = cloudTenant.info(); + + if (!info.isEmpty()) { + var contact = root.setObject("contact"); + contact.setString("name", info.contact().name()); + contact.setString("email", info.contact().email()); + + var tenant = root.setObject("tenant"); + tenant.setString("company", info.name()); + tenant.setString("website", info.website()); + + toSlime(info.address(), root); // will create "address" on the parent + } + + return new SlimeJsonResponse(slime); + } + + private SlimeJsonResponse withCloudTenant(String tenantName, HttpRequest request, BiFunction<CloudTenant, Inspector, SlimeJsonResponse> handler) { + return controller.tenants().get(tenantName) + .map(tenant -> handler.apply((CloudTenant) tenant, toSlime(request.getData()).get())) + .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist or does not support this")); + } + + private SlimeJsonResponse putTenantInfoProfile(CloudTenant cloudTenant, Inspector inspector) { + var info = cloudTenant.info(); + + var mergedContact = TenantContact.empty() + .withName(getString(inspector.field("contact").field("name"), info.contact().name())) + .withEmail(getString(inspector.field("contact").field("email"), info.contact().email())); + + var mergedAddress = updateTenantInfoAddress(inspector.field("address"), info.address()); + + var mergedInfo = info + .withName(getString(inspector.field("tenant").field("name"), info.name())) + .withWebsite(getString(inspector.field("tenant").field("website"), info.website())) + .withContact(mergedContact) + .withAddress(mergedAddress); + + validateMergedTenantInfo(mergedInfo); + + controller.tenants().lockOrThrow(cloudTenant.name(), LockedTenant.Cloud.class, lockedTenant -> { + lockedTenant = lockedTenant.withInfo(mergedInfo); + controller.tenants().store(lockedTenant); + }); + + return new MessageResponse("Tenant info updated"); + } + + private SlimeJsonResponse tenantInfoBilling(CloudTenant cloudTenant) { + var slime = new Slime(); + var root = slime.setObject(); + var info = cloudTenant.info(); + + if (!info.isEmpty()) { + var billingContact = info.billingContact(); + + var contact = root.setObject("contact"); + contact.setString("name", billingContact.contact().name()); + contact.setString("email", billingContact.contact().email()); + contact.setString("phone", billingContact.contact().phone()); + + toSlime(billingContact.address(), root); // will create "address" on the parent + } + + return new SlimeJsonResponse(slime); + } + + private SlimeJsonResponse putTenantInfoBilling(CloudTenant cloudTenant, Inspector inspector) { + var info = cloudTenant.info(); + var contact = info.billingContact().contact(); + var address = info.billingContact().address(); + + var mergedContact = updateTenantInfoContact(inspector.field("contact"), contact); + var mergedAddress = updateTenantInfoAddress(inspector.field("address"), info.billingContact().address()); + + var mergedBilling = info.billingContact() + .withContact(mergedContact) + .withAddress(mergedAddress); + + var mergedInfo = info.withBilling(mergedBilling); + + // Store changes + controller.tenants().lockOrThrow(cloudTenant.name(), LockedTenant.Cloud.class, lockedTenant -> { + lockedTenant = lockedTenant.withInfo(mergedInfo); + controller.tenants().store(lockedTenant); + }); + + return new MessageResponse("Tenant info updated"); + } + + private SlimeJsonResponse tenantInfoContacts(CloudTenant cloudTenant) { + var slime = new Slime(); + var root = slime.setObject(); + toSlime(cloudTenant.info().contacts(), root); + return new SlimeJsonResponse(slime); + } + + private SlimeJsonResponse putTenantInfoContacts(CloudTenant cloudTenant, Inspector inspector) { + var mergedInfo = cloudTenant.info() + .withContacts(updateTenantInfoContacts(inspector.field("contacts"), cloudTenant.info().contacts())); + + // Store changes + controller.tenants().lockOrThrow(cloudTenant.name(), LockedTenant.Cloud.class, lockedTenant -> { + lockedTenant = lockedTenant.withInfo(mergedInfo); + controller.tenants().store(lockedTenant); + }); + + return new MessageResponse("Tenant info updated"); + } + + private void validateMergedTenantInfo(TenantInfo mergedInfo) { + // Assert that we have a valid tenant info + if (mergedInfo.contact().name().isBlank()) { + throw new IllegalArgumentException("'contactName' cannot be empty"); + } + if (mergedInfo.contact().email().isBlank()) { + throw new IllegalArgumentException("'contactEmail' cannot be empty"); + } + if (! mergedInfo.contact().email().contains("@")) { + // email address validation is notoriously hard - we should probably just try to send a + // verification email to this address. checking for @ is a simple best-effort. + throw new IllegalArgumentException("'contactEmail' needs to be an email address"); + } + if (! mergedInfo.website().isBlank()) { + try { + new URL(mergedInfo.website()); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("'website' needs to be a valid address"); + } + } + } + private void toSlime(TenantAddress address, Cursor parentCursor) { if (address.isEmpty()) return; @@ -602,25 +754,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { .withBilling(updateTenantInfoBillingContact(insp.field("billingContact"), oldInfo.billingContact())) .withContacts(updateTenantInfoContacts(insp.field("contacts"), oldInfo.contacts())); - // Assert that we have a valid tenant info - if (mergedInfo.contact().name().isBlank()) { - throw new IllegalArgumentException("'contactName' cannot be empty"); - } - if (mergedInfo.contact().email().isBlank()) { - throw new IllegalArgumentException("'contactEmail' cannot be empty"); - } - if (! mergedInfo.contact().email().contains("@")) { - // email address validation is notoriously hard - we should probably just try to send a - // verification email to this address. checking for @ is a simple best-effort. - throw new IllegalArgumentException("'contactEmail' needs to be an email address"); - } - if (! mergedInfo.website().isBlank()) { - try { - new URL(mergedInfo.website()); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("'website' needs to be a valid address"); - } - } + validateMergedTenantInfo(mergedInfo); // Store changes controller.tenants().lockOrThrow(tenant.name(), LockedTenant.Cloud.class, lockedTenant -> { @@ -1029,7 +1163,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { return new SlimeJsonResponse(slime); } - private HttpResponse allowArchiveAccess(String tenantName, HttpRequest request) { + private HttpResponse allowAwsArchiveAccess(String tenantName, HttpRequest request) { if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud) throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant"); @@ -1037,27 +1171,62 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { var role = mandatory("role", data).asString(); if (role.isBlank()) { - return ErrorResponse.badRequest("Archive access role can't be whitespace only"); + return ErrorResponse.badRequest("AWS archive access role can't be whitespace only"); } controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, lockedTenant -> { - lockedTenant = lockedTenant.withArchiveAccessRole(Optional.of(role)); + var access = lockedTenant.get().archiveAccess(); + lockedTenant = lockedTenant.withArchiveAccess(access.withAWSRole(role)); controller.tenants().store(lockedTenant); }); - return new MessageResponse("Archive access role set to '" + role + "' for tenant " + tenantName + "."); + return new MessageResponse("AWS archive access role set to '" + role + "' for tenant " + tenantName + "."); } - private HttpResponse removeArchiveAccess(String tenantName) { + private HttpResponse removeAwsArchiveAccess(String tenantName) { if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud) throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant"); controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, lockedTenant -> { - lockedTenant = lockedTenant.withArchiveAccessRole(Optional.empty()); + var access = lockedTenant.get().archiveAccess(); + lockedTenant = lockedTenant.withArchiveAccess(access.removeAWSRole()); controller.tenants().store(lockedTenant); }); - return new MessageResponse("Archive access role removed for tenant " + tenantName + "."); + return new MessageResponse("AWS archive access role removed for tenant " + tenantName + "."); + } + + private HttpResponse allowGcpArchiveAccess(String tenantName, HttpRequest request) { + if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud) + throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant"); + + var data = toSlime(request.getData()).get(); + var member = mandatory("member", data).asString(); + + if (member.isBlank()) { + return ErrorResponse.badRequest("GCP archive access role can't be whitespace only"); + } + + controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, lockedTenant -> { + var access = lockedTenant.get().archiveAccess(); + lockedTenant = lockedTenant.withArchiveAccess(access.withGCPMember(member)); + controller.tenants().store(lockedTenant); + }); + + return new MessageResponse("GCP archive access member set to '" + member + "' for tenant " + tenantName + "."); + } + + private HttpResponse removeGcpArchiveAccess(String tenantName) { + if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud) + throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant"); + + controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, lockedTenant -> { + var access = lockedTenant.get().archiveAccess(); + lockedTenant = lockedTenant.withArchiveAccess(access.removeGCPMember()); + controller.tenants().store(lockedTenant); + }); + + return new MessageResponse("GCP archive access member removed for tenant " + tenantName + "."); } private HttpResponse patchApplication(String tenantName, String applicationName, HttpRequest request) { @@ -2176,16 +2345,24 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { .flatMap(instance -> instance.productionDeployments().keySet().stream()) .map(zone -> new DeploymentId(prodInstanceId, zone)) .collect(Collectors.toCollection(HashSet::new)); - ZoneId testedZone = type.zone(); + // If a production job is specified, the production deployment of the orchestrated instance is the relevant one, // as user instances should not exist in prod. + ApplicationId toTest = type.isProduction() ? prodInstanceId : id; if ( ! type.isProduction()) - deployments.add(new DeploymentId(id, testedZone)); + deployments.add(new DeploymentId(toTest, type.zone())); + + Deployment deployment = application.require(toTest.instance()).deployments().get(type.zone()); + if (deployment == null) + throw new NotExistsException(toTest + " is not deployed in " + type.zone()); return new SlimeJsonResponse(testConfigSerializer.configSlime(id, type, false, + deployment.version(), + deployment.revision(), + deployment.at(), controller.routing().readTestRunnerEndpointsOf(deployments), controller.applications().reachableContentClustersByZone(deployments))); } @@ -2355,7 +2532,9 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { log.warning(String.format("Failed to get quota for tenant %s: %s", tenant.name(), Exceptions.toMessageString(e))); } - cloudTenant.archiveAccessRole().ifPresent(role -> object.setString("archiveAccessRole", role)); + // TODO(enygaard, 2022-05-25) Remove when console is using new archive access structure + cloudTenant.archiveAccess().awsRole().ifPresent(role -> object.setString("archiveAccessRole", role)); + toSlime(cloudTenant.archiveAccess(), object.setObject("archiveAccess")); break; } @@ -2386,6 +2565,11 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { tenantMetaDataToSlime(tenant, applications, object.setObject("metaData")); } + private void toSlime(ArchiveAccess archiveAccess, Cursor object) { + archiveAccess.awsRole().ifPresent(role -> object.setString("awsRole", role)); + archiveAccess.gcpMember().ifPresent(member -> object.setString("gcpMember", member)); + } + private void toSlime(Quota quota, QuotaUsage usage, Cursor object) { quota.budget().ifPresentOrElse( budget -> object.setDouble("budget", budget.doubleValue()), @@ -2531,7 +2715,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { private static Principal requireUserPrincipal(HttpRequest request) { Principal principal = request.getJDiscRequest().getUserPrincipal(); - if (principal == null) throw new InternalServerErrorException("Expected a user principal"); + if (principal == null) throw new RestApiException.InternalServerError("Expected a user principal"); return principal; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java index 80425609aa6..8c49df43c30 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java @@ -112,8 +112,7 @@ class JobControllerApiHandlerHelper { Slime slime = new Slime(); Cursor detailsObject = slime.setObject(); - Run run = jobController.run(runId) - .orElseThrow(() -> new IllegalStateException("Unknown run '" + runId + "'")); + Run run = jobController.run(runId); detailsObject.setBool("active", ! run.hasEnded()); detailsObject.setString("status", nameOf(run.status())); try { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java index 206303adc80..d45e69f781b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java @@ -24,6 +24,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.billing.CollectionMetho import com.yahoo.vespa.hosted.controller.api.integration.billing.InstrumentOwner; import com.yahoo.vespa.hosted.controller.api.integration.billing.PaymentInstrument; import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId; +import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistry; import com.yahoo.vespa.hosted.controller.api.role.Role; import com.yahoo.vespa.hosted.controller.api.role.SecurityContext; import com.yahoo.vespa.hosted.controller.tenant.Tenant; @@ -56,11 +57,13 @@ public class BillingApiHandler extends ThreadedHttpRequestHandler { private final BillingController billingController; private final ApplicationController applicationController; private final TenantController tenantController; + private final PlanRegistry planRegistry; public BillingApiHandler(Executor executor, Controller controller) { super(executor); this.billingController = controller.serviceRegistry().billingController(); + this.planRegistry = controller.serviceRegistry().planRegistry(); this.applicationController = controller.applications(); this.tenantController = controller.tenants(); } @@ -103,6 +106,7 @@ public class BillingApiHandler extends ThreadedHttpRequestHandler { if (path.matches("/billing/v1/billing")) return getBillingAllTenants(request.getProperty("until")); if (path.matches("/billing/v1/invoice/export")) return getAllBills(); if (path.matches("/billing/v1/invoice/tenant/{tenant}/line-item")) return getLineItems(path.get("tenant")); + if (path.matches("/billing/v1/plans")) return getPlans(); return ErrorResponse.notFoundError("Nothing at " + path); } @@ -295,6 +299,18 @@ public class BillingApiHandler extends ThreadedHttpRequestHandler { } } + private HttpResponse getPlans() { + var slime = new Slime(); + var root = slime.setObject(); + var plans = root.setArray("plans"); + for (var plan : planRegistry.all()) { + var p = plans.addObject(); + p.setString("id", plan.id().value()); + p.setString("name", plan.displayName()); + } + return new SlimeJsonResponse(slime); + } + private HttpResponse getLineItems(String tenant) { var slimeResponse = new Slime(); var root = slimeResponse.setObject(); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java index c72d8ceb089..a3b77e22f1d 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java @@ -9,6 +9,7 @@ import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.ThreadedHttpRequestHandler; import com.yahoo.restapi.ErrorResponse; import com.yahoo.restapi.Path; +import com.yahoo.restapi.RestApiException; import com.yahoo.restapi.SlimeJsonResponse; import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Cursor; @@ -24,7 +25,6 @@ import com.yahoo.vespa.hosted.controller.maintenance.ChangeManagementAssessor; import com.yahoo.vespa.hosted.controller.persistence.ChangeRequestSerializer; import com.yahoo.yolean.Exceptions; -import javax.ws.rs.BadRequestException; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -96,13 +96,13 @@ public class ChangeManagementApiHandler extends AuditLoggingRequestHandler { try { return SlimeUtils.jsonToSlime(request.getData().readAllBytes()).get(); } catch (IOException e) { - throw new BadRequestException("Failed to parse request body"); + throw new RestApiException.BadRequest("Failed to parse request body"); } } private static Inspector getInspectorFieldOrThrow(Inspector inspector, String field) { if (!inspector.field(field).valid()) - throw new BadRequestException("Field " + field + " cannot be null"); + throw new RestApiException.BadRequest("Field " + field + " cannot be null"); return inspector.field(field); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiHandler.java index 0c0680d9166..25ac90ac0ea 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiHandler.java @@ -12,6 +12,7 @@ import com.yahoo.restapi.ErrorResponse; import com.yahoo.restapi.MessageResponse; import com.yahoo.restapi.Path; import com.yahoo.restapi.ResourceResponse; +import com.yahoo.restapi.RestApiException; import com.yahoo.security.X509CertificateUtils; import com.yahoo.slime.Inspector; import com.yahoo.slime.SlimeUtils; @@ -27,7 +28,6 @@ import com.yahoo.vespa.hosted.controller.support.access.SupportAccess; import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence; import com.yahoo.yolean.Exceptions; -import javax.ws.rs.InternalServerErrorException; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -203,7 +203,7 @@ public class ControllerApiHandler extends AuditLoggingRequestHandler { private static Principal requireUserPrincipal(HttpRequest request) { Principal principal = request.getJDiscRequest().getUserPrincipal(); - if (principal == null) throw new InternalServerErrorException("Expected a user principal"); + if (principal == null) throw new RestApiException.InternalServerError("Expected a user principal"); return principal; } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java index 4aefb9ea7c2..e06c2c3ccbd 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java @@ -139,8 +139,7 @@ public class AthenzRoleFilter extends JsonSecurityRequestFilterBase { if ( identity.getDomain().equals(SCREWDRIVER_DOMAIN) && application.isPresent() - && tenant.isPresent() - && ! tenant.get().name().value().equals("sandbox")) + && tenant.isPresent()) futures.add(executor.submit(() -> { if ( tenant.get().type() == Tenant.Type.athenz && hasDeployerAccess(identity, ((AthenzTenant) tenant.get()).domain(), application.get(), zone)) diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java index 88fd3a58d23..853739ee9c3 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java @@ -19,6 +19,7 @@ import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; import com.yahoo.slime.SlimeUtils; +import com.yahoo.slime.Type; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler; import com.yahoo.vespa.hosted.controller.versions.OsVersionTarget; @@ -32,6 +33,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; import java.util.StringJoiner; +import java.util.function.Function; import java.util.logging.Level; import java.util.stream.Collectors; @@ -71,7 +73,7 @@ public class OsApiHandler extends AuditLoggingRequestHandler { private HttpResponse patch(HttpRequest request) { Path path = new Path(request.getUri()); - if (path.matches("/os/v1/")) return new SlimeJsonResponse(setOsVersion(request)); + if (path.matches("/os/v1/")) return setOsVersion(request); return ErrorResponse.notFoundError("Nothing at " + path); } @@ -130,36 +132,20 @@ public class OsApiHandler extends AuditLoggingRequestHandler { return zones.zones().stream().map(ZoneApi::getId).collect(Collectors.toList()); } - private Slime setOsVersion(HttpRequest request) { + private HttpResponse setOsVersion(HttpRequest request) { Slime requestData = toSlime(request.getData()); Inspector root = requestData.get(); - Inspector versionField = root.field("version"); - Inspector cloudField = root.field("cloud"); - Inspector upgradeBudgetField = root.field("upgradeBudget"); - boolean force = root.field("force").asBool(); - if (!versionField.valid() || !cloudField.valid() || !upgradeBudgetField.valid()) { - throw new IllegalArgumentException("Fields 'version', 'cloud' and 'upgradeBudget' are required"); - } - - CloudName cloud = CloudName.from(cloudField.asString()); - Version target; - try { - target = Version.fromString(versionField.asString()); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid version '" + versionField.asString() + "'", e); - } - Duration upgradeBudget; - try { - upgradeBudget = Duration.parse(upgradeBudgetField.asString()); - } catch (Exception e) { - throw new IllegalArgumentException("Invalid duration '" + upgradeBudgetField.asString() + "'", e); + CloudName cloud = parseStringField("cloud", root, CloudName::from); + if (requireField("version", root).type() == Type.NIX) { + controller.cancelOsUpgradeIn(cloud); + return new MessageResponse("Cleared target OS version for cloud '" + cloud.value() + "'"); } + Version target = parseStringField("version", root, Version::fromString); + Duration upgradeBudget = parseStringField("upgradeBudget", root, Duration::parse); + boolean force = root.field("force").asBool(); controller.upgradeOsIn(cloud, target, upgradeBudget, force); - Slime response = new Slime(); - Cursor cursor = response.setObject(); - cursor.setString("message", "Set target OS version for cloud '" + cloud.value() + "' to " + - target.toFullString() + " with upgrade budget " + upgradeBudget); - return response; + return new MessageResponse("Set target OS version for cloud '" + cloud.value() + "' to " + + target.toFullString() + " with upgrade budget " + upgradeBudget); } private Slime osVersions() { @@ -196,4 +182,19 @@ public class OsApiHandler extends AuditLoggingRequestHandler { } } + private static <T> T parseStringField(String name, Inspector root, Function<String, T> parser) { + String fieldValue = requireField(name, root).asString(); + try { + return parser.apply(fieldValue); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid " + name + " '" + fieldValue + "'", e); + } + } + + private static Inspector requireField(String name, Inspector root) { + Inspector field = root.field(name); + if (!field.valid()) throw new IllegalArgumentException("Field '" + name + "' is required"); + return field; + } + } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java index 1ccb3205816..fa6741ec8b8 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java @@ -11,7 +11,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry; import com.yahoo.vespa.hosted.controller.application.Endpoint; import com.yahoo.vespa.hosted.controller.application.Endpoint.Port; import com.yahoo.vespa.hosted.controller.application.EndpointId; -import com.yahoo.vespa.hosted.controller.application.SystemApplication; import java.util.List; import java.util.Objects; @@ -87,11 +86,6 @@ public class RoutingPolicy { /** Returns the zone endpoints of this */ public List<Endpoint> zoneEndpointsIn(SystemName system, RoutingMethod routingMethod, ZoneRegistry zoneRegistry) { - Optional<Endpoint> infraEndpoint = SystemApplication.matching(id.owner()) - .flatMap(app -> app.endpointIn(id.zone(), zoneRegistry)); - if (infraEndpoint.isPresent()) { - return List.of(infraEndpoint.get()); - } DeploymentId deployment = new DeploymentId(id.owner(), id.zone()); return List.of(endpoint(routingMethod).target(id.cluster(), deployment).in(system)); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java index 87691d2927a..81e9b1972b9 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.security; import com.yahoo.component.annotation.Inject; import com.yahoo.config.provision.TenantName; +import com.yahoo.restapi.RestApiException; import com.yahoo.vespa.flags.BooleanFlag; import com.yahoo.vespa.flags.FetchVector; import com.yahoo.vespa.flags.FlagSource; @@ -22,7 +23,6 @@ import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.Tenant; -import javax.ws.rs.ForbiddenException; import java.time.Instant; import java.util.List; import java.util.stream.Collectors; @@ -76,7 +76,7 @@ public class CloudAccessControl implements AccessControl { var trialTenants = billingController.tenantsWithPlan(tenantNames, trialPlanId).size(); if (maxTrialTenants.value() >= 0 && maxTrialTenants.value() <= trialTenants) { - throw new ForbiddenException("Too many tenants with trial plans, please contact the Vespa support team"); + throw new RestApiException.Forbidden("Too many tenants with trial plans, please contact the Vespa support team"); } } @@ -84,11 +84,11 @@ public class CloudAccessControl implements AccessControl { if (allowedByPrivilegedRole(auth0Credentials)) return; if (!allowedByFeatureFlag(auth0Credentials)) { - throw new ForbiddenException("You are not currently permitted to create tenants. Please contact the Vespa team to request access."); + throw new RestApiException.Forbidden("You are not currently permitted to create tenants. Please contact the Vespa team to request access."); } if(administeredTenants(auth0Credentials) >= 3) { - throw new ForbiddenException("You are already administering 3 tenants. If you need more, please contact the Vespa team."); + throw new RestApiException.Forbidden("You are already administering 3 tenants. If you need more, please contact the Vespa team."); } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/DeploymentStatistics.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/DeploymentStatistics.java index 179a64d9491..e3077cb232f 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/DeploymentStatistics.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/DeploymentStatistics.java @@ -18,25 +18,29 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.Stream; import static java.util.Comparator.naturalOrder; import static java.util.function.Function.identity; /** - * Statistics about deployments on a platform version. This is immutable. + * Statistics about deployments on a platform version. + * + * @param version the version these statistics are for + * @param failingUpgrades the runs on the version of this, for currently failing instances, where the failure may be because of the upgrade + * @param otherFailing all other failing runs on the version of this, for currently failing instances + * @param productionSuccesses the production runs where the last success was on the version of this + * @param runningUpgrade the currently running runs on the version of this, where an upgrade is attempted + * @param otherRunning all other currently running runs on the version on this * * @author jonmv */ -public class DeploymentStatistics { - - private final Version version; - private final List<Run> failingUpgrades; - private final List<Run> otherFailing; - private final List<Run> productionSuccesses; - private final List<Run> runningUpgrade; - private final List<Run> otherRunning; +public record DeploymentStatistics(Version version, + List<Run> failingUpgrades, + List<Run> otherFailing, + List<Run> productionSuccesses, + List<Run> runningUpgrade, + List<Run> otherRunning) { public DeploymentStatistics(Version version, List<Run> failingUpgrades, List<Run> otherFailing, List<Run> productionSuccesses, List<Run> runningUpgrade, List<Run> otherRunning) { @@ -48,26 +52,7 @@ public class DeploymentStatistics { this.otherRunning = List.copyOf(otherRunning); } - /** Returns the version these statistics are for. */ - public Version version() { return version; } - - /** Returns the runs on the version of this, for currently failing instances, where the failure may be because of the upgrade. */ - public List<Run> failingUpgrades() { return failingUpgrades; } - - /** Returns all other failing runs on the version of this, for currently failing instances. */ - public List<Run> otherFailing() { return otherFailing; } - - /** Returns the production runs where the last success was on the version of this. */ - public List<Run> productionSuccesses() { return productionSuccesses; } - - /** Returns the currently running runs on the version of this, where an upgrade is attempted. */ - public List<Run> runningUpgrade() { return runningUpgrade; } - - /** Returns all other currently running runs on the version on this. */ - public List<Run> otherRunning() { return otherRunning; } - public static List<DeploymentStatistics> compute(Collection<Version> infrastructureVersions, DeploymentStatusList statuses) { - Set<Version> allVersions = new HashSet<>(infrastructureVersions); Map<Version, List<Run>> failingUpgrade = new HashMap<>(); Map<Version, List<Run>> otherFailing = new HashMap<>(); @@ -154,8 +139,7 @@ public class DeploymentStatistics { productionSuccesses.getOrDefault(version, List.of()), runningUpgrade.getOrDefault(version, List.of()), otherRunning.getOrDefault(version, List.of()))) - .collect(Collectors.toUnmodifiableList()); - + .toList(); } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersion.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersion.java index 69773597f37..04f5d9866f8 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersion.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersion.java @@ -13,45 +13,20 @@ import java.util.Optional; /** * Version information for a node allocated to a {@link com.yahoo.vespa.hosted.controller.application.SystemApplication}. * - * This is immutable. - * * @author mpolden */ -public class NodeVersion { - - private final HostName hostname; - private final ZoneId zone; - private final Version currentVersion; - private final Version wantedVersion; - private final Optional<Instant> suspendedAt; - - public NodeVersion(HostName hostname, ZoneId zone, Version currentVersion, Version wantedVersion, - Optional<Instant> suspendedAt) { - this.hostname = Objects.requireNonNull(hostname, "hostname must be non-null"); - this.zone = Objects.requireNonNull(zone, "zone must be non-null"); - this.currentVersion = Objects.requireNonNull(currentVersion, "version must be non-null"); - this.wantedVersion = Objects.requireNonNull(wantedVersion, "wantedVersion must be non-null"); - this.suspendedAt = Objects.requireNonNull(suspendedAt, "suspendedAt must be non-null"); - } - - /** Hostname of this */ - public HostName hostname() { - return hostname; - } - - /** Zone of this */ - public ZoneId zone() { - return zone; - } - - /** Current version of this */ - public Version currentVersion() { - return currentVersion; - } +public record NodeVersion(HostName hostname, + ZoneId zone, + Version currentVersion, + Version wantedVersion, + Optional<Instant> suspendedAt) { - /** Wanted version of this */ - public Version wantedVersion() { - return wantedVersion; + public NodeVersion { + Objects.requireNonNull(hostname, "hostname must be non-null"); + Objects.requireNonNull(zone, "zone must be non-null"); + Objects.requireNonNull(currentVersion, "version must be non-null"); + Objects.requireNonNull(wantedVersion, "wantedVersion must be non-null"); + Objects.requireNonNull(suspendedAt, "suspendedAt must be non-null"); } /** Returns the duration of the change in this, measured relative to instant */ @@ -61,33 +36,11 @@ public class NodeVersion { return Duration.between(suspendedAt.get(), instant).abs(); } - /** The most recent time the node referenced in this suspended. This is empty if the node is not suspended. */ - public Optional<Instant> suspendedAt() { - return suspendedAt; - } - @Override public String toString() { return hostname + ": " + currentVersion + " -> " + wantedVersion + " [zone=" + zone + ", suspendedAt=" + suspendedAt.map(Instant::toString).orElse("<not suspended>") + "]"; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - NodeVersion that = (NodeVersion) o; - return hostname.equals(that.hostname) && - zone.equals(that.zone) && - currentVersion.equals(that.currentVersion) && - wantedVersion.equals(that.wantedVersion) && - suspendedAt.equals(that.suspendedAt); - } - - @Override - public int hashCode() { - return Objects.hash(hostname, zone, currentVersion, wantedVersion, suspendedAt); - } - /** Returns whether this is upgrading */ private boolean upgrading() { return currentVersion.isBefore(wantedVersion); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersion.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersion.java index c240a7ddf35..30a88733ed3 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersion.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersion.java @@ -12,41 +12,14 @@ import java.util.Objects; * * @author mpolden */ -public class OsVersion implements Comparable<OsVersion> { +public record OsVersion(Version version, CloudName cloud) implements Comparable<OsVersion> { private static final Comparator<OsVersion> comparator = Comparator.comparing(OsVersion::cloud) .thenComparing(OsVersion::version); - private final Version version; - private final CloudName cloud; - - public OsVersion(Version version, CloudName cloud) { - this.version = Objects.requireNonNull(version, "version must be non-null"); - this.cloud = Objects.requireNonNull(cloud, "cloud must be non-null"); - } - - /** The version number of this */ - public Version version() { - return version; - } - - /** The cloud where this OS version is used */ - public CloudName cloud() { - return cloud; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - OsVersion osVersion = (OsVersion) o; - return Objects.equals(version, osVersion.version) && - Objects.equals(cloud, osVersion.cloud); - } - - @Override - public int hashCode() { - return Objects.hash(version, cloud); + public OsVersion { + Objects.requireNonNull(version, "version must be non-null"); + Objects.requireNonNull(cloud, "cloud must be non-null"); } @Override diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionStatus.java index fc7fbe45767..8ee891ae8a6 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionStatus.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionStatus.java @@ -26,22 +26,15 @@ import java.util.stream.Collectors; * * @author mpolden */ -public class OsVersionStatus { +public record OsVersionStatus(Map<OsVersion, List<NodeVersion>> versions) { public static final OsVersionStatus empty = new OsVersionStatus(ImmutableMap.of()); - private final Map<OsVersion, List<NodeVersion>> versions; - /** Public for serialization purpose only. Use {@link OsVersionStatus#compute(Controller)} for an up-to-date status */ public OsVersionStatus(Map<OsVersion, List<NodeVersion>> versions) { this.versions = ImmutableMap.copyOf(Objects.requireNonNull(versions, "versions must be non-null")); } - /** All known OS versions and their nodes */ - public Map<OsVersion, List<NodeVersion>> versions() { - return versions; - } - /** Returns nodes eligible for OS upgrades that exist in given cloud */ public List<NodeVersion> nodesIn(CloudName cloud) { return versions.entrySet().stream() @@ -91,7 +84,7 @@ public class OsVersionStatus { return controller.zoneRegistry().osUpgradePolicies().stream() .flatMap(upgradePolicy -> upgradePolicy.steps().stream()) .flatMap(Collection::stream) - .collect(Collectors.toUnmodifiableList()); + .toList(); } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionTarget.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionTarget.java index 1c27058a6ef..0a13244ce5e 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionTarget.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionTarget.java @@ -12,54 +12,17 @@ import java.util.Objects; * * @author mpolden */ -public class OsVersionTarget implements VersionTarget, Comparable<OsVersionTarget> { - - // 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 OsVersion osVersion; - private final Duration upgradeBudget; - private final Instant scheduledAt; - - public OsVersionTarget(OsVersion osVersion, Duration upgradeBudget, Instant scheduledAt) { - this.osVersion = Objects.requireNonNull(osVersion); - this.upgradeBudget = Objects.requireNonNull(upgradeBudget); - this.scheduledAt = Objects.requireNonNull(scheduledAt); +public record OsVersionTarget(OsVersion osVersion, + Duration upgradeBudget, + Instant scheduledAt) implements VersionTarget, Comparable<OsVersionTarget> { + + public OsVersionTarget { + Objects.requireNonNull(osVersion); + Objects.requireNonNull(upgradeBudget); + Objects.requireNonNull(scheduledAt); if (upgradeBudget.isNegative()) throw new IllegalArgumentException("upgradeBudget cannot be negative"); } - /** The OS version contained in this target */ - public OsVersion osVersion() { - return osVersion; - } - - /** The total time budget across all zones for applying target, if any */ - public Duration upgradeBudget() { - return upgradeBudget; - } - - /** Returns when this target was scheduled */ - public Instant scheduledAt() { - return scheduledAt; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - OsVersionTarget that = (OsVersionTarget) o; - return osVersion.equals(that.osVersion) && upgradeBudget.equals(that.upgradeBudget) && scheduledAt.equals(that.scheduledAt); - } - - @Override - public int hashCode() { - return Objects.hash(osVersion, upgradeBudget, scheduledAt); - } - @Override public int compareTo(OsVersionTarget o) { return osVersion.compareTo(o.osVersion); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java index 117abd52193..e937e8af60d 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java @@ -27,16 +27,12 @@ import java.util.stream.Collectors; * The versions in use are the set of all versions running in current applications, versions * of config servers in all zones, and the version of this controller itself. * - * This is immutable. - * * @author bratseth * @author mpolden */ -public class VersionStatus { +public record VersionStatus(List<VespaVersion> versions) { private static final Logger log = Logger.getLogger(VersionStatus.class.getName()); - - private final List<VespaVersion> versions; /** Create a version status. DO NOT USE: Public for testing and serialization only */ public VersionStatus(List<VespaVersion> versions) { @@ -172,7 +168,7 @@ public class VersionStatus { var nodes = controller.serviceRegistry().configServer().nodeRepository() .list(zone.getId(), NodeFilter.all().applications(application.id())).stream() .filter(SystemUpgrader::eligibleForUpgrade) - .collect(Collectors.toList()); + .toList(); if (nodes.isEmpty()) continue; boolean configConverged = application.configConvergedIn(zone.getId(), controller, Optional.empty()); if (!configConverged) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java index 162890ff74d..7f33f612cd0 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java @@ -16,35 +16,17 @@ import static com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy; /** * Information about a particular Vespa version. * VespaVersions are identified by their version number and ordered by increasing version numbers. - * - * This is immutable. * * @author bratseth */ -public class VespaVersion implements Comparable<VespaVersion> { - - private final Version version; - private final String releaseCommit; - private final Instant committedAt; - private final boolean isControllerVersion; - private final boolean isSystemVersion; - private final boolean isReleased; - private final List<NodeVersion> nodeVersions; - private final Confidence confidence; - - public VespaVersion(Version version, String releaseCommit, Instant committedAt, - boolean isControllerVersion, boolean isSystemVersion, boolean isReleased, - List<NodeVersion> nodeVersions, - Confidence confidence) { - this.version = version; - this.releaseCommit = releaseCommit; - this.committedAt = committedAt; - this.isControllerVersion = isControllerVersion; - this.isSystemVersion = isSystemVersion; - this.isReleased = isReleased; - this.nodeVersions = nodeVersions; - this.confidence = confidence; - } +public record VespaVersion(Version version, + String releaseCommit, + Instant committedAt, + boolean isControllerVersion, + boolean isSystemVersion, + boolean isReleased, + List<NodeVersion> nodeVersions, + Confidence confidence) implements Comparable<VespaVersion> { public static Confidence confidenceFrom(DeploymentStatistics statistics, Controller controller) { InstanceList all = InstanceList.from(controller.jobController().deploymentStatuses(ApplicationList.from(controller.applications().asList()) diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersionTarget.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersionTarget.java index 78d6d0ebf29..26890cfd8f8 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersionTarget.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersionTarget.java @@ -10,24 +10,10 @@ import java.util.Objects; * * @author mpolden */ -public class VespaVersionTarget implements VersionTarget { +public record VespaVersionTarget(Version version, boolean downgrade) implements VersionTarget { - private final Version version; - private final boolean downgrade; - - public VespaVersionTarget(Version version, boolean downgrade) { - this.version = Objects.requireNonNull(version); - this.downgrade = downgrade; - } - - @Override - public Version version() { - return version; - } - - @Override - public boolean downgrade() { - return downgrade; + public VespaVersionTarget { + Objects.requireNonNull(version); } @Override diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java index f4f50de59d7..53557bafcb0 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java @@ -811,14 +811,6 @@ public class ControllerTest { } @Test - public void testDeployApplicationPackageWithApplicationDir() { - ApplicationPackage applicationPackage = new ApplicationPackageBuilder() - .region("us-west-1") - .build(true); - tester.newDeploymentContext().submit(applicationPackage); - } - - @Test public void testDeployApplicationWithWarnings() { var context = tester.newDeploymentContext(); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() @@ -1195,4 +1187,20 @@ public class ControllerTest { assertEquals(cloudAccount, tester.controllerTester().configServer().cloudAccount(context.deploymentIdIn(zone)).get().value()); } + @Test + public void testSubmitWithElementDeprecatedOnPreviousMajor() { + DeploymentContext context = tester.newDeploymentContext(); + var applicationPackage = new ApplicationPackageBuilder() + .compileVersion(Version.fromString("8.1")) + .region("us-west-1") + .globalServiceId("qrs") + .build(); + try { + context.submit(applicationPackage).deploy(); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Element 'prod' contains attribute 'global-service-id' deprecated since major version 7")); + } + } + } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java index 35cca0e1f1f..81fc610d1f6 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java @@ -241,9 +241,15 @@ public class EndpointTest { var cluster = ClusterSpec.Id.from("default"); var prodZone = ZoneId.from("prod", "us-north-2"); Map<String, Endpoint> tests = Map.of( - "https://a1.t1.us-north-1.w.vespa-app.cloud/", + "https://a1.t1.aws-us-north-1.w.vespa-app.cloud/", Endpoint.of(instance1) - .targetRegion(cluster, ZoneId.from("prod", "us-north-1a")) + .targetRegion(cluster, ZoneId.from("prod", "aws-us-north-1a")) + .routingMethod(RoutingMethod.exclusive) + .on(Port.tls()) + .in(SystemName.Public), + "https://a1.t1.gcp-us-south1.w.vespa-app.cloud/", + Endpoint.of(instance1) + .targetRegion(cluster, ZoneId.from("prod", "gcp-us-south1-c")) .routingMethod(RoutingMethod.exclusive) .on(Port.tls()) .in(SystemName.Public), @@ -261,14 +267,13 @@ public class EndpointTest { .in(SystemName.Public) ); tests.forEach((expected, endpoint) -> assertEquals(expected, endpoint.url().toString())); - Endpoint endpoint = Endpoint.of(instance1) - .targetRegion(cluster, ZoneId.from("prod", "us-north-1a")) - .routingMethod(RoutingMethod.exclusive) - .on(Port.tls()) - .in(SystemName.main); + + assertEquals("Availability zone is removed from region", + "aws-us-north-1", + tests.get("https://a1.t1.aws-us-north-1.w.vespa-app.cloud/").targets().get(0).deployment().zoneId().region().value()); assertEquals("Availability zone is removed from region", - "us-north-1", - endpoint.targets().get(0).deployment().zoneId().region().value()); + "gcp-us-south1", + tests.get("https://a1.t1.gcp-us-south1.w.vespa-app.cloud/").targets().get(0).deployment().zoneId().region().value()); } @Test diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java index 99e22302c73..0458f77fc00 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java @@ -81,23 +81,6 @@ public class ApplicationPackageTest { } @Test - public void testMetaDataWithLegacyApplicationDirectory() { - byte[] zip = ApplicationPackage.filesZip(Map.of("application/deployment.xml", deploymentXml.getBytes(UTF_8), - "application/services.xml", servicesXml.getBytes(UTF_8), - "application/jdisc.xml", jdiscXml.getBytes(UTF_8), - "application/content/content.xml", contentXml.getBytes(UTF_8), - "application/content/nodes.xml", nodesXml.getBytes(UTF_8), - "application/gurba", "gurba".getBytes(UTF_8))); - - assertEquals(Map.of("deployment.xml", deploymentXml, - "services.xml", servicesXml, - "jdisc.xml", jdiscXml, - "content/content.xml", contentXml, - "content/nodes.xml", nodesXml), - unzip(new ApplicationPackage(zip, false).metaDataZip())); - } - - @Test public void testMetaDataWithMissingFiles() { byte[] zip = ApplicationPackage.filesZip(Map.of("services.xml", servicesXml.getBytes(UTF_8))); 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 1a052b6a578..4c3d76b1b17 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 @@ -32,7 +32,7 @@ public class CuratorArchiveBucketDbTest { assertEquals(Optional.of(URI.create("s3://existingBucket/default/")), bucketDb.archiveUriFor(ZoneId.defaultId(), TenantName.defaultName(), true)); // Assigns to existing bucket while there is space - IntStream.range(0, 29).forEach(i -> + IntStream.range(0, 4).forEach(i -> assertEquals( Optional.of(URI.create("s3://existingBucket/tenant" + i + "/")), bucketDb .archiveUriFor(ZoneId.defaultId(), TenantName.from("tenant" + i), true))); @@ -47,7 +47,7 @@ public class CuratorArchiveBucketDbTest { assertEquals(Optional.empty(), bucketDb.archiveUriFor(ZoneId.from("prod.us-east-3"), TenantName.from("newTenant"), false)); // 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()); + 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), diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java index 5d1a677bf51..ea8f4db0346 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java @@ -147,7 +147,10 @@ public class ApplicationPackageBuilder { } public ApplicationPackageBuilder region(String regionName) { - return region(RegionName.from(regionName), true); + prodBody.append(" <region>") + .append(regionName) + .append("</region>\n"); + return this; } public ApplicationPackageBuilder region(RegionName regionName, boolean active) { @@ -334,23 +337,15 @@ public class ApplicationPackageBuilder { } public ApplicationPackage build() { - return build(false); - } - - public ApplicationPackage build(boolean useApplicationDir) { - String dir = ""; - if (useApplicationDir) { - dir = "application/"; - } ByteArrayOutputStream zip = new ByteArrayOutputStream(); try (ZipOutputStream out = new ZipOutputStream(zip)) { out.setLevel(Deflater.NO_COMPRESSION); // This is for testing purposes so we skip compression for performance - writeZipEntry(out, dir + "deployment.xml", deploymentSpec()); - writeZipEntry(out, dir + "validation-overrides.xml", validationOverrides()); - writeZipEntry(out, dir + "search-definitions/test.sd", searchDefinition()); - writeZipEntry(out, dir + "build-meta.json", buildMeta(compileVersion)); + writeZipEntry(out, "deployment.xml", deploymentSpec()); + writeZipEntry(out, "validation-overrides.xml", validationOverrides()); + writeZipEntry(out, "schemas/test.sd", searchDefinition()); + writeZipEntry(out, "build-meta.json", buildMeta(compileVersion)); if (!trustedCertificates.isEmpty()) { - writeZipEntry(out, dir + "security/clients.pem", X509CertificateUtils.toPem(trustedCertificates).getBytes(UTF_8)); + writeZipEntry(out, "security/clients.pem", X509CertificateUtils.toPem(trustedCertificates).getBytes(UTF_8)); } } catch (IOException e) { throw new UncheckedIOException(e); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java index ae4b6259da1..b3db4a8b845 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java @@ -355,7 +355,7 @@ public class DeploymentContext { var job = jobId(type); RunId id = currentRun(job).id(); runner.advance(currentRun(job)); - Run run = jobs.run(id).get(); + Run run = jobs.run(id); assertTrue(run.hasFailed()); assertTrue(run.hasEnded()); if (messagePart.isPresent()) { @@ -441,11 +441,10 @@ public class DeploymentContext { RunId id = currentRun(job).id(); - assertEquals(unfinished, jobs.run(id).get().stepStatuses().get(Step.endTests)); + assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.endTests)); tester.cloud().set(noTests ? Status.NO_TESTS : Status.FAILURE); runner.advance(currentRun(job)); - assertTrue(jobs.run(id).get().hasEnded()); - assertEquals(noTests, jobs.run(id).get().hasSucceeded()); + assertTrue(jobs.run(id).hasEnded()); assertTrue(configServer().nodeRepository().list(job.type().zone(), NodeFilter.all().applications(TesterId.of(instanceId).id())).isEmpty()); return this; @@ -482,7 +481,7 @@ public class DeploymentContext { assertSame(RunStatus.aborted, run.status()); assertFalse(run.hasEnded()); runner.advance(run); - assertTrue(jobs.run(run.id()).get().hasEnded()); + assertTrue(jobs.run(run.id()).hasEnded()); return this; } @@ -494,8 +493,8 @@ public class DeploymentContext { doDeploy(job); tester.clock().advance(Timeouts.of(tester.controller().system()).noNodesDown().plusSeconds(1)); runner.advance(currentRun(job)); - assertTrue(jobs.run(id).get().hasFailed()); - assertTrue(jobs.run(id).get().hasEnded()); + assertTrue(jobs.run(id).hasFailed()); + assertTrue(jobs.run(id).hasEnded()); return this; } @@ -508,8 +507,8 @@ public class DeploymentContext { doUpgrade(job); tester.clock().advance(Timeouts.of(tester.controller().system()).noNodesDown().plusSeconds(1)); runner.advance(currentRun(job)); - assertTrue(jobs.run(id).get().hasFailed()); - assertTrue(jobs.run(id).get().hasEnded()); + assertTrue(jobs.run(id).hasFailed()); + assertTrue(jobs.run(id).hasEnded()); return this; } @@ -541,8 +540,8 @@ public class DeploymentContext { configServer().convergeServices(instanceId, testZone); configServer().convergeServices(testerId.id(), testZone); runner.run(); - assertEquals(unfinished, jobs.run(id).get().stepStatuses().get(Step.endTests)); - assertTrue(jobs.run(id).get().steps().get(Step.endTests).startTime().isPresent()); + assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.endTests)); + assertTrue(jobs.run(id).steps().get(Step.endTests).startTime().isPresent()); return id; } @@ -572,21 +571,21 @@ public class DeploymentContext { doInstallTester(job); if (job.type().equals(stagingTest)) { // Do the initial deployment and installation of the real application. - assertEquals(unfinished, jobs.run(id).get().stepStatuses().get(Step.installInitialReal)); + assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.installInitialReal)); tester.configServer().nodeRepository().doUpgrade(deployment, Optional.empty(), tester.configServer().application(job.application(), zone).get().version().get()); configServer().convergeServices(id.application(), zone); runner.advance(currentRun(job)); - assertEquals(succeeded, jobs.run(id).get().stepStatuses().get(Step.installInitialReal)); + assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.installInitialReal)); // All installation is complete and endpoints are ready, so setup may begin. - assertEquals(succeeded, jobs.run(id).get().stepStatuses().get(Step.installInitialReal)); - assertEquals(succeeded, jobs.run(id).get().stepStatuses().get(Step.installTester)); - assertEquals(succeeded, jobs.run(id).get().stepStatuses().get(Step.startStagingSetup)); + assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.installInitialReal)); + assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.installTester)); + assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.startStagingSetup)); - assertEquals(unfinished, jobs.run(id).get().stepStatuses().get(Step.endStagingSetup)); + assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.endStagingSetup)); tester.cloud().set(Status.SUCCESS); runner.advance(currentRun(job)); - assertEquals(succeeded, jobs.run(id).get().stepStatuses().get(Step.endStagingSetup)); + assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.endStagingSetup)); } } @@ -596,7 +595,7 @@ public class DeploymentContext { ZoneId zone = job.type().zone(); DeploymentId deployment = new DeploymentId(job.application(), zone); - assertEquals(unfinished, jobs.run(id).get().stepStatuses().get(Step.installReal)); + assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.installReal)); configServer().nodeRepository().doUpgrade(deployment, Optional.empty(), tester.configServer().application(job.application(), zone).get().version().get()); runner.advance(currentRun(job)); } @@ -616,15 +615,15 @@ public class DeploymentContext { RunId id = currentRun(job).id(); ZoneId zone = job.type().zone(); - assertEquals(unfinished, jobs.run(id).get().stepStatuses().get(Step.installReal)); + assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.installReal)); configServer().convergeServices(id.application(), zone); runner.advance(currentRun(job)); if (job.type().environment().isManuallyDeployed()) { - assertEquals(succeeded, jobs.run(id).get().stepStatuses().get(Step.installReal)); - assertTrue(jobs.run(id).get().hasEnded()); + assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.installReal)); + assertTrue(jobs.run(id).hasEnded()); return; } - assertEquals("Status of " + id, succeeded, jobs.run(id).get().stepStatuses().get(Step.installReal)); + assertEquals("Status of " + id, succeeded, jobs.run(id).stepStatuses().get(Step.installReal)); } /** Installs tester and starts tests. */ @@ -632,13 +631,13 @@ public class DeploymentContext { RunId id = currentRun(job).id(); ZoneId zone = job.type().zone(); - assertEquals(unfinished, jobs.run(id).get().stepStatuses().get(Step.installTester)); + assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.installTester)); configServer().nodeRepository().doUpgrade(new DeploymentId(TesterId.of(job.application()).id(), zone), Optional.empty(), tester.configServer().application(id.tester().id(), zone).get().version().get()); runner.advance(currentRun(job)); - assertEquals(unfinished, jobs.run(id).get().stepStatuses().get(Step.installTester)); + assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.installTester)); configServer().convergeServices(TesterId.of(id.application()).id(), zone); runner.advance(currentRun(job)); - assertEquals(succeeded, jobs.run(id).get().stepStatuses().get(Step.installTester)); + assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.installTester)); runner.advance(currentRun(job)); } @@ -649,15 +648,15 @@ public class DeploymentContext { // All installation is complete and endpoints are ready, so tests may begin. if (job.type().isDeployment()) - assertEquals(succeeded, jobs.run(id).get().stepStatuses().get(Step.installReal)); - assertEquals(succeeded, jobs.run(id).get().stepStatuses().get(Step.installTester)); - assertEquals(succeeded, jobs.run(id).get().stepStatuses().get(Step.startTests)); + assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.installReal)); + assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.installTester)); + assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.startTests)); - assertEquals(unfinished, jobs.run(id).get().stepStatuses().get(Step.endTests)); + assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.endTests)); tester.cloud().set(Status.SUCCESS); runner.advance(currentRun(job)); - assertTrue(jobs.run(id).get().hasEnded()); - assertFalse(jobs.run(id).get().hasFailed()); + assertTrue(jobs.run(id).hasEnded()); + assertFalse(jobs.run(id).hasFailed()); Instance instance = tester.application(TenantAndApplicationId.from(instanceId)).require(id.application().instance()); assertEquals(job.type().isProduction(), instance.deployments().containsKey(zone)); assertTrue(configServer().nodeRepository().list(zone, NodeFilter.all().applications(TesterId.of(instance.id()).id())).isEmpty()); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java index 4219c52be20..78e7606d7c6 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java @@ -136,8 +136,8 @@ public class DeploymentTester { triggerJobs(); for (Run run : jobs.active()) { jobs.abort(run.id(), "DeploymentTester.abortAll"); - runner.advance(jobs.run(run.id()).get()); - assertTrue(jobs.run(run.id()).get().hasEnded()); + runner.advance(jobs.run(run.id())); + assertTrue(jobs.run(run.id()).hasEnded()); } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java index beda9bd551d..d602ee8cde3 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java @@ -2267,4 +2267,16 @@ public class DeploymentTriggerTest { assertEquals(Set.of(), tests.deploymentStatus().jobsToRun().keySet()); } + @Test + public void testNoTests() { + DeploymentContext app = tester.newDeploymentContext(); + app.submit(new ApplicationPackageBuilder().systemTest().region("us-east-3").build()); + + // Declared tests must have run actual tests to succeed. + app.failTests(systemTest, true); + assertFalse(tester.jobs().last(app.instanceId(), systemTest).get().hasSucceeded()); + app.failTests(stagingTest, true); + assertTrue(tester.jobs().last(app.instanceId(), stagingTest).get().hasSucceeded()); + } + } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java index 031cdaa84ae..d781b1f1d3f 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java @@ -6,7 +6,6 @@ import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.AthenzService; import com.yahoo.config.provision.HostName; -import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.zone.RoutingMethod; import com.yahoo.config.provision.zone.ZoneId; @@ -24,23 +23,14 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud. import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer; import com.yahoo.vespa.hosted.controller.application.SystemApplication; import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage; -import com.yahoo.vespa.hosted.controller.application.pkg.TestPackage; -import com.yahoo.vespa.hosted.controller.config.ControllerConfig; import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock; import com.yahoo.vespa.hosted.controller.maintenance.JobRunner; import org.junit.Before; import org.junit.Test; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.security.cert.X509Certificate; import java.time.Duration; import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -60,7 +50,6 @@ import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.success; import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.failed; import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.succeeded; import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.unfinished; -import static java.nio.charset.StandardCharsets.UTF_8; import static java.time.temporal.ChronoUnit.SECONDS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -131,19 +120,19 @@ public class InternalStepRunnerTest { HostName host = tester.configServer().hostFor(instanceId, zone); tester.runner().run(); - assertEquals(succeeded, tester.jobs().run(id).get().stepStatuses().get(Step.deployReal)); + assertEquals(succeeded, tester.jobs().run(id).stepStatuses().get(Step.deployReal)); tester.configServer().convergeServices(app.instanceId(), zone); - assertEquals(unfinished, tester.jobs().run(id).get().stepStatuses().get(Step.installReal)); + assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.installReal)); tester.configServer().nodeRepository().doRestart(app.deploymentIdIn(zone), Optional.of(host)); tester.configServer().nodeRepository().requestReboot(app.deploymentIdIn(zone), Optional.of(host)); tester.runner().run(); - assertEquals(unfinished, tester.jobs().run(id).get().stepStatuses().get(Step.installReal)); + assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.installReal)); tester.clock().advance(InternalStepRunner.Timeouts.of(system()).noNodesDown().plus(Duration.ofSeconds(1))); tester.runner().run(); - assertEquals(installationFailed, tester.jobs().run(id).get().status()); + assertEquals(installationFailed, tester.jobs().run(id).status()); } @Test @@ -262,7 +251,7 @@ public class InternalStepRunnerTest { @Test public void noTestsThenErrorIsError() { RunId id = app.startSystemTestTests(); - Run run = tester.jobs().run(id).get(); + Run run = tester.jobs().run(id); run = run.with(noTests, new LockedStep(() -> { }, Step.endTests)); assertFalse(run.hasFailed()); run = run.with(RunStatus.error, new LockedStep(() -> { }, Step.deactivateReal)); @@ -275,8 +264,8 @@ public class InternalStepRunnerTest { RunId id = app.startSystemTestTests(); tester.cloud().set(Status.NO_TESTS); tester.runner().run(); - assertEquals(succeeded, tester.jobs().run(id).get().stepStatuses().get(Step.endTests)); - Run run = tester.jobs().run(id).get(); + assertEquals(succeeded, tester.jobs().run(id).stepStatuses().get(Step.endTests)); + Run run = tester.jobs().run(id); assertEquals(noTests, run.status()); } @@ -285,7 +274,7 @@ public class InternalStepRunnerTest { RunId id = app.startSystemTestTests(); tester.cloud().set(TesterCloud.Status.NOT_STARTED); tester.runner().run(); - assertEquals(failed, tester.jobs().run(id).get().stepStatuses().get(Step.endTests)); + assertEquals(failed, tester.jobs().run(id).stepStatuses().get(Step.endTests)); } @Test @@ -299,7 +288,7 @@ public class InternalStepRunnerTest { assertTestLogEntries(id, Step.endTests, new LogEntry(lastId + 1, Instant.ofEpochMilli(321), error, "Failure!"), new LogEntry(lastId + 2, tester.clock().instant(), info, "Tests failed.")); - assertEquals(failed, tester.jobs().run(id).get().stepStatuses().get(Step.endTests)); + assertEquals(failed, tester.jobs().run(id).stepStatuses().get(Step.endTests)); } @Test @@ -310,7 +299,7 @@ public class InternalStepRunnerTest { long lastId = tester.jobs().details(id).get().lastId().getAsLong(); tester.runner().run(); - assertEquals(failed, tester.jobs().run(id).get().stepStatuses().get(Step.endTests)); + assertEquals(failed, tester.jobs().run(id).stepStatuses().get(Step.endTests)); assertTestLogEntries(id, Step.endTests, new LogEntry(lastId + 1, Instant.ofEpochMilli(123), error, "Error!"), new LogEntry(lastId + 2, tester.clock().instant(), info, "Tester failed running its tests!")); @@ -320,7 +309,7 @@ public class InternalStepRunnerTest { public void testsSucceedWhenTheyDoRemotely() { RunId id = app.startSystemTestTests(); tester.runner().run(); - assertEquals(unfinished, tester.jobs().run(id).get().stepStatuses().get(Step.endTests)); + assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.endTests)); var testZone = DeploymentContext.systemTest.zone(); Inspector configObject = SlimeUtils.jsonToSlime(tester.cloud().config()).get(); assertEquals(app.instanceId().serializedForm(), configObject.field("application").asString()); @@ -349,7 +338,7 @@ public class InternalStepRunnerTest { new LogEntry(lastId + 2, Instant.ofEpochMilli(1234), info, "Steady!"), new LogEntry(lastId + 3, Instant.ofEpochMilli(12345), info, "Success!"), new LogEntry(lastId + 4, tester.clock().instant(), info, "Tests completed successfully.")); - assertEquals(succeeded, tester.jobs().run(id).get().stepStatuses().get(Step.endTests)); + assertEquals(succeeded, tester.jobs().run(id).stepStatuses().get(Step.endTests)); } @Test @@ -362,16 +351,16 @@ public class InternalStepRunnerTest { long lastId1 = tester.jobs().details(id).get().lastId().getAsLong(); Instant instant1 = tester.clock().instant(); tester.runner().run(); - assertEquals(unfinished, tester.jobs().run(id).get().stepStatuses().get(Step.endTests)); - assertEquals(running, tester.jobs().run(id).get().status()); + assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.endTests)); + assertEquals(running, tester.jobs().run(id).status()); tester.cloud().clearLog(); // Test sleeps for a while. tester.runner().run(); - assertEquals(unfinished, tester.jobs().run(id).get().stepStatuses().get(Step.deployTester)); + assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.deployTester)); tester.clock().advance(Duration.ofSeconds(899)); tester.runner().run(); - assertEquals(unfinished, tester.jobs().run(id).get().stepStatuses().get(Step.deployTester)); + assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.deployTester)); tester.clock().advance(JobRunner.jobTimeout); var testZone = DeploymentContext.systemTest.zone(); @@ -380,14 +369,14 @@ public class InternalStepRunnerTest { tester.configServer().convergeServices(app.instanceId(), testZone); tester.configServer().convergeServices(app.testerId().id(), testZone); tester.runner().run(); - assertEquals(unfinished, tester.jobs().run(id).get().stepStatuses().get(Step.endTests)); - assertTrue(tester.jobs().run(id).get().steps().get(Step.endTests).startTime().isPresent()); + assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.endTests)); + assertTrue(tester.jobs().run(id).steps().get(Step.endTests).startTime().isPresent()); tester.cloud().set(TesterCloud.Status.SUCCESS); tester.cloud().testReport(TestReport.fromJson("{\"bar\":2}")); long lastId2 = tester.jobs().details(id).get().lastId().getAsLong(); tester.runner().run(); - assertEquals(success, tester.jobs().run(id).get().status()); + assertEquals(success, tester.jobs().run(id).status()); assertTestLogEntries(id, Step.endTests, new LogEntry(lastId1 + 1, Instant.ofEpochMilli(123), info, "Not enough data!"), @@ -405,7 +394,7 @@ public class InternalStepRunnerTest { tester.jobs().deploy(app.instanceId(), DeploymentContext.devUsEast1, Optional.empty(), applicationPackage()); tester.runner().run(); RunId id = tester.jobs().last(app.instanceId(), DeploymentContext.devUsEast1).get().id(); - assertEquals(unfinished, tester.jobs().run(id).get().stepStatuses().get(Step.installReal)); + assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.installReal)); Version version = new Version("7.8.9"); Future<?> concurrentDeployment = Executors.newSingleThreadExecutor().submit(() -> { @@ -420,7 +409,7 @@ public class InternalStepRunnerTest { tester.runner().run(); // Job run order determined by JobType enum order per application. tester.configServer().convergeServices(app.instanceId(), zone); - assertEquals(unfinished, tester.jobs().run(id).get().stepStatuses().get(Step.installReal)); + assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.installReal)); assertEquals(applicationPackage().hash(), tester.configServer().application(app.instanceId(), zone).get().applicationPackage().hash()); assertEquals(otherPackage.hash(), tester.configServer().application(app.instanceId(), DeploymentContext.perfUsEast3.zone()).get().applicationPackage().hash()); @@ -455,40 +444,53 @@ public class InternalStepRunnerTest { @Test public void vespaLogIsCopied() { // Tests fail. We should get logs. This fails too, on the first attempt. + tester.controllerTester().computeVersionStatus(); RunId id = app.startSystemTestTests(); tester.cloud().set(TesterCloud.Status.ERROR); tester.configServer().setLogStream(() -> { throw new ConfigServerException(ConfigServerException.ErrorCode.NOT_FOUND, "404", "context"); }); long lastId = tester.jobs().details(id).get().lastId().getAsLong(); tester.runner().run(); - assertEquals(failed, tester.jobs().run(id).get().stepStatuses().get(Step.endTests)); - assertEquals(unfinished, tester.jobs().run(id).get().stepStatuses().get(Step.copyVespaLogs)); + assertEquals(failed, tester.jobs().run(id).stepStatuses().get(Step.endTests)); + assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.copyVespaLogs)); assertTestLogEntries(id, Step.copyVespaLogs, new LogEntry(lastId + 2, tester.clock().instant(), info, - "Found no logs, but will retry"), - new LogEntry(lastId + 4, tester.clock().instant(), info, "Found no logs, but will retry")); // Config servers now provide the log, and we get it. - tester.configServer().setLogStream(() -> vespaLog); + tester.configServer().setLogStream(() -> vespaLog(tester.clock().instant())); tester.runner().run(); - assertEquals(failed, tester.jobs().run(id).get().stepStatuses().get(Step.endTests)); + assertEquals(failed, tester.jobs().run(id).stepStatuses().get(Step.endTests)); assertTestLogEntries(id, Step.copyVespaLogs, new LogEntry(lastId + 2, tester.clock().instant(), info, "Found no logs, but will retry"), - new LogEntry(lastId + 4, tester.clock().instant(), info, - "Found no logs, but will retry"), - new LogEntry(lastId + 5, Instant.EPOCH.plus(3554970337935104L, ChronoUnit.MICROS), info, + new LogEntry(lastId + 3, tester.clock().instant().minusSeconds(4), info, + "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstdout\n" + + "ERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)"), + new LogEntry(lastId + 4, tester.clock().instant().minusSeconds(4), info, + "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstdout\n" + + "ERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)"), + /* + new LogEntry(lastId + 5, tester.clock().instant().minusSeconds(4), info, + "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstdout\n" + + "ERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)"), + new LogEntry(lastId + 6, tester.clock().instant().minusSeconds(4), info, "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstdout\n" + "ERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)"), - new LogEntry(lastId + 6, Instant.EPOCH.plus(3554970337947777L, ChronoUnit.MICROS), info, + */ + new LogEntry(lastId + 5, tester.clock().instant().minusSeconds(3), info, "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstdout\n" + "ERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)"), - new LogEntry(lastId + 7, Instant.EPOCH.plus(3554970337947820L, ChronoUnit.MICROS), info, + new LogEntry(lastId + 6, tester.clock().instant().minusSeconds(3), warning, + "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstderr\n" + + "java.lang.NullPointerException\n\tat org.apache.felix.framework.BundleRevisionImpl.calculateContentPath(BundleRevisionImpl.java:438)\n\tat org.apache.felix.framework.BundleRevisionImpl.initializeContentPath(BundleRevisionImpl.java:371)")); + /* + new LogEntry(lastId + 9, tester.clock().instant().minusSeconds(3), info, "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstdout\n" + "ERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)"), - new LogEntry(lastId + 8, Instant.EPOCH.plus(3554970337947845L, ChronoUnit.MICROS), warning, + new LogEntry(lastId + 10, tester.clock().instant().minusSeconds(3), warning, "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstderr\n" + "java.lang.NullPointerException\n\tat org.apache.felix.framework.BundleRevisionImpl.calculateContentPath(BundleRevisionImpl.java:438)\n\tat org.apache.felix.framework.BundleRevisionImpl.initializeContentPath(BundleRevisionImpl.java:371)")); + */ } @Test @@ -509,21 +511,21 @@ public class InternalStepRunnerTest { throw new ConfigServerException(ConfigServerException.ErrorCode.PARENT_HOST_NOT_READY, "provisioning", "deploy tester"); }); tester.runner().run(); - assertEquals(unfinished, tester.jobs().run(id).get().stepStatuses().get(Step.deployTester)); - assertEquals(unfinished, tester.jobs().run(id).get().stepStatuses().get(Step.deployReal)); + assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.deployTester)); + assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.deployReal)); List<X509Certificate> oldTrusted = new ArrayList<>(DeploymentContext.publicApplicationPackage().trustedCertificates()); - X509Certificate oldCert = tester.jobs().run(id).get().testerCertificate().get(); + X509Certificate oldCert = tester.jobs().run(id).testerCertificate().get(); oldTrusted.add(oldCert); assertEquals(oldTrusted, tester.configServer().application(app.instanceId(), id.type().zone()).get().applicationPackage().trustedCertificates()); tester.configServer().throwOnNextPrepare(null); tester.runner().run(); - assertEquals(succeeded, tester.jobs().run(id).get().stepStatuses().get(Step.deployTester)); - assertEquals(succeeded, tester.jobs().run(id).get().stepStatuses().get(Step.deployReal)); + assertEquals(succeeded, tester.jobs().run(id).stepStatuses().get(Step.deployTester)); + assertEquals(succeeded, tester.jobs().run(id).stepStatuses().get(Step.deployReal)); List<X509Certificate> newTrusted = new ArrayList<>(DeploymentContext.publicApplicationPackage().trustedCertificates()); - X509Certificate newCert = tester.jobs().run(id).get().testerCertificate().get(); + X509Certificate newCert = tester.jobs().run(id).testerCertificate().get(); newTrusted.add(newCert); assertEquals(newTrusted, tester.configServer().application(app.instanceId(), id.type().zone()).get().applicationPackage().trustedCertificates()); assertNotEquals(oldCert, newCert); @@ -536,22 +538,24 @@ public class InternalStepRunnerTest { RunId id = app.startSystemTestTests(); List<X509Certificate> trusted = new ArrayList<>(DeploymentContext.publicApplicationPackage().trustedCertificates()); - trusted.add(tester.jobs().run(id).get().testerCertificate().get()); + trusted.add(tester.jobs().run(id).testerCertificate().get()); assertEquals(trusted, tester.configServer().application(app.instanceId(), id.type().zone()).get().applicationPackage().trustedCertificates()); tester.clock().advance(InternalStepRunner.Timeouts.of(system()).testerCertificate().plus(Duration.ofSeconds(1))); tester.runner().run(); - assertEquals(RunStatus.error, tester.jobs().run(id).get().status()); + assertEquals(RunStatus.error, tester.jobs().run(id).status()); } private void assertTestLogEntries(RunId id, Step step, LogEntry... entries) { assertEquals(List.of(entries), tester.jobs().details(id).get().get(step)); } - private static final String vespaLog = "-1554970337.084804\t17480180-v6-3.ostk.bm2.prod.ne1.yahoo.com\t5549/832\tcontainer\tContainer.com.yahoo.container.jdisc.ConfiguredApplication\tinfo\tSwitching to the latest deployed set of configurations and components. Application switch number: 2\n" + - "3554970337.935104\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstdout\tinfo\tERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)\n" + - "3554970337.947777\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstdout\tinfo\tERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)\n" + - "3554970337.947820\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstdout\tinfo\tERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)\n" + - "3554970337.947845\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstderr\twarning\tjava.lang.NullPointerException\\n\\tat org.apache.felix.framework.BundleRevisionImpl.calculateContentPath(BundleRevisionImpl.java:438)\\n\\tat org.apache.felix.framework.BundleRevisionImpl.initializeContentPath(BundleRevisionImpl.java:371)"; + private static String vespaLog(Instant now) { + return "-1\t17480180-v6-3.ostk.bm2.prod.ne1.yahoo.com\t5549/832\tcontainer\tContainer.com.yahoo.container.jdisc.ConfiguredApplication\tinfo\tSwitching to the latest deployed set of configurations and components. Application switch number: 2\n" + + (now.getEpochSecond() - 4) + "." + now.getNano() / 1000 + "\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstdout\tinfo\tERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)\n" + + (now.getEpochSecond() - 4) + "." + now.getNano() / 1000 + "\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstdout\tinfo\tERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)\n" + + (now.getEpochSecond() - 3) + "." + now.getNano() / 1000 + "\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstdout\tinfo\tERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)\n" + + (now.getEpochSecond() - 3) + "." + now.getNano() / 1000 + "\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstderr\twarning\tjava.lang.NullPointerException\\n\\tat org.apache.felix.framework.BundleRevisionImpl.calculateContentPath(BundleRevisionImpl.java:438)\\n\\tat org.apache.felix.framework.BundleRevisionImpl.initializeContentPath(BundleRevisionImpl.java:371)"; + } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java index 59ee8cc6eae..67583891765 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.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.deployment; +import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.slime.SlimeUtils; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId; import com.yahoo.vespa.hosted.controller.application.Endpoint; import com.yahoo.vespa.hosted.controller.application.EndpointId; import org.junit.Test; @@ -14,6 +16,7 @@ import org.junit.Test; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; +import java.time.Instant; import java.util.List; import java.util.Map; @@ -31,6 +34,9 @@ public class TestConfigSerializerTest { byte[] json = new TestConfigSerializer(SystemName.PublicCd).configJson(instanceId, DeploymentContext.systemTest, true, + Version.fromString("1.2.3"), + RevisionId.forProduction(321), + Instant.ofEpochMilli(222), Map.of(zone, List.of(Endpoint.of(ApplicationId.defaultId()) .target(EndpointId.of("ai"), ClusterSpec.Id.from("qrs"), List.of(new DeploymentId(ApplicationId.defaultId(), 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 650fda3c811..af542521b31 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 @@ -1,10 +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.integration; -import com.yahoo.component.annotation.Inject; import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.component.AbstractComponent; import com.yahoo.component.Version; +import com.yahoo.component.annotation.Inject; import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.SystemName; @@ -15,7 +15,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveService; import com.yahoo.vespa.hosted.controller.api.integration.archive.MockArchiveService; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AccessControlService; import com.yahoo.vespa.hosted.controller.api.integration.athenz.MockAccessControlService; -import com.yahoo.vespa.hosted.controller.api.integration.aws.MockCloudEventFetcher; import com.yahoo.vespa.hosted.controller.api.integration.aws.MockResourceTagger; import com.yahoo.vespa.hosted.controller.api.integration.aws.MockRoleService; import com.yahoo.vespa.hosted.controller.api.integration.aws.ResourceTagger; @@ -38,6 +37,8 @@ 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.GcpSecretStore; +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.stubs.DummyOwnershipIssues; import com.yahoo.vespa.hosted.controller.api.integration.stubs.DummySystemMonitor; @@ -74,7 +75,6 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg private final MemoryEntityService memoryEntityService = new MemoryEntityService(); private final DummySystemMonitor systemMonitor = new DummySystemMonitor(); private final CostReportConsumerMock costReportConsumerMock = new CostReportConsumerMock(); - private final MockCloudEventFetcher mockAwsEventFetcher = new MockCloudEventFetcher(); private final ArtifactRepositoryMock artifactRepositoryMock = new ArtifactRepositoryMock(); private final MockTesterCloud mockTesterCloud; private final ApplicationStoreMock applicationStoreMock = new ApplicationStoreMock(); @@ -176,11 +176,6 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg } @Override - public MockCloudEventFetcher eventFetcherService() { - return mockAwsEventFetcher; - } - - @Override public ArtifactRepositoryMock artifactRepository() { return artifactRepositoryMock; } @@ -295,4 +290,6 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg public RoleMaintainerMock roleMaintainerMock() { return roleMaintainer; } + + public GcpSecretStore gcpSecretStore() { return new NoopGcpSecretStore(); } } 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 5571f957e83..3535417c586 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 @@ -9,6 +9,7 @@ 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; import org.junit.Test; @@ -33,24 +34,24 @@ public class ArchiveAccessMaintainerTest { 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); + var tenant2 = createTenantWithAccessRole(tester, "tenant2", tenant2role); 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(); - assertNull(archiveService.authorizedIamRolesForBucket.get(testBucket)); - assertNull(archiveService.authorizedIamRolesForKey.get(testBucket.keyArn())); + + assertEquals(0, archiveService.authorizeAccessByTenantName.size()); MockMetric metric = new MockMetric(); new ArchiveAccessMaintainer(tester.controller(), metric, Duration.ofMinutes(10)).maintain(); - assertEquals(Map.of(tenant1, tenant1role), archiveService.authorizedIamRolesForBucket.get(testBucket)); - assertEquals(Set.of(tenant1role), archiveService.authorizedIamRolesForKey.get(testBucket.keyArn())); + assertEquals(new ArchiveAccess().withAWSRole(tenant1role), archiveService.authorizeAccessByTenantName.get(tenant1)); + assertEquals(new ArchiveAccess().withAWSRole(tenant2role), archiveService.authorizeAccessByTenantName.get(tenant2)); var expected = Map.of("archive.bucketCount", tester.controller().zoneRegistry().zonesIncludingSystem().all().ids().stream() .collect(Collectors.toMap( - zone -> Map.of("zone", zone.value()), + zone -> Map.of("zone", zone.value(), "cloud", "default"), zone -> zone.equals(testZone) ? 1d : 0d))); assertEquals(expected, metric.metrics()); @@ -59,7 +60,7 @@ public class ArchiveAccessMaintainerTest { 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)); + lockedTenant = lockedTenant.withArchiveAccess(new ArchiveAccess().withAWSRole(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 c1d9c03819d..d208657c1c4 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 @@ -40,27 +40,24 @@ public class ArchiveUriUpdaterTest { var application = tester.newDeploymentContext(tenant1.value(), "app1", "instance1"); ZoneId zone = ZoneId.from("prod", "aws-us-east-1c"); - // Initially we should not set any archive URIs as the archive service does not return any + // Initially we should only is the bucket for hosted-vespa tenant updater.maintain(); - assertArchiveUris(Map.of(), zone); - // but the controller zone is always present - assertArchiveUris(Map.of(TenantName.from("hosted-vespa"), "s3://bucketName/hosted-vespa/"), - ZoneId.from("prod", "controller")); + 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")); // Archive service now has URI for tenant1, but tenant1 is not deployed in zone setBucketNameInService(Map.of(tenant1, "uri-1"), zone); - setBucketNameInService(Map.of(tenantInfra, "uri-3"), zone); updater.maintain(); - assertArchiveUris(Map.of(), zone); + assertArchiveUris(Map.of(TenantName.from("hosted-vespa"), "s3://bucketName/hosted-vespa/"), zone); deploy(application, zone); updater.maintain(); - assertArchiveUris(Map.of(tenant1, "s3://uri-1/tenant1/", tenantInfra, "s3://uri-3/hosted-vespa/"), zone); + assertArchiveUris(Map.of(tenant1, "s3://uri-1/tenant1/", tenantInfra, "s3://bucketName/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, "s3://uri-1/tenant1/", tenantInfra, "s3://uri-3/hosted-vespa/"), zone); + assertArchiveUris(Map.of(tenant1, "s3://uri-1/tenant1/", tenantInfra, "s3://bucketName/hosted-vespa/"), zone); } private void assertArchiveUris(Map<TenantName, String> expectedUris, ZoneId zone) { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudEventTrackerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudEventTrackerTest.java deleted file mode 100644 index 6ccd307f0d9..00000000000 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudEventTrackerTest.java +++ /dev/null @@ -1,145 +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.maintenance; - -import com.yahoo.config.provision.HostName; -import com.yahoo.config.provision.NodeType; -import com.yahoo.config.provision.zone.ZoneId; -import com.yahoo.vespa.hosted.controller.ControllerTester; -import com.yahoo.vespa.hosted.controller.api.integration.aws.CloudEvent; -import com.yahoo.vespa.hosted.controller.api.integration.aws.MockCloudEventFetcher; -import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node; -import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter; -import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock; -import org.junit.Test; - -import java.time.Duration; -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import static org.junit.Assert.assertEquals; - -/** - * @author olaa - */ -public class CloudEventTrackerTest { - - private final ControllerTester tester = new ControllerTester(); - private final ZoneApiMock unsupportedZone = createZone("prod.zone3", "region-1", "other"); - private final ZoneApiMock zone1 = createZone("prod.zone1", "region-1", "aws"); - private final ZoneApiMock zone2 = createZone("prod.zone2", "region-2", "aws"); - - /** - * Test scenario: Consider three zones, two of which are supported - * - * We want to test the following: - * 1. Unsupported zone is completely ignored - * 2. Hosts affected by cloud event are deprovisioned - */ - @Test - public void maintain() { - setUpZones(); - CloudEventTracker cloudEventTracker = new CloudEventTracker(tester.controller(), Duration.ofMinutes(15)); - assertEquals(Set.of("host1.com", "host2.com", "host3.com"), hostsNotDeprovisioning(unsupportedZone.getId())); - assertEquals(Set.of("host1.com", "host2.com", "host3.com"), hostsNotDeprovisioning(zone1.getId())); - assertEquals(Set.of("host4.com", "host5.com", "confighost.com"), hostsNotDeprovisioning(zone2.getId())); - - mockEvents(); - cloudEventTracker.maintain(); - assertEquals(Set.of("host1.com", "host2.com", "host3.com"), hostsNotDeprovisioning(unsupportedZone.getId())); - assertEquals(Set.of("host3.com"), hostsNotDeprovisioning(zone1.getId())); - assertEquals(Set.of("host4.com"), hostsNotDeprovisioning(zone2.getId())); - } - - private void mockEvents() { - MockCloudEventFetcher eventFetcher = (MockCloudEventFetcher) tester.controller().serviceRegistry().eventFetcherService(); - - Date date = new Date(); - CloudEvent event1 = new CloudEvent("event 1", - "instance code", - "description", - date, - date, - date, - "region-1", - Set.of("host1", "host2")); - - CloudEvent event2 = new CloudEvent("event 2", - "instance code", - "description", - date, - date, - date, - "region-2", - Set.of("host5", "confighost")); - - eventFetcher.addEvent("region-1", event1); - eventFetcher.addEvent("region-2", event2); - } - - private void setUpZones() { - tester.zoneRegistry().setZones( - unsupportedZone, - zone1, - zone2); - - tester.configServer().nodeRepository().putNodes( - unsupportedZone.getId(), - createNodesWithHostnames( - "host1.com", - "host2.com", - "host3.com" - ) - ); - tester.configServer().nodeRepository().putNodes( - zone1.getId(), - createNodesWithHostnames( - "host1.com", - "host2.com", - "host3.com" - ) - ); - tester.configServer().nodeRepository().putNodes( - zone2.getId(), - createNodesWithHostnames( - "host4.com", - "host5.com" - ) - ); - tester.configServer().nodeRepository().putNodes( - zone2.getId(), - List.of(createNode("confighost.com", NodeType.confighost)) - ); - } - - private List<Node> createNodesWithHostnames(String... hostnames) { - return Arrays.stream(hostnames) - .map(hostname -> createNode(hostname, NodeType.host)) - .collect(Collectors.toUnmodifiableList()); - } - - private Node createNode(String hostname, NodeType nodeType) { - return Node.builder() - .hostname(HostName.of(hostname)) - .type(nodeType) - .build(); - } - - private Set<String> hostsNotDeprovisioning(ZoneId zoneId) { - return tester.configServer().nodeRepository().list(zoneId, NodeFilter.all()) - .stream() - .filter(node -> !node.wantToDeprovision()) - .map(node -> node.hostname().value()) - .collect(Collectors.toSet()); - } - - private ZoneApiMock createZone(String zoneId, String cloudNativeRegionName, String cloud) { - return ZoneApiMock.newBuilder().withId(zoneId) - .withCloudNativeRegionName(cloudNativeRegionName) - .withCloud(cloud) - .build(); - } - -} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java index 2e34c6511a7..df30b6b57ee 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java @@ -38,7 +38,7 @@ public class CloudTrialExpirerTest { @Test public void tombstone_inactive_none() { - registerTenant("none-tenant", "none", Duration.ofDays(28).plusMillis(1)); + registerTenant("none-tenant", "none", Duration.ofDays(365).plusMillis(1)); expirer.maintain(); assertEquals(Tenant.Type.deleted, tester.controller().tenants().get(TenantName.from("none-tenant"), true).get().type()); } @@ -75,7 +75,7 @@ public class CloudTrialExpirerTest { @Test public void delete_tenants_with_applications_with_no_deployments() { - registerTenant("with-apps", "trial", Duration.ofDays(30)); + registerTenant("with-apps", "trial", Duration.ofDays(366)); tester.createApplication("with-apps", "app1", "instance1"); expirer.maintain(); assertPlan("with-apps", "none"); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java index 5fd1e8347ef..b9ced334e5a 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java @@ -93,8 +93,7 @@ public class JobRunnerTest { ApplicationId id = appId.defaultInstance(); byte[] testPackageBytes = new byte[0]; jobs.submit(appId, Submission.basic(applicationPackage, testPackageBytes), 2); - - start(jobs, id, systemTest); + start(jobs, id, systemTest); try { start(jobs, id, systemTest); fail("Job is already running, so this should not be allowed!"); @@ -106,12 +105,16 @@ public class JobRunnerTest { assertFalse(jobs.last(id, systemTest).get().hasEnded()); assertTrue(jobs.last(id, stagingTest).get().stepStatuses().values().stream().allMatch(unfinished::equals)); assertFalse(jobs.last(id, stagingTest).get().hasEnded()); - runner.maintain(); + runner.maintain(); phaser.arriveAndAwaitAdvance(); assertTrue(jobs.last(id, systemTest).get().stepStatuses().values().stream().allMatch(succeeded::equals)); - assertTrue(jobs.last(id, stagingTest).get().hasEnded()); assertTrue(jobs.last(id, stagingTest).get().hasFailed()); + + runner.maintain(); + phaser.arriveAndAwaitAdvance(); + assertTrue(jobs.last(id, systemTest).get().hasEnded()); + assertTrue(jobs.last(id, stagingTest).get().hasEnded()); } @Test @@ -165,8 +168,8 @@ public class JobRunnerTest { outcomes.put(endTests, testFailure); runner.maintain(); assertTrue(run.get().hasFailed()); - assertEquals(List.of(copyVespaLogs, deactivateTester), run.get().readySteps()); - assertStepsWithStartTime(run.get(), deployTester, deployReal, installTester, installReal, startTests, endTests, copyVespaLogs, deactivateTester); + assertEquals(List.of(copyVespaLogs), run.get().readySteps()); + assertStepsWithStartTime(run.get(), deployTester, deployReal, installTester, installReal, startTests, endTests, copyVespaLogs); outcomes.put(copyVespaLogs, running); runner.maintain(); @@ -442,8 +445,8 @@ public class JobRunnerTest { @Override public void execute(Runnable command) { phaser.register(); delegate.execute(() -> { - command.run(); - phaser.arriveAndDeregister(); + try { command.run(); } + finally { phaser.arriveAndDeregister(); } }); } }; diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/SystemRoutingPolicyMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/SystemRoutingPolicyMaintainerTest.java deleted file mode 100644 index 8b2bfe8ee95..00000000000 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/SystemRoutingPolicyMaintainerTest.java +++ /dev/null @@ -1,53 +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.maintenance; - -import com.yahoo.config.provision.ClusterSpec; -import com.yahoo.config.provision.HostName; -import com.yahoo.config.provision.zone.ZoneId; -import com.yahoo.vespa.hosted.controller.ControllerTester; -import com.yahoo.vespa.hosted.controller.api.integration.configserver.LoadBalancer; -import com.yahoo.vespa.hosted.controller.api.integration.dns.Record; -import com.yahoo.vespa.hosted.controller.application.SystemApplication; -import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock; -import org.junit.Test; - -import java.time.Duration; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; - -/** - * @author mpolden - */ -public class SystemRoutingPolicyMaintainerTest { - - @Test - public void maintain() { - var tester = new ControllerTester(); - var updater = new SystemRoutingPolicyMaintainer(tester.controller(), Duration.ofDays(1)); - var dispatcher = new NameServiceDispatcher(tester.controller(), Duration.ofSeconds(Integer.MAX_VALUE)); - - var zone = ZoneId.from("prod", "us-west-1"); - tester.zoneRegistry().exclusiveRoutingIn(ZoneApiMock.from(zone)); - tester.configServer().putLoadBalancers(zone, List.of(new LoadBalancer("lb1", - SystemApplication.configServer.id(), - ClusterSpec.Id.from("config"), - Optional.of(HostName.of("lb1.example.com")), - LoadBalancer.State.active, - Optional.of("dns-zone-1")))); - - // Record is created - updater.run(); - dispatcher.run(); - Set<Record> records = tester.nameService().records(); - assertEquals(1, records.size()); - Record record = records.iterator().next(); - assertSame(Record.Type.CNAME, record.type()); - assertEquals("cfg.prod.us-west-1.test.vip", record.name().asString()); - assertEquals("lb1.example.com.", record.data().asString()); - } - -} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java index 75dbebe96ff..5666f8bafd8 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java @@ -20,6 +20,7 @@ import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext; import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock; import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo; import com.yahoo.vespa.hosted.controller.tenant.TenantContacts; @@ -60,7 +61,7 @@ public class NotificationsDbTest { List.of(TenantContacts.Audience.NOTIFICATIONS), email)))), List.of(), - Optional.empty()); + new ArchiveAccess()); private static final List<Notification> notifications = List.of( notification(1001, Type.deployment, Level.error, NotificationSource.from(tenant), "tenant msg"), notification(1101, Type.applicationPackage, Level.warning, NotificationSource.from(TenantAndApplicationId.from(tenant.value(), "app1")), "app msg"), diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotifierTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotifierTest.java new file mode 100644 index 00000000000..8bf0e584892 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotifierTest.java @@ -0,0 +1,87 @@ +package com.yahoo.vespa.hosted.controller.notification; + +import com.google.common.collect.ImmutableBiMap; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.flags.Flags; +import com.yahoo.vespa.flags.InMemoryFlagSource; +import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer; +import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock; +import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; +import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; +import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo; +import com.yahoo.vespa.hosted.controller.tenant.TenantContacts; +import com.yahoo.vespa.hosted.controller.tenant.TenantInfo; +import org.junit.Before; +import org.junit.Test; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; + +public class NotifierTest { + private static final TenantName tenant = TenantName.from("tenant1"); + private static final String email = "user1@example.com"; + + private static final CloudTenant cloudTenant = new CloudTenant(tenant, + Instant.now(), + LastLoginInfo.EMPTY, + Optional.empty(), + ImmutableBiMap.of(), + TenantInfo.empty() + .withContacts(new TenantContacts( + List.of(new TenantContacts.EmailContact( + List.of(TenantContacts.Audience.NOTIFICATIONS), + email)))), + List.of(), + new ArchiveAccess()); + + + MockCuratorDb curatorDb = new MockCuratorDb(SystemName.Public); + + @Before + public void init() { + curatorDb.writeTenant(cloudTenant); + } + + @Test + public void dispatch() { + var mailer = new MockMailer(); + var flagSource = new InMemoryFlagSource().withBooleanFlag(Flags.NOTIFICATION_DISPATCH_FLAG.id(), true); + var notifier = new Notifier(curatorDb, new ZoneRegistryMock(SystemName.cd), mailer, flagSource); + + var notification = new Notification(Instant.now(), Notification.Type.testPackage, Notification.Level.warning, + NotificationSource.from(ApplicationId.from(tenant, ApplicationName.defaultName(), InstanceName.defaultName())), + List.of("test package has production tests, but no production tests are declared in deployment.xml", + "see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa")); + notifier.dispatch(notification); + assertEquals(1, mailer.inbox(email).size()); + var mail = mailer.inbox(email).get(0); + + assertEquals("[WARNING] Test package Vespa Notification for tenant1.default.default", mail.subject()); + assertEquals("There are problems with tests for default.default<br>\n" + + "<ul>\n" + + "<li>test package has production tests, but no production tests are declared in deployment.xml</li><br>\n" + + "<li>see <a href=\"https://docs.vespa.ai/en/testing.html\">https://docs.vespa.ai/en/testing.html</a> for details on how to write system tests for Vespa</li></ul>\n" + + "<br>\n" + + "<a href=\"https://dashboard.tld/tenant1/default\">Vespa Console</a>", + mail.htmlMessage().get()); + } + + @Test + public void linkify() { + var data = Map.of( + "Hello. https://example.com/foo/bar.html is a nice place.", "Hello. <a href=\"https://example.com/foo/bar.html\">https://example.com/foo/bar.html</a> is a nice place.", + "No url.", "No url."); + data.forEach((input, expected) -> assertEquals(expected, Notifier.linkify(input))); + } +}
\ No newline at end of file diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializerTest.java index fd0ea50e50b..6a01a70eb98 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializerTest.java @@ -25,7 +25,6 @@ import java.time.Instant; import java.util.List; import java.util.Optional; -import static com.yahoo.config.provision.SystemName.main; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.aborted; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running; import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.failed; @@ -132,6 +131,7 @@ public class RunSerializerTest { assertEquals(run.end(), phoenix.end()); assertEquals(run.status(), phoenix.status()); assertEquals(run.lastTestLogEntry(), phoenix.lastTestLogEntry()); + assertEquals(run.lastVespaLogTimestamp(), phoenix.lastVespaLogTimestamp()); assertEquals(run.noNodesDownSince(), phoenix.noNodesDownSince()); assertEquals(run.testerCertificate(), phoenix.testerCertificate()); assertEquals(run.versions(), phoenix.versions()); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java index e0d14f19f21..a9e633a78d6 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java @@ -6,12 +6,14 @@ import com.yahoo.config.provision.TenantName; import com.yahoo.security.KeyUtils; import com.yahoo.slime.Cursor; import com.yahoo.slime.Slime; +import com.yahoo.slime.SlimeUtils; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.hosted.controller.api.identifiers.Property; import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact; import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore; import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant; @@ -101,7 +103,7 @@ public class TenantSerializerTest { otherPublicKey, new SimplePrincipal("jane")), TenantInfo.empty(), List.of(), - Optional.empty() + new ArchiveAccess() ); CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant)); assertEquals(tenant.name(), serialized.name()); @@ -123,13 +125,61 @@ public class TenantSerializerTest { new TenantSecretStore("ss1", "123", "role1"), new TenantSecretStore("ss2", "124", "role2") ), - Optional.of("arn:aws:iam::123456789012:role/my-role") + new ArchiveAccess().withAWSRole("arn:aws:iam::123456789012:role/my-role") ); CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant)); assertEquals(tenant.info(), serialized.info()); assertEquals(tenant.tenantSecretStores(), serialized.tenantSecretStores()); } + @Test + public void cloud_tenant_with_old_archive_access_serialization() { + var json = "{\n" + + " \"name\": \"elderly-lady\",\n" + + " \"type\": \"cloud\",\n" + + " \"createdAt\": 1234,\n" + + " \"lastLoginInfo\": {\n" + + " \"user\": 123,\n" + + " \"developer\": 456\n" + + " },\n" + + " \"creator\": \"foobar-user\",\n" + + " \"pemDeveloperKeys\": [\n" + + " {\n" + + " \"key\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\\n-----END PUBLIC KEY-----\\n\",\n" + + " \"user\": \"joe\"\n" + + " },\n" + + " {\n" + + " \"key\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\\n-----END PUBLIC KEY-----\\n\",\n" + + " \"user\": \"jane\"\n" + + " }\n" + + " ],\n" + + " \"billingInfo\": {\n" + + " \"customerId\": \"customer\",\n" + + " \"productCode\": \"Vespa\"\n" + + " },\n" + + " \"archiveAccessRole\": \"arn:aws:iam::123456789012:role/my-role\"\n" + + "}"; + var tenant = (CloudTenant) serializer.tenantFrom(SlimeUtils.jsonToSlime(json)); + assertEquals("arn:aws:iam::123456789012:role/my-role", tenant.archiveAccess().awsRole().get()); + assertFalse(tenant.archiveAccess().gcpMember().isPresent()); + } + + @Test + public void cloud_tenant_with_archive_access() { + CloudTenant tenant = new CloudTenant(TenantName.from("elderly-lady"), + Instant.ofEpochMilli(1234L), + lastLoginInfo(123L, 456L, null), + Optional.of(new SimplePrincipal("foobar-user")), + ImmutableBiMap.of(publicKey, new SimplePrincipal("joe"), + otherPublicKey, new SimplePrincipal("jane")), + TenantInfo.empty(), + List.of(), + new ArchiveAccess().withAWSRole("arn:aws:iam::123456789012:role/my-role").withGCPMember("user:foo@example.com") + ); + CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant)); + assertEquals(serialized.archiveAccess().awsRole().get(), "arn:aws:iam::123456789012:role/my-role"); + assertEquals(serialized.archiveAccess().gcpMember().get(), "user:foo@example.com"); + } @Test public void cloud_tenant_with_tenant_info_partial() { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponseTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponseTest.java index 32fe8ddecff..845d007c154 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponseTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponseTest.java @@ -24,7 +24,7 @@ public class ProxyResponseTest { Map.of(), null, List.of(URI.create("http://example.com")), Path.parse("configserver")); ProxyResponse proxyResponse = new ProxyResponse( request, - "response link is http://configserver:1234/bla/bla/", + "response link is http://configserver:4443/bla/bla/", 200, URI.create("http://configserver:1234"), "application/json"); @@ -42,7 +42,7 @@ public class ProxyResponseTest { Map.of(), null, List.of(URI.create("http://example.com")), Path.parse("configserver")); ProxyResponse proxyResponse = new ProxyResponse( request, - "response link is http://configserver:1234/bla/bla/", + "response link is http://configserver:4443/bla/bla/", 200, URI.create("http://configserver:1234"), "application/json"); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ApplicationRequestToDiscFilterRequestWrapper.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ApplicationRequestToDiscFilterRequestWrapper.java index 5f580b6f6b3..52fd7393c4d 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ApplicationRequestToDiscFilterRequestWrapper.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ApplicationRequestToDiscFilterRequestWrapper.java @@ -62,12 +62,6 @@ public class ApplicationRequestToDiscFilterRequestWrapper extends DiscFilterRequ } @Override - @Deprecated - public void setUri(URI uri) { - throw new UnsupportedOperationException(); - } - - @Override public String getParameter(String name) { throw new UnsupportedOperationException(); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java index 5368cc73480..26dd6335ab8 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java @@ -7,6 +7,7 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.TenantName; +import com.yahoo.restapi.RestApiException; import com.yahoo.vespa.flags.InMemoryFlagSource; import com.yahoo.vespa.flags.PermanentFlags; import com.yahoo.vespa.hosted.controller.ControllerTester; @@ -29,7 +30,6 @@ import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import org.junit.Before; import org.junit.Test; -import javax.ws.rs.ForbiddenException; import java.io.File; import java.util.Collections; import java.util.Optional; @@ -80,6 +80,52 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest { } @Test + public void tenant_info_profile() { + var request = request("/application/v4/tenant/scoober/info/profile", GET) + .roles(Set.of(Role.reader(tenantName))); + tester.assertResponse(request, "{}", 200); + + var updateRequest = request("/application/v4/tenant/scoober/info/profile", PUT) + .data("{\"contact\":{\"name\":\"Some Name\",\"email\":\"foo@example.com\"},\"tenant\":{\"company\":\"Scoober, Inc.\",\"website\":\"https://example.com/\"}}") + .roles(Set.of(Role.administrator(tenantName))); + tester.assertResponse(updateRequest, "{\"message\":\"Tenant info updated\"}", 200); + + tester.assertResponse(request, "{\"contact\":{\"name\":\"Some Name\",\"email\":\"foo@example.com\"},\"tenant\":{\"company\":\"\",\"website\":\"https://example.com/\"}}", 200); + } + + @Test + public void tenant_info_billing() { + var request = request("/application/v4/tenant/scoober/info/billing", GET) + .roles(Set.of(Role.reader(tenantName))); + tester.assertResponse(request, "{}", 200); + + var fullAddress = "{\"addressLines\":\"addressLines\",\"postalCodeOrZip\":\"postalCodeOrZip\",\"city\":\"city\",\"stateRegionProvince\":\"stateRegionProvince\",\"country\":\"country\"}"; + var fullBillingContact = "{\"contact\":{\"name\":\"name\",\"email\":\"foo@example\",\"phone\":\"phone\"},\"address\":" + fullAddress + "}"; + + var updateRequest = request("/application/v4/tenant/scoober/info/billing", PUT) + .data(fullBillingContact) + .roles(Set.of(Role.administrator(tenantName))); + tester.assertResponse(updateRequest, "{\"message\":\"Tenant info updated\"}", 200); + + tester.assertResponse(request, "{\"contact\":{\"name\":\"name\",\"email\":\"foo@example\",\"phone\":\"phone\"},\"address\":{\"addressLines\":\"addressLines\",\"postalCodeOrZip\":\"postalCodeOrZip\",\"city\":\"city\",\"stateRegionProvince\":\"stateRegionProvince\",\"country\":\"country\"}}", 200); + } + + @Test + public void tenant_info_contacts() { + var request = request("/application/v4/tenant/scoober/info/contacts", GET) + .roles(Set.of(Role.reader(tenantName))); + tester.assertResponse(request, "{\"contacts\":[]}", 200); + + + var fullContacts = "{\"contacts\":[{\"audiences\":[\"tenant\"],\"email\":\"contact1@example.com\"},{\"audiences\":[\"notifications\"],\"email\":\"contact2@example.com\"},{\"audiences\":[\"tenant\",\"notifications\"],\"email\":\"contact3@example.com\"}]}"; + var updateRequest = request("/application/v4/tenant/scoober/info/contacts", PUT) + .data(fullContacts) + .roles(Set.of(Role.administrator(tenantName))); + tester.assertResponse(updateRequest, "{\"message\":\"Tenant info updated\"}", 200); + tester.assertResponse(request, fullContacts, 200); + } + + @Test public void tenant_info_workflow() { var infoRequest = request("/application/v4/tenant/scoober/info", GET) @@ -221,7 +267,7 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest { try { tester.controller().tenants().create(tenantSpec("tenant2"), credentials("administrator")); fail("Should not be allowed to create tenant that exceed trial limit"); - } catch (ForbiddenException e) { + } catch (RestApiException.Forbidden e) { assertEquals("Too many tenants with trial plans, please contact the Vespa support team", e.getMessage()); } } @@ -315,9 +361,40 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest { .data("{\"role\":\"dummy\"}").roles(Role.administrator(tenantName)), "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Invalid archive access role 'dummy': Must match expected pattern: 'arn:aws:iam::\\\\d{12}:.+'\"}", 400); + tester.assertResponse(request("/application/v4/tenant/scoober/archive-access/aws", PUT) + .data("{\"role\":\"arn:aws:iam::123456789012:role/my-role\"}").roles(Role.administrator(tenantName)), + "{\"message\":\"AWS archive access role set to 'arn:aws:iam::123456789012:role/my-role' for tenant scoober.\"}", 200); + tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)), + (response) -> assertTrue(response.getBodyAsString().contains("\"archiveAccessRole\":\"arn:aws:iam::123456789012:role/my-role\"")), + 200); + tester.assertResponse(request("/application/v4/tenant/scoober/archive-access/aws", DELETE).roles(Role.administrator(tenantName)), + "{\"message\":\"AWS archive access role removed for tenant scoober.\"}", 200); + tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)), + (response) -> assertFalse(response.getBodyAsString().contains("\"archiveAccessRole\":\"arn:aws:iam::123456789012:role/my-role\"")), + 200); + + tester.assertResponse(request("/application/v4/tenant/scoober/archive-access/gcp", PUT) + .data("{\"member\":\"user:test@example.com\"}").roles(Role.administrator(tenantName)), + "{\"message\":\"GCP archive access member set to 'user:test@example.com' for tenant scoober.\"}", 200); + tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)), + (response) -> assertTrue(response.getBodyAsString().contains("\"gcpMember\":\"user:test@example.com\"")), + 200); + tester.assertResponse(request("/application/v4/tenant/scoober/archive-access/gcp", DELETE).roles(Role.administrator(tenantName)), + "{\"message\":\"GCP archive access member removed for tenant scoober.\"}", 200); + tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)), + (response) -> assertFalse(response.getBodyAsString().contains("\"gcpMember\":\"user:test@example.com\"")), + 200); + + tester.assertResponse(request("/application/v4/tenant/scoober/archive-access", PUT) + .data("{\"role\":\"arn:aws:iam::123456789012:role/my-role\"}").roles(Role.administrator(tenantName)), + "{\"message\":\"AWS archive access role set to 'arn:aws:iam::123456789012:role/my-role' for tenant scoober.\"}", 200); + tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)), + (response) -> assertTrue(response.getBodyAsString().contains("\"archiveAccessRole\":\"arn:aws:iam::123456789012:role/my-role\"")), + 200); + tester.assertResponse(request("/application/v4/tenant/scoober/archive-access", PUT) .data("{\"role\":\"arn:aws:iam::123456789012:role/my-role\"}").roles(Role.administrator(tenantName)), - "{\"message\":\"Archive access role set to 'arn:aws:iam::123456789012:role/my-role' for tenant scoober.\"}", 200); + "{\"message\":\"AWS archive access role set to 'arn:aws:iam::123456789012:role/my-role' for tenant scoober.\"}", 200); tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)), (response) -> assertTrue(response.getBodyAsString().contains("\"archiveAccessRole\":\"arn:aws:iam::123456789012:role/my-role\"")), 200); @@ -327,7 +404,7 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest { new File("deployment-cloud.json")); tester.assertResponse(request("/application/v4/tenant/scoober/archive-access", DELETE).roles(Role.administrator(tenantName)), - "{\"message\":\"Archive access role removed for tenant scoober.\"}", 200); + "{\"message\":\"AWS archive access role removed for tenant scoober.\"}", 200); tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)), (response) -> assertFalse(response.getBodyAsString().contains("archiveAccessRole")), 200); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java index 6bfbb044944..d9f0f010104 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java @@ -305,7 +305,7 @@ public class ApplicationApiTest extends ControllerContainerTest { tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/myuser/job/dev-us-east-1/diff/1", GET).userIdentity(HOSTED_VESPA_OPERATOR), (response) -> assertTrue(response.getBodyAsString(), - response.getBodyAsString().contains("--- search-definitions/test.sd\n" + + response.getBodyAsString().contains("--- schemas/test.sd\n" + "@@ -1,0 +1,1 @@\n" + "+ search test { }\n")), 200); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config-dev.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config-dev.json index 3a5e6dc5dc3..ac0f2b9f740 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config-dev.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config-dev.json @@ -3,6 +3,9 @@ "zone": "dev.us-east-1", "system": "main", "isCI": false, + "platform": "6.1.0", + "revision": 1, + "deployedAt": 1600000000000, "endpoints": { "dev.us-east-1": [ "https://my-user.application1.tenant1.us-east-1.dev.vespa.oath.cloud/" diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config.json index 0a9236655ba..671c34cb2c0 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config.json @@ -3,6 +3,9 @@ "zone": "prod.us-central-1", "system": "main", "isCI": false, + "platform": "6.1.0", + "revision": 1, + "deployedAt": 1600000000000, "endpoints": { "prod.us-central-1": [ "https://application1.tenant1.us-central-1.vespa.oath.cloud/" diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java index b58e5294f66..c9e86849ed8 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerTest.java @@ -79,6 +79,13 @@ public class BillingApiHandlerTest extends ControllerContainerCloudTest { } @Test + public void list_plans() { + var listPlansRequest = request("/billing/v1/plans", GET) + .roles(Role.hostedAccountant()); + tester.assertResponse(listPlansRequest, "{\"plans\":[{\"id\":\"trial\",\"name\":\"Free Trial - for testing purposes\"},{\"id\":\"paid\",\"name\":\"Paid Plan - for testing purposes\"},{\"id\":\"none\",\"name\":\"None Plan - for testing purposes\"}]}"); + } + + @Test public void setting_and_deleting_instrument() { assertTrue(billingController.getDefaultInstrument(tenant).isEmpty()); 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 8b2e5578ae0..be28d88abaa 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 @@ -22,9 +22,6 @@ "name": "ChangeRequestMaintainer" }, { - "name": "CloudEventTracker" - }, - { "name": "CloudTrialExpirer" }, { @@ -88,9 +85,6 @@ "name": "RetriggerMaintainer" }, { - "name": "SystemRoutingPolicyMaintainer" - }, - { "name": "SystemUpgrader" }, { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiTest.java index 8db6bdf9a4a..279bd289c00 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiTest.java @@ -26,8 +26,7 @@ public class BadgeApiTest extends ControllerContainerTest { public void testBadgeApi() throws IOException { ContainerTester tester = new ContainerTester(container, responseFiles); var application = new DeploymentTester(new ControllerTester(tester)).newDeploymentContext("tenant", "application", "default"); - ApplicationPackage applicationPackage = new ApplicationPackageBuilder().systemTest() - .parallel("us-west-1", "aws-us-east-1a") + ApplicationPackage applicationPackage = new ApplicationPackageBuilder().parallel("us-west-1", "aws-us-east-1a") .test("us-west-1") .region("ap-southeast-1") .test("ap-southeast-1") @@ -59,6 +58,8 @@ public class BadgeApiTest extends ControllerContainerTest { Files.readString(Paths.get(responseFiles + "overview.svg")), 200); tester.assertResponse(authenticatedRequest("http://localhost:8080/badge/v1/tenant/application/default/production-us-west-1?historyLength=0"), Files.readString(Paths.get(responseFiles + "single-running.svg")), 200); + tester.assertResponse(authenticatedRequest("http://localhost:8080/badge/v1/tenant/application/default/system-test"), + Files.readString(Paths.get(responseFiles + "running-test.svg")), 200); tester.assertResponse(authenticatedRequest("http://localhost:8080/badge/v1/tenant/application/default/production-us-west-1?historyLength=32"), Files.readString(Paths.get(responseFiles + "history.svg")), 200); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/overview.svg b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/overview.svg index a0005ed6d76..46e4acaace6 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/overview.svg +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/overview.svg @@ -1,4 +1,4 @@ -<svg xmlns='http://www.w3.org/2000/svg' width='763.7809900000001' height='20' role='img' aria-label='Deployment Status'> +<svg xmlns='http://www.w3.org/2000/svg' width='689.25265' height='20' role='img' aria-label='Deployment Status'> <title>Deployment Status</title> <linearGradient id='light' x2='0' y2='100%'> <stop offset='0' stop-color='#fff' stop-opacity='.5'/> @@ -46,32 +46,29 @@ <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' /> </linearGradient> <clipPath id='rounded'> - <rect width='763.7809900000001' height='20' rx='3' fill='#fff'/> + <rect width='689.25265' height='20' rx='3' fill='#fff'/> </clipPath> <g clip-path='url(#rounded)'> - <rect x='757.7809900000001' rx='3' width='8' height='20' fill='url(#shadow)'/> - <rect x='725.59036' rx='3' width='38.19063' height='20' fill='url(#run-on-success)'/> - <polygon points='635.8470950000001 0 635.8470950000001 20 734.59036 20 742.59036 0' fill='#00f844'/> - <rect x='635.8470950000001' rx='3' width='131.74345499999998' height='20' fill='url(#shade)'/> - <rect x='635.8470950000001' rx='3' width='8' height='20' fill='url(#shadow)'/> - <rect x='603.656465' rx='3' width='38.19063' height='20' fill='#bf103c'/> - <polygon points='486.981225 0 486.981225 20 612.656465 20 620.656465 0' fill='#00f844'/> - <rect x='486.981225' rx='3' width='158.67543' height='20' fill='url(#shade)'/> - <rect x='486.981225' rx='3' width='8' height='20' fill='url(#shadow)'/> - <rect x='348.865175' rx='3' width='144.11604999999997' height='20' fill='url(#run-on-success)'/> - <rect x='358.865175' rx='3' width='134.11604999999997' height='20' fill='url(#shade)'/> - <rect x='358.865175' rx='3' width='8' height='20' fill='url(#shadow)'/> - <rect x='326.674545' rx='3' width='38.19063' height='20' fill='#00f844'/> - <polygon points='237.71563000000003 0 237.71563000000003 20 335.674545 20 343.674545 0' fill='url(#run-on-failure)'/> - <rect x='237.71563000000003' rx='3' width='130.959105' height='20' fill='url(#shade)'/> - <rect x='237.71563000000003' rx='3' width='8' height='20' fill='url(#shadow)'/> - <rect x='153.18729000000002' rx='3' width='90.52834000000001' height='20' fill='url(#run-on-warning)'/> - <rect x='163.18729000000002' rx='3' width='80.52834000000001' height='20' fill='url(#shade)'/> + <rect x='683.25265' rx='3' width='8' height='20' fill='url(#shadow)'/> + <rect x='651.06202' rx='3' width='38.19063' height='20' fill='url(#run-on-success)'/> + <polygon points='561.318755 0 561.318755 20 660.06202 20 668.06202 0' fill='#00f844'/> + <rect x='561.318755' rx='3' width='131.74345499999998' height='20' fill='url(#shade)'/> + <rect x='561.318755' rx='3' width='8' height='20' fill='url(#shadow)'/> + <rect x='529.128125' rx='3' width='38.19063' height='20' fill='#bf103c'/> + <polygon points='412.452885 0 412.452885 20 538.128125 20 546.128125 0' fill='#00f844'/> + <rect x='412.452885' rx='3' width='158.67543' height='20' fill='url(#shade)'/> + <rect x='412.452885' rx='3' width='8' height='20' fill='url(#shadow)'/> + <rect x='274.336835' rx='3' width='144.11604999999997' height='20' fill='url(#run-on-success)'/> + <rect x='284.336835' rx='3' width='134.11604999999997' height='20' fill='url(#shade)'/> + <rect x='284.336835' rx='3' width='8' height='20' fill='url(#shadow)'/> + <rect x='252.146205' rx='3' width='38.19063' height='20' fill='#00f844'/> + <polygon points='163.18729000000002 0 163.18729000000002 20 261.146205 20 269.146205 0' fill='url(#run-on-failure)'/> + <rect x='163.18729000000002' rx='3' width='130.959105' height='20' fill='url(#shade)'/> <rect width='169.18729000000002' height='20' fill='#404040'/> <rect x='-6.0' rx='3' width='175.18729000000002' height='20' fill='url(#shade)'/> <rect width='2' height='20' fill='url(#left-light)'/> - <rect x='761.7809900000001' width='2' height='20' fill='url(#right-shadow)'/> - <rect width='763.7809900000001' height='20' fill='url(#light)'/> + <rect x='687.25265' width='2' height='20' fill='url(#right-shadow)'/> + <rect width='689.25265' height='20' fill='url(#light)'/> </g> <g fill='#fff' text-anchor='middle' font-family='Verdana,Geneva,DejaVu Sans,sans-serif' text-rendering='geometricPrecision' font-size='11'> <svg x='6.5' y='3.0' width='16.0' height='16.0' viewBox='0 0 150 150'> @@ -96,29 +93,27 @@ </svg> <text font-size='11' x='96.09364500000001' y='15' fill='#000' fill-opacity='.4' textLength='135.18729000000002'>tenant.application.default</text> <text font-size='11' x='95.59364500000001' y='14' fill='#fff' textLength='135.18729000000002'>tenant.application.default</text> - <text font-size='11' x='206.95146000000003' y='15' fill='#000' fill-opacity='.4' textLength='62.52834000000001'>system-test</text> - <text font-size='11' x='206.45146000000003' y='14' fill='#fff' textLength='62.52834000000001'>system-test</text> - <text font-size='11' x='276.60659250000003' y='15' fill='#000' fill-opacity='.4' textLength='52.781925'>us-west-1</text> - <text font-size='11' x='276.10659250000003' y='14' fill='#fff' textLength='52.781925'>us-west-1</text> - <text font-size='9' x='323.08605000000006' y='15' fill='#000' fill-opacity='.4' textLength='28.176989999999996'>deploy</text> - <text font-size='9' x='322.58605000000006' y='14' fill='#fff' textLength='28.176989999999996'>deploy</text> - <text font-size='9' x='351.26986000000005' y='15' fill='#000' fill-opacity='.4' textLength='16.190630000000002'>test</text> - <text font-size='9' x='350.76986000000005' y='14' fill='#fff' textLength='16.190630000000002'>test</text> - <text font-size='11' x='412.334705' y='15' fill='#000' fill-opacity='.4' textLength='81.93905999999998'>aws-us-east-1a</text> - <text font-size='11' x='411.834705' y='14' fill='#fff' textLength='81.93905999999998'>aws-us-east-1a</text> - <text font-size='9' x='473.39273000000003' y='15' fill='#000' fill-opacity='.4' textLength='28.176989999999996'>deploy</text> - <text font-size='9' x='472.89273000000003' y='14' fill='#fff' textLength='28.176989999999996'>deploy</text> - <text font-size='11' x='539.73035' y='15' fill='#000' fill-opacity='.4' textLength='80.49825'>ap-southeast-1</text> - <text font-size='11' x='539.23035' y='14' fill='#fff' textLength='80.49825'>ap-southeast-1</text> - <text font-size='9' x='600.06797' y='15' fill='#000' fill-opacity='.4' textLength='28.176989999999996'>deploy</text> - <text font-size='9' x='599.56797' y='14' fill='#fff' textLength='28.176989999999996'>deploy</text> - <text font-size='9' x='628.25178' y='15' fill='#000' fill-opacity='.4' textLength='16.190630000000002'>test</text> - <text font-size='9' x='627.75178' y='14' fill='#fff' textLength='16.190630000000002'>test</text> - <text font-size='11' x='675.1302325' y='15' fill='#000' fill-opacity='.4' textLength='53.566275'>eu-west-1</text> - <text font-size='11' x='674.6302325' y='14' fill='#fff' textLength='53.566275'>eu-west-1</text> - <text font-size='9' x='722.0018650000001' y='15' fill='#000' fill-opacity='.4' textLength='28.176989999999996'>deploy</text> - <text font-size='9' x='721.5018650000001' y='14' fill='#fff' textLength='28.176989999999996'>deploy</text> - <text font-size='9' x='750.1856750000001' y='15' fill='#000' fill-opacity='.4' textLength='16.190630000000002'>test</text> - <text font-size='9' x='749.6856750000001' y='14' fill='#fff' textLength='16.190630000000002'>test</text> + <text font-size='11' x='202.07825250000002' y='15' fill='#000' fill-opacity='.4' textLength='52.781925'>us-west-1</text> + <text font-size='11' x='201.57825250000002' y='14' fill='#fff' textLength='52.781925'>us-west-1</text> + <text font-size='9' x='248.55771000000001' y='15' fill='#000' fill-opacity='.4' textLength='28.176989999999996'>deploy</text> + <text font-size='9' x='248.05771000000001' y='14' fill='#fff' textLength='28.176989999999996'>deploy</text> + <text font-size='9' x='276.74152000000004' y='15' fill='#000' fill-opacity='.4' textLength='16.190630000000002'>test</text> + <text font-size='9' x='276.24152000000004' y='14' fill='#fff' textLength='16.190630000000002'>test</text> + <text font-size='11' x='337.806365' y='15' fill='#000' fill-opacity='.4' textLength='81.93905999999998'>aws-us-east-1a</text> + <text font-size='11' x='337.306365' y='14' fill='#fff' textLength='81.93905999999998'>aws-us-east-1a</text> + <text font-size='9' x='398.86439' y='15' fill='#000' fill-opacity='.4' textLength='28.176989999999996'>deploy</text> + <text font-size='9' x='398.36439' y='14' fill='#fff' textLength='28.176989999999996'>deploy</text> + <text font-size='11' x='465.20201' y='15' fill='#000' fill-opacity='.4' textLength='80.49825'>ap-southeast-1</text> + <text font-size='11' x='464.70201' y='14' fill='#fff' textLength='80.49825'>ap-southeast-1</text> + <text font-size='9' x='525.53963' y='15' fill='#000' fill-opacity='.4' textLength='28.176989999999996'>deploy</text> + <text font-size='9' x='525.03963' y='14' fill='#fff' textLength='28.176989999999996'>deploy</text> + <text font-size='9' x='553.72344' y='15' fill='#000' fill-opacity='.4' textLength='16.190630000000002'>test</text> + <text font-size='9' x='553.22344' y='14' fill='#fff' textLength='16.190630000000002'>test</text> + <text font-size='11' x='600.6018925' y='15' fill='#000' fill-opacity='.4' textLength='53.566275'>eu-west-1</text> + <text font-size='11' x='600.1018925' y='14' fill='#fff' textLength='53.566275'>eu-west-1</text> + <text font-size='9' x='647.473525' y='15' fill='#000' fill-opacity='.4' textLength='28.176989999999996'>deploy</text> + <text font-size='9' x='646.973525' y='14' fill='#fff' textLength='28.176989999999996'>deploy</text> + <text font-size='9' x='675.657335' y='15' fill='#000' fill-opacity='.4' textLength='16.190630000000002'>test</text> + <text font-size='9' x='675.157335' y='14' fill='#fff' textLength='16.190630000000002'>test</text> </g> </svg> diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/running-test.svg b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/running-test.svg new file mode 100644 index 00000000000..9463c01e8ad --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/running-test.svg @@ -0,0 +1,93 @@ +<svg xmlns='http://www.w3.org/2000/svg' width='300.38627894289516' height='20' role='img' aria-label='Deployment Status'> + <title>Deployment Status</title> + <linearGradient id='light' x2='0' y2='100%'> + <stop offset='0' stop-color='#fff' stop-opacity='.5'/> + <stop offset='.1' stop-color='#fff' stop-opacity='.15'/> + <stop offset='.9' stop-color='#000' stop-opacity='.15'/> + <stop offset='1' stop-color='#000' stop-opacity='.5'/> + </linearGradient> + <linearGradient id='left-light' x2='100%' y2='0'> + <stop offset='0' stop-color='#fff' stop-opacity='.3'/> + <stop offset='.5' stop-color='#fff' stop-opacity='.1'/> + <stop offset='1' stop-color='#fff' stop-opacity='.0'/> + </linearGradient> + <linearGradient id='right-shadow' x2='100%' y2='0'> + <stop offset='0' stop-color='#000' stop-opacity='.0'/> + <stop offset='.5' stop-color='#000' stop-opacity='.1'/> + <stop offset='1' stop-color='#000' stop-opacity='.3'/> + </linearGradient> + <linearGradient id='shadow' x2='100%' y2='0'> + <stop offset='0' stop-color='#222' stop-opacity='.3'/> + <stop offset='.625' stop-color='#555' stop-opacity='.3'/> + <stop offset='.9' stop-color='#555' stop-opacity='.05'/> + <stop offset='1' stop-color='#555' stop-opacity='.0'/> + </linearGradient> + <linearGradient id='shade' x2='100%' y2='0'> + <stop offset='0' stop-color='#000' stop-opacity='.20'/> + <stop offset='0.05' stop-color='#000' stop-opacity='.10'/> + <stop offset='1' stop-color='#000' stop-opacity='.0'/> + </linearGradient> + <linearGradient id='run-on-failure' x1='40%' x2='80%' y2='0%'> + <stop offset='0' stop-color='#ab83ff' /> + <stop offset='1' stop-color='#bf103c' /> + <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' /> + <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' /> + </linearGradient> + <linearGradient id='run-on-warning' x1='40%' x2='80%' y2='0%'> + <stop offset='0' stop-color='#ab83ff' /> + <stop offset='1' stop-color='#bd890b' /> + <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' /> + <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' /> + </linearGradient> + <linearGradient id='run-on-success' x1='40%' x2='80%' y2='0%'> + <stop offset='0' stop-color='#ab83ff' /> + <stop offset='1' stop-color='#00f844' /> + <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' /> + <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' /> + </linearGradient> + <clipPath id='rounded'> + <rect width='300.38627894289516' height='20' rx='3' fill='#fff'/> + </clipPath> + <g clip-path='url(#rounded)'> + <rect x='297.7936599677133' rx='3' width='8' height='20' fill='url(#shadow)'/> + <rect x='267.7546449838567' rx='3' width='36.03901498385664' height='20' fill='#00f844'/> + <rect x='267.7546449838567' rx='3' width='36.03901498385664' height='20' fill='url(#shade)'/> + <rect x='271.5979829411765' rx='3' width='8' height='20' fill='url(#shadow)'/> + <rect x='237.71563000000003' rx='3' width='39.88235294117647' height='20' fill='#00f844'/> + <rect x='237.71563000000003' rx='3' width='39.88235294117647' height='20' fill='url(#shade)'/> + <rect x='237.71563000000003' rx='3' width='8' height='20' fill='url(#shadow)'/> + <rect x='163.18729000000002' rx='3' width='80.52834000000001' height='20' fill='url(#run-on-warning)'/> + <rect x='163.18729000000002' rx='3' width='80.52834000000001' height='20' fill='url(#shade)'/> + <rect width='169.18729000000002' height='20' fill='#404040'/> + <rect x='-6.0' rx='3' width='175.18729000000002' height='20' fill='url(#shade)'/> + <rect width='2' height='20' fill='url(#left-light)'/> + <rect x='298.38627894289516' width='2' height='20' fill='url(#right-shadow)'/> + <rect width='300.38627894289516' height='20' fill='url(#light)'/> + </g> + <g fill='#fff' text-anchor='middle' font-family='Verdana,Geneva,DejaVu Sans,sans-serif' text-rendering='geometricPrecision' font-size='11'> + <svg x='6.5' y='3.0' width='16.0' height='16.0' viewBox='0 0 150 150'> + <polygon fill='#402a14' fill-opacity='0.5' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/> + <polygon fill='#402a14' fill-opacity='0.5' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/> + <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/> + <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/> + </svg> + <svg x='6.0' y='2.0' width='16.0' height='16.0' viewBox='0 0 150 150'> + <linearGradient id='yellow-shaded' x1='91.17' y1='44.83' x2='136.24' y2='73.4' gradientUnits='userSpaceOnUse'> + <stop offset='0.01' stop-color='#c6783e'/> + <stop offset='0.54' stop-color='#ff9750'/> + </linearGradient> + <linearGradient id='blue-shaded' x1='60.71' y1='104.56' x2='-15.54' y2='63' gradientUnits='userSpaceOnUse'> + <stop offset='0' stop-color='#005a8e'/> + <stop offset='0.54' stop-color='#1a7db6'/> + </linearGradient> + <polygon fill='#ff9d4b' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/> + <polygon fill='url(#yellow-shaded)' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/> + <polygon fill='#1a7db6' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/> + <polygon fill='url(#blue-shaded)' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/> + </svg> + <text font-size='11' x='96.09364500000001' y='15' fill='#000' fill-opacity='.4' textLength='135.18729000000002'>tenant.application.default</text> + <text font-size='11' x='95.59364500000001' y='14' fill='#fff' textLength='135.18729000000002'>tenant.application.default</text> + <text font-size='11' x='206.95146000000003' y='15' fill='#000' fill-opacity='.4' textLength='62.52834000000001'>system-test</text> + <text font-size='11' x='206.45146000000003' y='14' fill='#fff' textLength='62.52834000000001'>system-test</text> + </g> +</svg> diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java index 15c7dbf73ab..9024d7c8e7e 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java @@ -16,6 +16,7 @@ import com.yahoo.vespa.hosted.controller.api.role.SecurityContext; import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.restapi.ApplicationRequestToDiscFilterRequestWrapper; +import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo; import com.yahoo.vespa.hosted.controller.tenant.TenantInfo; @@ -76,7 +77,7 @@ public class SignatureFilterTest { ImmutableBiMap.of(), TenantInfo.empty(), List.of(), - Optional.empty())); + new ArchiveAccess())); tester.curator().writeApplication(new Application(appId, tester.clock().instant())); } @@ -122,7 +123,7 @@ public class SignatureFilterTest { ImmutableBiMap.of(publicKey, () -> "user"), TenantInfo.empty(), List.of(), - Optional.empty())); + new ArchiveAccess())); verifySecurityContext(requestOf(signer.signed(request.copy(), Method.POST, () -> new ByteArrayInputStream(hiBytes)), hiBytes), new SecurityContext(new SimplePrincipal("user"), Set.of(Role.reader(id.tenant()), diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiTest.java index 5c210616cb1..15e7a804143 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiTest.java @@ -95,14 +95,18 @@ public class OsApiTest extends ControllerContainerTest { assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"7.5.1\", \"cloud\": \"cloud1\", \"force\": true, \"upgradeBudget\": \"PT0S\"}", Request.Method.PATCH), "{\"message\":\"Set target OS version for cloud 'cloud1' to 7.5.1 with upgrade budget PT0S\"}", 200); + // Clear target for a given cloud + assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": null, \"cloud\": \"cloud2\"}", Request.Method.PATCH), + "{\"message\":\"Cleared target OS version for cloud 'cloud2'\"}", 200); + // Error: Missing fields assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"7.6\"}", Request.Method.PATCH), - "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Fields 'version', 'cloud' and 'upgradeBudget' are required\"}", 400); + "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Field 'cloud' is required\"}", 400); assertResponse(new Request("http://localhost:8080/os/v1/", "{\"cloud\": \"cloud1\"}", Request.Method.PATCH), - "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Fields 'version', 'cloud' and 'upgradeBudget' are required\"}", 400); + "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Field 'version' is required\"}", 400); // Error: Invalid versions - assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": null, \"cloud\": \"cloud1\", \"upgradeBudget\": \"PT0S\"}", Request.Method.PATCH), + assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"0.0.0\", \"cloud\": \"cloud1\", \"upgradeBudget\": \"PT0S\"}", Request.Method.PATCH), "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Invalid version '0.0.0'\"}", 400); assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"foo\", \"cloud\": \"cloud1\", \"upgradeBudget\": \"PT0S\"}", Request.Method.PATCH), "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Invalid version 'foo': For input string: \\\"foo\\\"\"}", 400); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json index 3237e99783d..f980f9231f3 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json @@ -24,6 +24,7 @@ "budgetUsed": 0.0, "clusterSize": 5 }, + "archiveAccess": { }, "applications": [ { "tenant": "my-tenant", diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-secrets.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-secrets.json index 0cc8ba2cd9e..1152033791b 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-secrets.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-secrets.json @@ -32,6 +32,7 @@ "budgetUsed": 0.0, "clusterSize": 5 }, + "archiveAccess": { }, "applications": [ { "tenant": "my-tenant", diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json index 3153c6e218a..631346181a1 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json @@ -15,6 +15,7 @@ "budgetUsed": 0.0, "clusterSize": 5 }, + "archiveAccess": { }, "applications": [ ], "metaData": { "createdAtMillis": 1600000000000 diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java index 303230b91ad..c0fb9b3d8c7 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java @@ -25,14 +25,12 @@ import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName; import com.yahoo.vespa.hosted.controller.application.Endpoint; import com.yahoo.vespa.hosted.controller.application.EndpointId; import com.yahoo.vespa.hosted.controller.application.EndpointList; -import com.yahoo.vespa.hosted.controller.application.SystemApplication; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; 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 com.yahoo.vespa.hosted.controller.integration.ZoneApiMock; -import com.yahoo.vespa.hosted.controller.maintenance.NameServiceDispatcher; import com.yahoo.vespa.hosted.rotation.config.RotationsConfig; import org.junit.Test; @@ -693,22 +691,6 @@ public class RoutingPoliciesTest { } @Test - public void config_server_routing_policy() { - var tester = new RoutingPoliciesTester(); - var app = SystemApplication.configServer.id(); - RecordName name = RecordName.from("cfg.prod.us-west-1.test.vip"); - - tester.provisionLoadBalancers(1, app, zone1); - tester.routingPolicies().refresh(new DeploymentId(app, zone1), DeploymentSpec.empty); - new NameServiceDispatcher(tester.tester.controller(), Duration.ofSeconds(Integer.MAX_VALUE)).run(); - - List<Record> records = tester.controllerTester().nameService().findRecords(Record.Type.CNAME, name); - assertEquals(1, records.size()); - assertEquals(RecordData.from("lb-0--hosted-vespa.zone-config-servers.default--prod.us-west-1."), - records.get(0).data()); - } - - @Test public void application_endpoint_routing_policy() { RoutingPoliciesTester tester = new RoutingPoliciesTester(); TenantAndApplicationId application = TenantAndApplicationId.from("tenant1", "app1"); diff --git a/controller-server/src/test/resources/application-packages/changed-deployment-xml.zip b/controller-server/src/test/resources/application-packages/changed-deployment-xml.zip Binary files differindex e4ec61c50ab..e6482904b22 100644 --- a/controller-server/src/test/resources/application-packages/changed-deployment-xml.zip +++ b/controller-server/src/test/resources/application-packages/changed-deployment-xml.zip diff --git a/controller-server/src/test/resources/application-packages/changed-services-xml.zip b/controller-server/src/test/resources/application-packages/changed-services-xml.zip Binary files differindex daaa1bd9e3c..e11b1ef162e 100644 --- a/controller-server/src/test/resources/application-packages/changed-services-xml.zip +++ b/controller-server/src/test/resources/application-packages/changed-services-xml.zip diff --git a/controller-server/src/test/resources/application-packages/include-absolute.zip b/controller-server/src/test/resources/application-packages/include-absolute.zip Binary files differindex 3b30cd8265a..49c99ff5da9 100644 --- a/controller-server/src/test/resources/application-packages/include-absolute.zip +++ b/controller-server/src/test/resources/application-packages/include-absolute.zip diff --git a/controller-server/src/test/resources/application-packages/include-parent.zip b/controller-server/src/test/resources/application-packages/include-parent.zip Binary files differindex 18c1b0f5e37..8702b512c98 100644 --- a/controller-server/src/test/resources/application-packages/include-parent.zip +++ b/controller-server/src/test/resources/application-packages/include-parent.zip diff --git a/controller-server/src/test/resources/application-packages/original.zip b/controller-server/src/test/resources/application-packages/original.zip Binary files differindex 3963527a6cd..cabac1999c3 100644 --- a/controller-server/src/test/resources/application-packages/original.zip +++ b/controller-server/src/test/resources/application-packages/original.zip diff --git a/controller-server/src/test/resources/application-packages/similar-deployment-xml.zip b/controller-server/src/test/resources/application-packages/similar-deployment-xml.zip Binary files differindex 4075ee08ce3..67c38c344c0 100644 --- a/controller-server/src/test/resources/application-packages/similar-deployment-xml.zip +++ b/controller-server/src/test/resources/application-packages/similar-deployment-xml.zip diff --git a/controller-server/src/test/resources/testConfig.json b/controller-server/src/test/resources/testConfig.json index 5c3d5942001..0ea4b163992 100644 --- a/controller-server/src/test/resources/testConfig.json +++ b/controller-server/src/test/resources/testConfig.json @@ -3,6 +3,9 @@ "zone": "test.us-east-1", "system": "publiccd", "isCI": true, + "platform": "1.2.3", + "revision": 321, + "deployedAt": 222, "endpoints": { "test.us-east-1": [ "https://ai.default.default.global.vespa.oath.cloud/" |