diff options
Diffstat (limited to 'controller-server')
101 files changed, 2990 insertions, 2611 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java index 0a8b5ca8c3d..681c1b4283a 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java @@ -45,7 +45,6 @@ public class Application { private final ValidationOverrides validationOverrides; private final Optional<ApplicationVersion> latestVersion; private final OptionalLong projectId; - private final boolean internal; private final Change change; private final Change outstandingChange; private final Optional<IssueId> deploymentIssueId; @@ -60,14 +59,14 @@ public class Application { public Application(TenantAndApplicationId id, Instant now) { this(id, now, DeploymentSpec.empty, ValidationOverrides.empty, Change.empty(), Change.empty(), Optional.empty(), Optional.empty(), Optional.empty(), OptionalInt.empty(), - new ApplicationMetrics(0, 0), Set.of(), OptionalLong.empty(), false, Optional.empty(), List.of()); + new ApplicationMetrics(0, 0), Set.of(), OptionalLong.empty(), Optional.empty(), List.of()); } // DO NOT USE! For serialization purposes, only. public Application(TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides, Change change, Change outstandingChange, Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner, OptionalInt majorVersion, ApplicationMetrics metrics, Set<PublicKey> deployKeys, OptionalLong projectId, - boolean internal, Optional<ApplicationVersion> latestVersion, Collection<Instance> instances) { + Optional<ApplicationVersion> latestVersion, Collection<Instance> instances) { this.id = Objects.requireNonNull(id, "id cannot be null"); this.createdAt = Objects.requireNonNull(createdAt, "instant of creation cannot be null"); this.deploymentSpec = Objects.requireNonNull(deploymentSpec, "deploymentSpec cannot be null"); @@ -81,7 +80,6 @@ public class Application { this.metrics = Objects.requireNonNull(metrics, "metrics cannot be null"); this.deployKeys = Objects.requireNonNull(deployKeys, "deployKeys cannot be null"); this.projectId = Objects.requireNonNull(projectId, "projectId cannot be null"); - this.internal = internal; this.latestVersion = requireNotUnknown(latestVersion); this.instances = ImmutableSortedMap.copyOf(instances.stream().collect(Collectors.toMap(Instance::name, Function.identity()))); } @@ -102,10 +100,6 @@ public class Application { /** Returns the last submitted version of this application. */ public Optional<ApplicationVersion> latestVersion() { return latestVersion; } - /** Returns whether this application is run on the internal deployment pipeline. */ - // TODO jonmv: Remove, as will be always true. - public boolean internal() { return internal; } - /** * Returns the last deployed validation overrides of this application, * or the empty validation overrides if it has never been deployed diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java index 7c718518129..d28df826957 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java @@ -147,7 +147,7 @@ public class ApplicationController { routingPolicies = new RoutingPolicies(controller); rotationRepository = new RotationRepository(rotationsConfig, this, curator); - deploymentTrigger = new DeploymentTrigger(controller, controller.serviceRegistry().buildService(), clock); + deploymentTrigger = new DeploymentTrigger(controller, clock); provisionApplicationCertificate = Flags.PROVISION_APPLICATION_CERTIFICATE.bindTo(controller.flagSource()); applicationPackageValidator = new ApplicationPackageValidator(controller); @@ -207,6 +207,11 @@ public class ApplicationController { return curator.readApplications(); } + /** Returns the ID of all known applications. */ + public List<TenantAndApplicationId> idList() { + return curator.readApplicationIds(); + } + /** Returns a snapshot of all applications of a tenant */ public List<Application> asList(TenantName tenant) { return curator.readApplications(tenant); @@ -385,7 +390,7 @@ public class ApplicationController { applicationVersion = preferOldestVersion ? triggered.sourceApplication().orElse(triggered.application()) : triggered.application(); - applicationPackage = getApplicationPackage(instanceId, application.get().internal(), applicationVersion); + applicationPackage = getApplicationPackage(instanceId, applicationVersion); applicationPackage = withTesterCertificate(applicationPackage, instanceId, jobType); validateRun(application.get(), instance, zone, platformVersion, applicationVersion); } @@ -397,13 +402,6 @@ public class ApplicationController { applicationCertificate = Optional.empty(); } - // TODO jonmv: REMOVE! This is now irrelevant for non-CD-test deployments and non-unit tests. - if ( ! preferOldestVersion - && ! application.get().internal() - && ! zone.environment().isManuallyDeployed()) { - application = storeWithUpdatedConfig(application, applicationPackage); - } - endpoints = registerEndpointsInDns(applicationPackage.deploymentSpec(), application.get().require(instanceId.instance()), zone); } // Release application lock while doing the deployment, which is a lengthy task. @@ -434,25 +432,8 @@ public class ApplicationController { } /** Fetches the requested application package from the artifact store(s). */ - public ApplicationPackage getApplicationPackage(ApplicationId id, boolean internal, ApplicationVersion version) { - try { - return internal - ? new ApplicationPackage(applicationStore.get(id.tenant(), id.application(), version)) - : new ApplicationPackage(artifactRepository.getApplicationPackage(id, version.id())); - } - catch (RuntimeException e) { // If application has switched deployment pipeline, artifacts stored prior to the switch are in the other artifact store. - try { - log.info("Fetching application package for " + id + " from alternate repository; it is now deployed " - + (internal ? "internally" : "externally") + "\nException was: " + Exceptions.toMessageString(e)); - return internal - ? new ApplicationPackage(artifactRepository.getApplicationPackage(id, version.id())) - : new ApplicationPackage(applicationStore.get(id.tenant(), id.application(), version)); - } - catch (RuntimeException s) { // If this fails, too, the first failure is most likely the relevant one. - e.addSuppressed(s); - throw e; - } - } + public ApplicationPackage getApplicationPackage(ApplicationId id, ApplicationVersion version) { + return new ApplicationPackage(applicationStore.get(id.tenant(), id.application(), version)); } /** Stores the deployment spec and validation overrides from the application package, and runs cleanup. */ @@ -800,6 +781,7 @@ public class ApplicationController { }); }); curator.writeApplication(application.without(instanceId.instance()).get()); + controller.jobController().collectGarbage(); log.info("Deleted " + instanceId); }); @@ -945,50 +927,61 @@ public class ApplicationController { public void verifyApplicationIdentityConfiguration(TenantName tenantName, ApplicationPackage applicationPackage, Optional<Principal> deployer) { verifyAllowedLaunchAthenzService(applicationPackage.deploymentSpec()); - applicationPackage.deploymentSpec().athenzDomain().ifPresent(identityDomain -> { - Tenant tenant = controller.tenants().require(tenantName); - deployer.filter(AthenzPrincipal.class::isInstance) - .map(AthenzPrincipal.class::cast) - .map(AthenzPrincipal::getIdentity) - .filter(AthenzUser.class::isInstance) - .ifPresentOrElse(user -> { - if ( ! ((AthenzFacade) accessControl).hasTenantAdminAccess(user, new AthenzDomain(identityDomain.value()))) - throw new IllegalArgumentException("User " + user.getFullName() + " is not allowed to launch " + - "services in Athenz domain " + identityDomain.value() + ". " + - "Please reach out to the domain admin."); - }, - () -> { - if (tenant.type() != Tenant.Type.athenz) - throw new IllegalArgumentException("Athenz domain defined in deployment.xml, but no " + - "Athenz domain for tenant " + tenantName.value()); - - AthenzDomain tenantDomain = ((AthenzTenant) tenant).domain(); - if ( ! Objects.equals(tenantDomain.getName(), identityDomain.value())) - throw new IllegalArgumentException("Athenz domain in deployment.xml: [" + identityDomain.value() + "] " + - "must match tenant domain: [" + tenantDomain.getName() + "]"); - }); - }); + Tenant tenant = controller.tenants().require(tenantName); + Stream.concat(applicationPackage.deploymentSpec().athenzDomain().stream(), + applicationPackage.deploymentSpec().instances().stream() + .flatMap(spec -> spec.athenzDomain().stream())) + .distinct() + .forEach(identityDomain -> { + deployer.filter(AthenzPrincipal.class::isInstance) + .map(AthenzPrincipal.class::cast) + .map(AthenzPrincipal::getIdentity) + .filter(AthenzUser.class::isInstance) + .ifPresentOrElse(user -> { + if ( ! ((AthenzFacade) accessControl).hasTenantAdminAccess(user, new AthenzDomain(identityDomain.value()))) + throw new IllegalArgumentException("User " + user.getFullName() + " is not allowed to launch " + + "services in Athenz domain " + identityDomain.value() + ". " + + "Please reach out to the domain admin."); + }, + () -> { + if (tenant.type() != Tenant.Type.athenz) + throw new IllegalArgumentException("Athenz domain defined in deployment.xml, but no " + + "Athenz domain for tenant " + tenantName.value()); + + AthenzDomain tenantDomain = ((AthenzTenant) tenant).domain(); + if ( ! Objects.equals(tenantDomain.getName(), identityDomain.value())) + throw new IllegalArgumentException("Athenz domain in deployment.xml: [" + identityDomain.value() + "] " + + "must match tenant domain: [" + tenantDomain.getName() + "]"); + }); + }); } /* * Verifies that the configured athenz service (if any) can be launched. */ private void verifyAllowedLaunchAthenzService(DeploymentSpec deploymentSpec) { - deploymentSpec.athenzDomain().ifPresent(athenzDomain -> { - controller.zoneRegistry().zones().reachable().ids() - .forEach(zone -> { - AthenzIdentity configServerAthenzIdentity = controller.zoneRegistry().getConfigServerHttpsIdentity(zone); - deploymentSpec.athenzService(zone.environment(), zone.region()) - .map(service -> new AthenzService(athenzDomain.value(), service.value())) - .ifPresent(service -> { - boolean allowedToLaunch = ((AthenzFacade) accessControl).canLaunch(configServerAthenzIdentity, service); - if (!allowedToLaunch) - throw new IllegalArgumentException("Not allowed to launch Athenz service " + service.getFullName()); - }); - }); + controller.zoneRegistry().zones().reachable().ids().forEach(zone -> { + AthenzIdentity configServerAthenzIdentity = controller.zoneRegistry().getConfigServerHttpsIdentity(zone); + deploymentSpec.athenzDomain().ifPresent(domain -> { + deploymentSpec.athenzService().ifPresent(service -> { + verifyAthenzServiceCanBeLaunchedBy(configServerAthenzIdentity, new AthenzService(domain.value(), service.value())); + }); + }); + deploymentSpec.instances().forEach(spec -> { + spec.athenzDomain().ifPresent(domain -> { + spec.athenzService(zone.environment(), zone.region()).ifPresent(service -> { + verifyAthenzServiceCanBeLaunchedBy(configServerAthenzIdentity, new AthenzService(domain.value(), service.value())); + }); + }); + }); }); } + private void verifyAthenzServiceCanBeLaunchedBy(AthenzIdentity configServerAthenzIdentity, AthenzService athenzService) { + if ( ! ((AthenzFacade) accessControl).canLaunch(configServerAthenzIdentity, athenzService)) + throw new IllegalArgumentException("Not allowed to launch Athenz service " + athenzService.getFullName()); + } + /** Returns the latest known version within the given major. */ private Optional<Version> lastCompatibleVersion(int targetMajorVersion) { return controller.versionStatus().versions().stream() 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 dcadd992b32..4f6fe2ac2db 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 @@ -78,19 +78,17 @@ public class Controller extends AbstractComponent implements ApplicationIdSource */ @Inject public Controller(CuratorDb curator, RotationsConfig rotationsConfig, - ZoneRegistry zoneRegistry, AccessControl accessControl, FlagSource flagSource, MavenRepository mavenRepository, ServiceRegistry serviceRegistry) { - this(curator, rotationsConfig, zoneRegistry, + this(curator, rotationsConfig, accessControl, com.yahoo.net.HostName::getLocalhost, flagSource, mavenRepository, serviceRegistry); } public Controller(CuratorDb curator, RotationsConfig rotationsConfig, - ZoneRegistry zoneRegistry, AccessControl accessControl, Supplier<String> hostnameSupplier, FlagSource flagSource, MavenRepository mavenRepository, @@ -98,8 +96,8 @@ public class Controller extends AbstractComponent implements ApplicationIdSource this.hostnameSupplier = Objects.requireNonNull(hostnameSupplier, "HostnameSupplier cannot be null"); this.curator = Objects.requireNonNull(curator, "Curator cannot be null"); - this.zoneRegistry = Objects.requireNonNull(zoneRegistry, "ZoneRegistry cannot be null"); this.serviceRegistry = Objects.requireNonNull(serviceRegistry, "ServiceRegistry cannot be null"); + this.zoneRegistry = Objects.requireNonNull(serviceRegistry.zoneRegistry(), "ZoneRegistry cannot be null"); this.clock = Objects.requireNonNull(serviceRegistry.clock(), "Clock cannot be null"); this.flagSource = Objects.requireNonNull(flagSource, "FlagSource cannot be null"); this.mavenRepository = Objects.requireNonNull(mavenRepository, "MavenRepository cannot be null"); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java index 46d1d436521..fa81a990c70 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java @@ -45,7 +45,6 @@ public class LockedApplication { private final ApplicationMetrics metrics; private final Set<PublicKey> deployKeys; private final OptionalLong projectId; - private final boolean internal; private final Optional<ApplicationVersion> latestVersion; private final Map<InstanceName, Instance> instances; @@ -60,14 +59,14 @@ public class LockedApplication { application.deploymentSpec(), application.validationOverrides(), application.change(), application.outstandingChange(), application.deploymentIssueId(), application.ownershipIssueId(), application.owner(), application.majorVersion(), application.metrics(), application.deployKeys(), - application.projectId(), application.internal(), application.latestVersion(), application.instances()); + application.projectId(), application.latestVersion(), application.instances()); } private LockedApplication(Lock lock, TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides, Change change, Change outstandingChange, Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner, OptionalInt majorVersion, ApplicationMetrics metrics, Set<PublicKey> deployKeys, - OptionalLong projectId, boolean internal, Optional<ApplicationVersion> latestVersion, + OptionalLong projectId, Optional<ApplicationVersion> latestVersion, Map<InstanceName, Instance> instances) { this.lock = lock; this.id = id; @@ -83,7 +82,6 @@ public class LockedApplication { this.metrics = metrics; this.deployKeys = deployKeys; this.projectId = projectId; - this.internal = internal; this.latestVersion = latestVersion; this.instances = Map.copyOf(instances); } @@ -92,7 +90,7 @@ public class LockedApplication { public Application get() { return new Application(id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, internal, latestVersion, instances.values()); + projectId, latestVersion, instances.values()); } public LockedApplication withNewInstance(InstanceName instance) { @@ -100,7 +98,7 @@ public class LockedApplication { instances.put(instance, new Instance(id.instance(instance))); return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, internal, latestVersion, instances); + projectId, latestVersion, instances); } public LockedApplication with(InstanceName instance, UnaryOperator<Instance> modification) { @@ -108,7 +106,7 @@ public class LockedApplication { instances.put(instance, modification.apply(instances.get(instance))); return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, internal, latestVersion, instances); + projectId, latestVersion, instances); } public LockedApplication without(InstanceName instance) { @@ -116,67 +114,61 @@ public class LockedApplication { instances.remove(instance); return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, internal, latestVersion, instances); + projectId, latestVersion, instances); } public LockedApplication withNewSubmission(ApplicationVersion latestVersion) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, internal, Optional.of(latestVersion), instances); - } - - public LockedApplication withBuiltInternally(boolean builtInternally) { - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, - deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, builtInternally, latestVersion, instances); + projectId, Optional.of(latestVersion), instances); } public LockedApplication withProjectId(OptionalLong projectId) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, internal, latestVersion, instances); + projectId, latestVersion, instances); } public LockedApplication withDeploymentIssueId(IssueId issueId) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, Optional.ofNullable(issueId), ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, internal, latestVersion, instances); + projectId, latestVersion, instances); } public LockedApplication with(DeploymentSpec deploymentSpec) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, internal, latestVersion, instances); + projectId, latestVersion, instances); } public LockedApplication with(ValidationOverrides validationOverrides) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, internal, latestVersion, instances); + projectId, latestVersion, instances); } public LockedApplication withChange(Change change) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, internal, latestVersion, instances); + projectId, latestVersion, instances); } public LockedApplication withOutstandingChange(Change outstandingChange) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, internal, latestVersion, instances); + projectId, latestVersion, instances); } public LockedApplication withOwnershipIssueId(IssueId issueId) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, deploymentIssueId, Optional.of(issueId), owner, majorVersion, metrics, deployKeys, - projectId, internal, latestVersion, instances); + projectId, latestVersion, instances); } public LockedApplication withOwner(User owner) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, deploymentIssueId, ownershipIssueId, Optional.of(owner), majorVersion, metrics, deployKeys, - projectId, internal, latestVersion, instances); + projectId, latestVersion, instances); } /** Set a major version for this, or set to null to remove any major version override */ @@ -184,13 +176,13 @@ public class LockedApplication { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, deploymentIssueId, ownershipIssueId, owner, majorVersion == null ? OptionalInt.empty() : OptionalInt.of(majorVersion), - metrics, deployKeys, projectId, internal, latestVersion, instances); + metrics, deployKeys, projectId, latestVersion, instances); } public LockedApplication with(ApplicationMetrics metrics) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, internal, latestVersion, instances); + projectId, latestVersion, instances); } public LockedApplication withDeployKey(PublicKey pemDeployKey) { @@ -198,7 +190,7 @@ public class LockedApplication { keys.add(pemDeployKey); return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, keys, - projectId, internal, latestVersion, instances); + projectId, latestVersion, instances); } public LockedApplication withoutDeployKey(PublicKey pemDeployKey) { @@ -206,7 +198,7 @@ public class LockedApplication { keys.remove(pemDeployKey); return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, keys, - projectId, internal, latestVersion, instances); + projectId, latestVersion, instances); } @Override diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java index a2487e8a0d1..3fe8e9f52c3 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java @@ -1,8 +1,9 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.application; -import com.google.common.collect.ImmutableList; +import com.yahoo.collections.AbstractFilteringList; import com.yahoo.component.Version; +import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy; import com.yahoo.config.provision.ApplicationId; import com.yahoo.vespa.hosted.controller.Application; @@ -11,11 +12,9 @@ import com.yahoo.vespa.hosted.controller.Instance; import java.time.Instant; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Optional; -import java.util.function.Predicate; import java.util.stream.Collectors; /** @@ -23,166 +22,137 @@ import java.util.stream.Collectors; * * @author jonmv */ -public class ApplicationList { +public class ApplicationList extends AbstractFilteringList<Application, ApplicationList> { - private final List<Application> list; - - private ApplicationList(List<Application> applications) { - this.list = applications; + private ApplicationList(Collection<? extends Application> applications, boolean negate) { + super(applications, negate, ApplicationList::new); } // ----------------------------------- Factories - public static ApplicationList from(Collection<Application> applications) { - return new ApplicationList(List.copyOf(applications)); + public static ApplicationList from(Collection<? extends Application> applications) { + return new ApplicationList(applications, false); } public static ApplicationList from(Collection<ApplicationId> ids, ApplicationController applications) { - return new ApplicationList(ids.stream() - .map(TenantAndApplicationId::from) - .distinct() - .map(applications::requireApplication) - .collect(Collectors.toUnmodifiableList())); + return from(ids.stream() + .map(TenantAndApplicationId::from) + .distinct() + .map(applications::requireApplication) + .collect(Collectors.toUnmodifiableList())); } // ----------------------------------- Accessors - /** Returns the applications in this as an immutable list */ - public List<Application> asList() { return list; } - /** Returns the ids of the applications in this as an immutable list */ - public List<TenantAndApplicationId> idList() { return list.stream().map(Application::id).collect(Collectors.toUnmodifiableList()); } - - public boolean isEmpty() { return list.isEmpty(); } - - public int size() { return list.size(); } + public List<TenantAndApplicationId> idList() { + return mapToList(Application::id); + } // ----------------------------------- Filters /** Returns the subset of applications which are upgrading (to any version), not considering block windows. */ public ApplicationList upgrading() { - return filteredOn(application -> application.change().platform().isPresent()); + return matching(application -> application.change().platform().isPresent()); } /** Returns the subset of applications which are currently upgrading to the given version */ public ApplicationList upgradingTo(Version version) { - return filteredOn(application -> isUpgradingTo(version, application)); + return upgradingTo(List.of(version)); } - /** Returns the subset of applications which are not pinned to a certain Vespa version. */ - public ApplicationList unpinned() { - return filteredOn(application -> ! application.change().isPinned()); + /** Returns the subset of applications which are currently upgrading to the given version */ + public ApplicationList upgradingTo(Collection<Version> versions) { + return matching(application -> versions.stream().anyMatch(version -> isUpgradingTo(version, application))); } - /** Returns the subset of applications which are currently not upgrading to the given version */ - public ApplicationList notUpgradingTo(Version version) { - return notUpgradingTo(Collections.singletonList(version)); + /** Returns the subset of applications which are not pinned to a certain Vespa version. */ + public ApplicationList unpinned() { + return matching(application -> ! application.change().isPinned()); } - public ApplicationList notFailingUpgrade() { - return filteredOn(application -> application.instances().values().stream() + public ApplicationList failingUpgrade() { + return matching(application -> ! application.instances().values().stream() .allMatch(instance -> JobList.from(instance) .failing() .not().failingApplicationChange() .isEmpty())); } - /** Returns the subset of applications which are currently not upgrading to any of the given versions */ - public ApplicationList notUpgradingTo(Collection<Version> versions) { - return filteredOn(application -> versions.stream().noneMatch(version -> isUpgradingTo(version, application))); - } - - /** - * Returns the subset of applications which are currently not upgrading to the given version, - * or returns all if no version is specified - */ - public ApplicationList notUpgradingTo(Optional<Version> version) { - if (version.isEmpty()) return this; - return notUpgradingTo(version.get()); - } - /** Returns the subset of applications which have changes left to deploy; blocked, or deploying */ public ApplicationList withChanges() { - return filteredOn(application -> application.change().hasTargets() || application.outstandingChange().hasTargets()); + return matching(application -> application.change().hasTargets() || application.outstandingChange().hasTargets()); } - /** Returns the subset of applications which are currently not deploying a change */ - public ApplicationList notDeploying() { - return filteredOn(application -> ! application.change().hasTargets()); - } - - /** Returns the subset of applications which currently does not have any failing jobs */ - public ApplicationList notFailing() { - return filteredOn(application -> application.instances().values().stream() - .noneMatch(instance -> instance.deploymentJobs().hasFailures())); + /** Returns the subset of applications which are currently deploying a change */ + public ApplicationList deploying() { + return matching(application -> application.change().hasTargets()); } /** Returns the subset of applications which currently have failing jobs */ public ApplicationList failing() { - return filteredOn(application -> application.instances().values().stream() + return matching(application -> application.instances().values().stream() .anyMatch(instance -> instance.deploymentJobs().hasFailures())); } /** Returns the subset of applications which have been failing an upgrade to the given version since the given instant */ public ApplicationList failingUpgradeToVersionSince(Version version, Instant threshold) { - return filteredOn(application -> application.instances().values().stream() - .anyMatch(instance -> failingUpgradeToVersionSince(instance, version, threshold))); + return matching(application -> application.instances().values().stream() + .anyMatch(instance -> failingUpgradeToVersionSince(instance, version, threshold))); } /** Returns the subset of applications which have been failing an application change since the given instant */ public ApplicationList failingApplicationChangeSince(Instant threshold) { - return filteredOn(application -> application.instances().values().stream() - .anyMatch(instance -> failingApplicationChangeSince(instance, threshold))); + return matching(application -> application.instances().values().stream() + .anyMatch(instance -> failingApplicationChangeSince(instance, threshold))); } - /** Returns the subset of applications which currently does not have any failing jobs on the given version */ - public ApplicationList notFailingOn(Version version) { - return filteredOn(application -> application.instances().values().stream() - .noneMatch(instance -> failingOn(version, instance))); + /** Returns the subset of applications which currently have failing jobs on the given version */ + public ApplicationList failingOn(Version version) { + return matching(application -> application.instances().values().stream() + .anyMatch(instance -> failingOn(version, instance))); } /** Returns the subset of applications which have at least one production deployment */ public ApplicationList withProductionDeployment() { - return filteredOn(application -> application.instances().values().stream() + return matching(application -> application.instances().values().stream() .anyMatch(instance -> instance.productionDeployments().size() > 0)); } /** Returns the subset of applications which started failing on the given version */ public ApplicationList startedFailingOn(Version version) { - return filteredOn(application -> application.instances().values().stream() + return matching(application -> application.instances().values().stream() .anyMatch(instance -> ! JobList.from(instance).firstFailing().on(version).isEmpty())); } /** Returns the subset of applications which has the given upgrade policy */ + // TODO jonmv: Make this instance based when instances are orchestrated, and deployments reported per instance. public ApplicationList with(UpgradePolicy policy) { - return filteredOn(application -> application.deploymentSpec().upgradePolicy() == policy); - } - - /** Returns the subset of applications which does not have the given upgrade policy */ - public ApplicationList without(UpgradePolicy policy) { - return filteredOn(application -> application.deploymentSpec().upgradePolicy() != policy); + return matching(application -> application.deploymentSpec().instances().stream() + .anyMatch(instance -> instance.upgradePolicy() == policy)); } /** Returns the subset of applications which have at least one deployment on a lower version than the given one */ public ApplicationList onLowerVersionThan(Version version) { - return filteredOn(application -> application.instances().values().stream() + return matching(application -> application.instances().values().stream() .flatMap(instance -> instance.productionDeployments().values().stream()) .anyMatch(deployment -> deployment.version().isBefore(version))); } /** Returns the subset of applications which have a project ID */ public ApplicationList withProjectId() { - return filteredOn(application -> application.projectId().isPresent()); + return matching(application -> application.projectId().isPresent()); } /** Returns the subset of applications that are allowed to upgrade at the given time */ public ApplicationList canUpgradeAt(Instant instant) { - return filteredOn(application -> application.deploymentSpec().canUpgradeAt(instant)); + return matching(application -> application.deploymentSpec().instances().stream() + .allMatch(instance -> instance.canUpgradeAt(instant))); } /** Returns the subset of applications that have at least one assigned rotation */ public ApplicationList hasRotation() { - return filteredOn(application -> application.instances().values().stream() + return matching(application -> application.instances().values().stream() .anyMatch(instance -> ! instance.rotations().isEmpty())); } @@ -193,15 +163,14 @@ public class ApplicationList { * @param defaultMajorVersion the default major version to assume for applications not specifying one */ public ApplicationList allowMajorVersion(int targetMajorVersion, int defaultMajorVersion) { - return filteredOn(application -> targetMajorVersion <= application.deploymentSpec().majorVersion() + return matching(application -> targetMajorVersion <= application.deploymentSpec().majorVersion() .orElse(application.majorVersion() .orElse(defaultMajorVersion))); } - /** Returns the first n application in this (or all, if there are less than n). */ - public ApplicationList first(int n) { - if (list.size() < n) return this; - return new ApplicationList(list.subList(0, n)); + /** Returns the subset of application which have submitted a non-empty deployment spec. */ + public ApplicationList withDeploymentSpec() { + return matching(application -> ! DeploymentSpec.empty.equals(application.deploymentSpec())); } // ----------------------------------- Sorting @@ -212,10 +181,8 @@ public class ApplicationList { * Applications without any deployments are ordered first. */ public ApplicationList byIncreasingDeployedVersion() { - return new ApplicationList(list.stream() - .sorted(Comparator.comparing(application -> application.oldestDeployedPlatform() - .orElse(Version.emptyVersion))) - .collect(Collectors.toUnmodifiableList())); + return sortedBy(Comparator.comparing(application -> application.oldestDeployedPlatform() + .orElse(Version.emptyVersion))); } // ----------------------------------- Internal helpers @@ -246,8 +213,4 @@ public class ApplicationList { .isEmpty(); } - private ApplicationList filteredOn(Predicate<Application> condition) { - return new ApplicationList(list.stream().filter(condition).collect(Collectors.toUnmodifiableList())); - } - } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationPackageValidator.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationPackageValidator.java index 5ee269f8448..d7347b46f52 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationPackageValidator.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationPackageValidator.java @@ -50,15 +50,17 @@ public class ApplicationPackageValidator { /** Verify that each of the production zones listed in the deployment spec exist in this system */ private void validateSteps(DeploymentSpec deploymentSpec) { - new DeploymentSteps(deploymentSpec, controller::system).jobs(); - deploymentSpec.instances().stream().flatMap(instance -> instance.zones().stream()) - .filter(zone -> zone.environment() == Environment.prod) - .forEach(zone -> { - if ( ! controller.zoneRegistry().hasZone(ZoneId.from(zone.environment(), - zone.region().orElse(null)))) { - throw new IllegalArgumentException("Zone " + zone + " in deployment spec was not found in this system!"); - } - }); + for (var spec : deploymentSpec.instances()) { + new DeploymentSteps(spec, controller::system).jobs(); + spec.zones().stream() + .filter(zone -> zone.environment() == Environment.prod) + .forEach(zone -> { + if ( ! controller.zoneRegistry().hasZone(ZoneId.from(zone.environment(), + zone.region().orElseThrow()))) { + throw new IllegalArgumentException("Zone " + zone + " in deployment spec was not found in this system!"); + } + }); + } } /** Verify that no single endpoint contains regions in different clouds */ diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java index bc2b18005b6..e126128ce2e 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java @@ -3,10 +3,8 @@ package com.yahoo.vespa.hosted.controller.application; import com.google.common.collect.ImmutableMap; import com.yahoo.config.provision.ApplicationId; -import com.yahoo.vespa.hosted.controller.api.integration.BuildService; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision; import java.util.Collection; import java.util.LinkedHashMap; @@ -105,17 +103,6 @@ public class DeploymentJobs { this.version = version; } - public static JobReport ofComponent(ApplicationId applicationId, long projectId, long buildNumber, - Optional<JobError> jobError, SourceRevision sourceRevision) { - return new JobReport(applicationId, JobType.component, projectId, buildNumber, - jobError, Optional.of(ApplicationVersion.from(sourceRevision, buildNumber))); - } - - public static JobReport ofSubmission(ApplicationId applicationId, long projectId, ApplicationVersion version) { - return new JobReport(applicationId, JobType.component, projectId, version.buildNumber().getAsLong(), - Optional.empty(), Optional.of(version)); - } - public static JobReport ofJob(ApplicationId applicationId, JobType jobType, long buildNumber, Optional<JobError> jobError) { return new JobReport(applicationId, jobType, -1, buildNumber, jobError, Optional.empty()); } @@ -127,7 +114,6 @@ public class DeploymentJobs { public boolean success() { return ! jobError.isPresent(); } public Optional<ApplicationVersion> version() { return version; } public Optional<JobError> jobError() { return jobError; } - public BuildService.BuildJob buildJob() { return BuildService.BuildJob.of(applicationId, projectId, jobType.jobName()); } } 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 83d1b5ef803..9a6daf026c3 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 @@ -145,7 +145,6 @@ public class Endpoint { } private static String instancePart(ApplicationId application, ZoneId zone, String separator) { - if (zone == null) return ""; // Always omit instance for global endpoints if (application.instance().isDefault()) return ""; // Skip "default" return application.instance().value() + separator; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java index c4613db27d1..e12bb5cda7f 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java @@ -1,10 +1,12 @@ // Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.application; +import com.yahoo.collections.AbstractFilteringList; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.SystemName; import com.yahoo.vespa.hosted.controller.application.Endpoint.Port; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.function.Predicate; @@ -17,41 +19,34 @@ import java.util.stream.Stream; * * @author mpolden */ -public class EndpointList { +public class EndpointList extends AbstractFilteringList<Endpoint, EndpointList> { public static final EndpointList EMPTY = new EndpointList(List.of()); - private final List<Endpoint> endpoints; - - private EndpointList(List<Endpoint> endpoints) { + private EndpointList(Collection<? extends Endpoint> endpoints, boolean negate) { + super(endpoints, negate, EndpointList::new); if (endpoints.stream().distinct().count() != endpoints.size()) { throw new IllegalArgumentException("Expected all endpoints to be distinct, got " + endpoints); } - this.endpoints = List.copyOf(endpoints); } - public List<Endpoint> asList() { - return endpoints; + private EndpointList(Collection<? extends Endpoint> endpoints) { + this(endpoints, false); } /** Returns the main endpoint, if any */ public Optional<Endpoint> main() { - return endpoints.stream().filter(Predicate.not(Endpoint::legacy)).findFirst(); + return asList().stream().filter(Predicate.not(Endpoint::legacy)).findFirst(); } /** Returns the subset of endpoints are either legacy or not */ public EndpointList legacy(boolean legacy) { - return of(endpoints.stream().filter(endpoint -> endpoint.legacy() == legacy)); + return matching(endpoint -> endpoint.legacy() == legacy); } /** Returns the subset of endpoints with given scope */ public EndpointList scope(Endpoint.Scope scope) { - return of(endpoints.stream().filter(endpoint -> endpoint.scope() == scope)); - } - - /** Returns the union of this and given endpoints */ - public EndpointList and(EndpointList endpoints) { - return of(Stream.concat(asList().stream(), endpoints.asList().stream())); + return matching(endpoint -> endpoint.scope() == scope); } public static EndpointList of(Stream<Endpoint> endpoints) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java index 5306cc4ae2b..cc6b770a809 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java @@ -43,7 +43,7 @@ public class JobStatus { this.jobError = requireNonNull(jobError, "jobError cannot be null"); // Never say we triggered component because we don't: - this.lastTriggered = type == JobType.component ? Optional.empty() : requireNonNull(lastTriggered, "lastTriggered cannot be null"); + this.lastTriggered = requireNonNull(lastTriggered, "lastTriggered cannot be null"); this.lastCompleted = requireNonNull(lastCompleted, "lastCompleted cannot be null"); this.firstFailing = requireNonNull(firstFailing, "firstFailing cannot be null"); this.lastSuccess = requireNonNull(lastSuccess, "lastSuccess cannot be null"); 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 70c504dd220..8f84845a94b 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 @@ -208,6 +208,10 @@ public class AthenzFacade implements AccessControl { return hasAccess("launch", service.getDomain().getName() + ":service."+service.getName(), principal); } + public boolean hasSystemFlagsDeployAccess(AthenzIdentity identity) { + return hasAccess("deploy", new AthenzResourceName(service.getDomain(), "system-flags").toResourceNameString(), identity); + } + /** * Used when creating tenancies. As there are no tenancy policies at this point, * we cannot use {@link #hasTenantAdminAccess(AthenzIdentity, AthenzDomain)} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java new file mode 100644 index 00000000000..1582bc144f4 --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java @@ -0,0 +1,49 @@ +package com.yahoo.vespa.hosted.controller.deployment; + +import com.yahoo.config.provision.InstanceName; +import com.yahoo.vespa.hosted.controller.Application; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; + +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Status of the deployment jobs of an {@link Application}. + * + * @author jonmv + */ +public class DeploymentStatus { + + private final Application application; + private final Map<JobId, JobStatus> jobs; + + public DeploymentStatus(Application application, Map<JobId, JobStatus> jobs) { + this.application = Objects.requireNonNull(application); + this.jobs = Map.copyOf(jobs); + } + + public Application application() { + return application; + } + + public Map<JobId, JobStatus> jobs() { + return jobs; + } + + public boolean hasFailures() { + return ! JobList.from(jobs.values()) + .failing() + .not().withStatus(RunStatus.outOfCapacity) + .isEmpty(); + } + + public Map<JobType, JobStatus> instanceJobs(InstanceName instance) { + return jobs.entrySet().stream() + .filter(entry -> entry.getKey().application().equals(application.id().instance(instance))) + .collect(Collectors.toUnmodifiableMap(entry -> entry.getKey().type(), + entry -> entry.getValue())); + } + +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentSteps.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentSteps.java index 33db6b95db1..1b722c80ec1 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentSteps.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentSteps.java @@ -1,12 +1,14 @@ // Copyright 2017 Yahoo Holdings. 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.config.application.api.DeploymentInstanceSpec; import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.zone.ZoneId; -import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; +import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.JobStatus; import java.util.Collection; @@ -16,8 +18,8 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.Stream; -import static java.util.Collections.singletonList; import static java.util.Comparator.comparingInt; import static java.util.stream.Collectors.collectingAndThen; @@ -28,19 +30,20 @@ import static java.util.stream.Collectors.collectingAndThen; */ public class DeploymentSteps { - private final DeploymentSpec spec; + private final DeploymentInstanceSpec spec; private final Supplier<SystemName> system; - public DeploymentSteps(DeploymentSpec spec, Supplier<SystemName> system) { + public DeploymentSteps(DeploymentInstanceSpec spec, Supplier<SystemName> system) { this.spec = Objects.requireNonNull(spec, "spec cannot be null"); this.system = Objects.requireNonNull(system, "system cannot be null"); } - /** Returns jobs for this, in the order they are declared */ + /** Returns jobs for this, in the order they should run */ public List<JobType> jobs() { - return spec.steps().stream() - .flatMap(step -> toJobs(step).stream()) - .collect(collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + return Stream.concat(production().isEmpty() ? Stream.of() : Stream.of(JobType.systemTest, JobType.stagingTest), + spec.steps().stream().flatMap(step -> toJobs(step).stream())) + .distinct() + .collect(Collectors.toUnmodifiableList()); } /** Returns job status sorted according to deployment spec */ @@ -48,7 +51,7 @@ public class DeploymentSteps { List<JobType> sortedJobs = jobs(); return jobStatus.stream() .sorted(comparingInt(job -> sortedJobs.indexOf(job.type()))) - .collect(collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + .collect(Collectors.toUnmodifiableList()); } /** Returns deployments sorted according to declared zones */ @@ -56,7 +59,7 @@ public class DeploymentSteps { List<ZoneId> productionZones = spec.zones().stream() .filter(z -> z.region().isPresent()) .map(z -> ZoneId.from(z.environment(), z.region().get())) - .collect(Collectors.toList()); + .collect(Collectors.toUnmodifiableList()); return deployments.stream() .sorted(comparingInt(deployment -> productionZones.indexOf(deployment.zone()))) .collect(collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); @@ -67,34 +70,28 @@ public class DeploymentSteps { return step.zones().stream() .map(this::toJob) .flatMap(Optional::stream) - .collect(collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + .collect(Collectors.toUnmodifiableList()); } - /** Returns test jobs in this */ + /** Returns test jobs to run for this spec */ public List<JobType> testJobs() { - return toJobs(test()); + return jobs().stream().filter(JobType::isTest).collect(Collectors.toUnmodifiableList()); } - /** Returns production jobs in this */ + /** Returns declared production jobs in this */ public List<JobType> productionJobs() { return toJobs(production()); } - /** Returns test steps in this */ - public List<DeploymentSpec.Step> test() { - if (spec.steps().isEmpty()) { - return singletonList(new DeploymentSpec.DeclaredZone(Environment.test)); - } + /** Returns declared production steps in this */ + public List<DeploymentSpec.Step> production() { return spec.steps().stream() - .filter(step -> step.deploysTo(Environment.test) || step.deploysTo(Environment.staging)) - .collect(collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + .filter(step -> ! isTest(step)) + .collect(Collectors.toUnmodifiableList()); } - /** Returns production steps in this */ - public List<DeploymentSpec.Step> production() { - return spec.steps().stream() - .filter(step -> step.deploysTo(Environment.prod) || step.zones().isEmpty()) - .collect(collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + private boolean isTest(DeploymentSpec.Step step) { + return step.deploysTo(Environment.test) || step.deploysTo(Environment.staging); } /** Resolve job from deployment zone */ @@ -106,7 +103,7 @@ public class DeploymentSteps { private List<JobType> toJobs(List<DeploymentSpec.Step> steps) { return steps.stream() .flatMap(step -> toJobs(step).stream()) - .collect(collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + .collect(Collectors.toUnmodifiableList()); } } 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 9573f5d07f5..f1b93c7b3b2 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 @@ -5,7 +5,6 @@ import com.yahoo.config.application.api.DeploymentInstanceSpec; import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.DeploymentSpec.Step; import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.log.LogLevel; import com.yahoo.vespa.hosted.controller.Application; @@ -13,15 +12,13 @@ import com.yahoo.vespa.hosted.controller.ApplicationController; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; -import com.yahoo.vespa.hosted.controller.api.integration.BuildService; -import com.yahoo.vespa.hosted.controller.api.integration.BuildService.JobState; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.application.ApplicationList; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.Deployment; import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobReport; -import com.yahoo.vespa.hosted.controller.application.JobStatus; import com.yahoo.vespa.hosted.controller.application.JobStatus.JobRun; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; @@ -31,7 +28,6 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -44,15 +40,9 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; -import static com.yahoo.vespa.hosted.controller.api.integration.BuildService.BuildJob; -import static com.yahoo.vespa.hosted.controller.api.integration.BuildService.JobState.idle; -import static com.yahoo.vespa.hosted.controller.api.integration.BuildService.JobState.queued; -import static com.yahoo.vespa.hosted.controller.api.integration.BuildService.JobState.running; -import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.component; import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.stagingTest; import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.systemTest; import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; import static java.util.Comparator.comparing; import static java.util.Comparator.naturalOrder; import static java.util.stream.Collectors.groupingBy; @@ -72,31 +62,20 @@ import static java.util.stream.Collectors.toList; */ public class DeploymentTrigger { - /* - * Instance orchestration TODO jonmv. - * Store new production application packages under non-instance path - * Read production packages from non-instance path, with fallback - * Deprecate and redirect some instance qualified paths in application/v4 - * Orchestrate deployment across instances. - */ - public static final Duration maxPause = Duration.ofDays(3); - private final static Logger log = Logger.getLogger(DeploymentTrigger.class.getName()); private final Controller controller; private final Clock clock; - private final BuildService buildService; private final JobController jobs; - public DeploymentTrigger(Controller controller, BuildService buildService, Clock clock) { + public DeploymentTrigger(Controller controller, Clock clock) { this.controller = Objects.requireNonNull(controller, "controller cannot be null"); this.clock = Objects.requireNonNull(clock, "clock cannot be null"); - this.buildService = Objects.requireNonNull(buildService, "buildService cannot be null"); this.jobs = controller.jobController(); } - public DeploymentSteps steps(DeploymentSpec spec) { + public DeploymentSteps steps(DeploymentInstanceSpec spec) { return new DeploymentSteps(spec, controller::system); } @@ -111,10 +90,9 @@ public class DeploymentTrigger { if (acceptNewApplicationVersion(application.get())) { application = application.withChange(application.get().change().with(version)) .withOutstandingChange(Change.empty()); - if (application.get().internal()) - for (Run run : jobs.active(id)) - if ( ! run.id().type().environment().isManuallyDeployed()) - jobs.abort(run.id()); + for (Run run : jobs.active(id)) + if ( ! run.id().type().environment().isManuallyDeployed()) + jobs.abort(run.id()); } else application = application.withOutstandingChange(Change.of(version)); @@ -143,24 +121,17 @@ public class DeploymentTrigger { } applications().lockApplicationOrThrow(TenantAndApplicationId.from(report.applicationId()), application -> { - if (report.jobType() == component) { - if (report.success()) - notifyOfSubmission(application.get().id(), report.version().get(), report.projectId()); - - return; - } - JobRun triggering; - Optional<JobStatus> status = application.get().require(report.applicationId().instance()) - .deploymentJobs().statusOf(report.jobType()); - triggering = status.filter(job -> job.lastTriggered().isPresent() - && job.lastCompleted() - .map(completion -> ! completion.at().isAfter(job.lastTriggered().get().at())) - .orElse(true)) - .orElseThrow(() -> new IllegalStateException("Notified of completion of " + report.jobType().jobName() + " for " + - report.applicationId() + ", but that has not been triggered; last was " + - status.flatMap(job -> job.lastTriggered().map(run -> run.at().toString())) - .orElse("never"))) - .lastTriggered().get(); + var status = application.get().require(report.applicationId().instance()) + .deploymentJobs().statusOf(report.jobType()); + var triggering = status.filter(job -> job.lastTriggered().isPresent() + && job.lastCompleted() + .map(completion -> ! completion.at().isAfter(job.lastTriggered().get().at())) + .orElse(true)) + .orElseThrow(() -> new IllegalStateException("Notified of completion of " + report.jobType().jobName() + " for " + + report.applicationId() + ", but that has not been triggered; last was " + + status.flatMap(job -> job.lastTriggered().map(run -> run.at().toString())) + .orElse("never"))) + .lastTriggered().get(); application = application.with(report.applicationId().instance(), instance -> instance.withJobCompletion(report.jobType(), @@ -170,11 +141,6 @@ public class DeploymentTrigger { }); } - /** Returns a map of jobs that are scheduled to be run, grouped by the job type */ - public Map<JobType, ? extends List<? extends BuildJob>> jobsToRun() { - return computeReadyJobs().stream().collect(groupingBy(Job::jobType)); - } - /** * Finds and triggers jobs that can and should run but are currently not, and returns the number of triggered jobs. * @@ -212,13 +178,10 @@ public class DeploymentTrigger { log.log(LogLevel.DEBUG, String.format("Triggering %s: %s", job, job.triggering)); try { applications().lockApplicationOrThrow(TenantAndApplicationId.from(job.applicationId()), application -> { - if (application.get().internal()) - jobs.start(job.applicationId(), job.jobType, new Versions(job.triggering.platform(), - job.triggering.application(), - job.triggering.sourcePlatform(), - job.triggering.sourceApplication())); - else - buildService.trigger(job); + jobs.start(job.applicationId(), job.jobType, new Versions(job.triggering.platform(), + job.triggering.application(), + job.triggering.sourcePlatform(), + job.triggering.sourceApplication())); applications().store(application.with(job.applicationId().instance(), instance -> instance.withJobTriggering(job.jobType, job.triggering))); @@ -238,19 +201,13 @@ public class DeploymentTrigger { public List<JobType> forceTrigger(ApplicationId applicationId, JobType jobType, String user) { Application application = applications().requireApplication(TenantAndApplicationId.from(applicationId)); Instance instance = application.require(applicationId.instance()); - if (jobType == component) { - if (application.internal()) - throw new IllegalArgumentException(applicationId + " has no component job we can trigger."); - - buildService.trigger(BuildJob.of(applicationId, application.projectId().getAsLong(), jobType.jobName())); - return singletonList(component); - } Versions versions = Versions.from(application.change(), application, deploymentFor(instance, jobType), controller.systemVersion()); String reason = "Job triggered manually by " + user; - return (jobType.isProduction() && ! isTested(instance, versions) - ? testJobs(application.deploymentSpec(), application.change(), instance, versions, reason, clock.instant(), __ -> true).stream() - : Stream.of(deploymentJob(instance, versions, application.change(), jobType, reason, clock.instant()))) + var jobStatus = jobs.deploymentStatus(application).instanceJobs(instance.name()); + return (jobType.isProduction() && ! isTested(jobStatus, versions) + ? testJobs(application.deploymentSpec(), application.change(), instance, jobStatus, versions, reason, clock.instant(), __ -> true).stream() + : Stream.of(deploymentJob(instance, versions, application.change(), jobType, jobStatus.get(jobType), reason, clock.instant()))) .peek(this::trigger) .map(Job::jobType).collect(toList()); } @@ -306,9 +263,8 @@ public class DeploymentTrigger { return controller.applications(); } - private Optional<JobRun> successOn(Instance instance, JobType jobType, Versions versions) { - return instance.deploymentJobs().statusOf(jobType).flatMap(JobStatus::lastSuccess) - .filter(versions::targetsMatch); + private Optional<Run> successOn(JobStatus status, Versions versions) { + return status.lastSuccess().filter(run -> versions.targetsMatch(run.versions())); } private Optional<Deployment> deploymentFor(Instance instance, JobType jobType) { @@ -324,12 +280,13 @@ public class DeploymentTrigger { /** Returns the set of all jobs which have changes to propagate from the upstream steps. */ private List<Job> computeReadyJobs() { return ApplicationList.from(applications().asList()) - .withProjectId() - .withChanges() - .idList().stream() - .map(this::computeReadyJobs) - .flatMap(Collection::stream) - .collect(toList()); + .withProjectId() // Need to keep this, as we have applications with deployment spec that shouldn't be orchestrated. + .withChanges() + .withDeploymentSpec() + .idList().stream() + .map(this::computeReadyJobs) + .flatMap(Collection::stream) + .collect(toList()); } /** @@ -338,40 +295,40 @@ public class DeploymentTrigger { private List<Job> computeReadyJobs(TenantAndApplicationId id) { List<Job> jobs = new ArrayList<>(); applications().getApplication(id).ifPresent(application -> { - Collection<Instance> instances = application.deploymentSpec().equals(DeploymentSpec.empty) - ? application.instances().values() - : application.deploymentSpec().instances().stream() - .flatMap(instance -> application.get(instance.name()).stream()) - .collect(Collectors.toUnmodifiableList()); + Collection<Instance> instances = application.deploymentSpec().instances().stream() + .flatMap(instance -> application.get(instance.name()).stream()) + .collect(Collectors.toUnmodifiableList()); + DeploymentStatus deploymentStatus = this.jobs.deploymentStatus(application); for (Instance instance : instances) { + var jobStatus = deploymentStatus.instanceJobs(instance.name()); Change change = application.change(); - Optional<Instant> completedAt = max(instance.deploymentJobs().statusOf(systemTest) - .<Instant>flatMap(job -> job.lastSuccess().map(JobRun::at)), - instance.deploymentJobs().statusOf(stagingTest) - .<Instant>flatMap(job -> job.lastSuccess().map(JobRun::at))); + Optional<Instant> completedAt = max(Optional.ofNullable(jobStatus.get(systemTest)) + .<Instant>flatMap(job -> job.lastSuccess().map(run -> run.end().get())), + Optional.ofNullable(jobStatus.get(stagingTest)) + .<Instant>flatMap(job -> job.lastSuccess().map(run -> run.end().get()))); String reason = "New change available"; List<Job> testJobs = null; // null means "uninitialised", while empty means "don't run any jobs". - DeploymentSteps steps = steps(application.deploymentSpec()); + DeploymentSteps steps = steps(application.deploymentSpec().requireInstance(instance.name())); if (change.hasTargets()) { for (Step step : steps.production()) { List<JobType> stepJobs = steps.toJobs(step); - List<JobType> remainingJobs = stepJobs.stream().filter(job -> ! isComplete(change, change, instance, job)).collect(toList()); + List<JobType> remainingJobs = stepJobs.stream().filter(job -> ! isComplete(change, change, instance, job, jobStatus.get(job))).collect(toList()); if ( ! remainingJobs.isEmpty()) { // Change is incomplete; trigger remaining jobs if ready, or their test jobs if untested. for (JobType job : remainingJobs) { Versions versions = Versions.from(change, application, deploymentFor(instance, job), controller.systemVersion()); - if (isTested(instance, versions)) { - if (completedAt.isPresent() && canTrigger(job, versions, instance, application.deploymentSpec(), stepJobs)) { - jobs.add(deploymentJob(instance, versions, change, job, reason, completedAt.get())); + if (isTested(jobStatus, versions)) { + if (completedAt.isPresent() && canTrigger(job, jobStatus, versions, instance, application.deploymentSpec(), stepJobs)) { + jobs.add(deploymentJob(instance, versions, change, job, jobStatus.get(job), reason, completedAt.get())); } - if ( ! alreadyTriggered(instance, versions) && testJobs == null) { + if ( ! alreadyTriggered(jobStatus, versions) && testJobs == null) { testJobs = emptyList(); } } else if (testJobs == null) { testJobs = testJobs(application.deploymentSpec(), - change, instance, versions, + change, instance, jobStatus, versions, String.format("Testing deployment for %s (%s)", job.jobName(), versions.toString()), completedAt.orElseGet(clock::instant)); @@ -385,14 +342,14 @@ public class DeploymentTrigger { reason += " after a delay of " + step.delay(); } else { - completedAt = stepJobs.stream().map(job -> instance.deploymentJobs().statusOf(job).get().lastCompleted().get().at()).max(naturalOrder()); + completedAt = stepJobs.stream().map(job -> jobStatus.get(job).lastCompleted().get().end().get()).max(naturalOrder()); reason = "Available change in " + stepJobs.stream().map(JobType::jobName).collect(joining(", ")); } } } } if (testJobs == null) { // If nothing to test, but outstanding commits, test those. - testJobs = testJobs(application.deploymentSpec(), change, instance, + testJobs = testJobs(application.deploymentSpec(), change, instance, jobStatus, Versions.from(application.outstandingChange().onTopOf(change), application, steps.sortedDeployments(instance.productionDeployments().values()).stream().findFirst(), @@ -406,21 +363,21 @@ public class DeploymentTrigger { } /** Returns whether given job should be triggered */ - private boolean canTrigger(JobType job, Versions versions, Instance instance, DeploymentSpec deploymentSpec, List<JobType> parallelJobs) { - if (jobStateOf(instance, job) != idle) return false; + private boolean canTrigger(JobType job, Map<JobType, JobStatus> status, Versions versions, Instance instance, DeploymentSpec deploymentSpec, List<JobType> parallelJobs) { + if (status.get(job).isRunning()) return false; // Are we already running jobs which are not in the set which can run in parallel with this? - if (parallelJobs != null && ! parallelJobs.containsAll(runningProductionJobs(instance))) return false; + if (parallelJobs != null && ! parallelJobs.containsAll(runningProductionJobs(status))) return false; // Are there another suspended deployment such that we shouldn't simultaneously change this? if (job.isProduction() && isSuspendedInAnotherZone(instance, job.zone(controller.system()))) return false; - return triggerAt(clock.instant(), job, versions, instance, deploymentSpec); + return triggerAt(clock.instant(), job, status.get(job), versions, instance, deploymentSpec); } /** Returns whether given job should be triggered */ - private boolean canTrigger(JobType job, Versions versions, Instance instance, DeploymentSpec deploymentSpec) { - return canTrigger(job, versions, instance, deploymentSpec, null); + private boolean canTrigger(JobType job, Map<JobType, JobStatus> status, Versions versions, Instance instance, DeploymentSpec deploymentSpec) { + return canTrigger(job, status, versions, instance, deploymentSpec, null); } private boolean isSuspendedInAnotherZone(Instance instance, ZoneId zone) { @@ -433,24 +390,23 @@ public class DeploymentTrigger { } /** Returns whether the given job can trigger at the given instant */ - public boolean triggerAt(Instant instant, JobType job, Versions versions, Instance instance, DeploymentSpec deploymentSpec) { - Optional<JobStatus> jobStatus = instance.deploymentJobs().statusOf(job); - if (jobStatus.isEmpty()) return true; - if (jobStatus.get().pausedUntil().isPresent() && jobStatus.get().pausedUntil().getAsLong() > clock.instant().toEpochMilli()) return false; - if (jobStatus.get().isSuccess()) return true; // Success - if (jobStatus.get().lastCompleted().isEmpty()) return true; // Never completed - if (jobStatus.get().firstFailing().isEmpty()) return true; // Should not happen as firstFailing should be set for an unsuccessful job - if ( ! versions.targetsMatch(jobStatus.get().lastCompleted().get())) return true; // Always trigger as targets have changed - if (deploymentSpec.upgradePolicy() == DeploymentSpec.UpgradePolicy.canary) return true; // Don't throttle canaries - - Instant firstFailing = jobStatus.get().firstFailing().get().at(); - Instant lastCompleted = jobStatus.get().lastCompleted().get().at(); + public boolean triggerAt(Instant instant, JobType job, JobStatus jobStatus, Versions versions, Instance instance, DeploymentSpec deploymentSpec) { + if (instance.deploymentJobs().statusOf(job).map(status -> status.pausedUntil().orElse(0)).orElse(0L) > clock.millis()) return false; + if (jobStatus.lastTriggered().isEmpty()) return true; + if (jobStatus.isSuccess()) return true; // Success + if (jobStatus.lastCompleted().isEmpty()) return true; // Never completed + if (jobStatus.firstFailing().isEmpty()) return true; // Should not happen as firstFailing should be set for an unsuccessful job + if ( ! versions.targetsMatch(jobStatus.lastCompleted().get().versions())) return true; // Always trigger as targets have changed + if (deploymentSpec.requireInstance(instance.name()).upgradePolicy() == DeploymentSpec.UpgradePolicy.canary) return true; // Don't throttle canaries + + Instant firstFailing = jobStatus.firstFailing().get().end().get(); + Instant lastCompleted = jobStatus.lastCompleted().get().end().get(); // Retry all errors immediately for 1 minute if (firstFailing.isAfter(instant.minus(Duration.ofMinutes(1)))) return true; // Retry out of capacity errors in test environments every minute - if (job.isTest() && jobStatus.get().isOutOfCapacity()) { + if (job.isTest() && jobStatus.isOutOfCapacity()) { return lastCompleted.isBefore(instant.minus(Duration.ofMinutes(1))); } @@ -463,27 +419,12 @@ public class DeploymentTrigger { // ---------- Job state helpers ---------- - private List<JobType> runningProductionJobs(Instance instance) { - return instance.deploymentJobs().jobStatus().keySet().parallelStream() - .filter(JobType::isProduction) - .filter(job -> isRunning(instance, job)) - .collect(toList()); - } - - /** Returns whether the given job is currently running; false if completed since last triggered, asking the build service otherwise. */ - private boolean isRunning(Instance instance, JobType jobType) { - return ! instance.deploymentJobs().statusOf(jobType) - .flatMap(job -> job.lastCompleted().map(run -> run.at().isAfter(job.lastTriggered().get().at()))) - .orElse(false) - && EnumSet.of(running, queued).contains(jobStateOf(instance, jobType)); - } - - private JobState jobStateOf(Instance instance, JobType jobType) { - if (controller.applications().requireApplication(TenantAndApplicationId.from(instance.id())).internal()) { - Optional<Run> run = controller.jobController().last(instance.id(), jobType); - return run.isPresent() && ! run.get().hasEnded() ? JobState.running : JobState.idle; - } - return buildService.stateOf(BuildJob.of(instance.id(), 0, jobType.jobName())); + private List<JobType> runningProductionJobs(Map<JobType, JobStatus> status) { + return status.values().parallelStream() + .filter(job -> job.isRunning()) + .map(job -> job.id().type()) + .filter(JobType::isProduction) + .collect(toList()); } // ---------- Completion logic ---------- @@ -500,17 +441,18 @@ public class DeploymentTrigger { * Additionally, if the application is pinned to a Vespa version, and the given change has a (this) platform, * the deployment for the job must be on the pinned version. */ - public boolean isComplete(Change change, Change fullChange, Instance instance, JobType jobType) { + public boolean isComplete(Change change, Change fullChange, Instance instance, JobType jobType, + JobStatus status) { Optional<Deployment> existingDeployment = deploymentFor(instance, jobType); if ( change.isPinned() && change.platform().isPresent() && ! existingDeployment.map(Deployment::version).equals(change.platform())) return false; - return instance.deploymentJobs().statusOf(jobType).flatMap(JobStatus::lastSuccess) - .map(job -> change.platform().map(job.platform()::equals).orElse(true) - && change.application().map(job.application()::equals).orElse(true)) - .orElse(false) + return status.lastSuccess() + .map(run -> change.platform().map(run.versions().targetPlatform()::equals).orElse(true) + && change.application().map(run.versions().targetApplication()::equals).orElse(true)) + .orElse(false) || jobType.isProduction() && existingDeployment.map(deployment -> ! isUpgrade(change, deployment) && isDowngrade(fullChange, deployment)) .orElse(false); @@ -524,27 +466,28 @@ public class DeploymentTrigger { return change.downgrades(deployment.version()) || change.downgrades(deployment.applicationVersion()); } - private boolean isTested(Instance instance, Versions versions) { - return testedIn(instance, systemTest, versions) - && testedIn(instance, stagingTest, versions) - || alreadyTriggered(instance, versions); + private boolean isTested(Map<JobType, JobStatus> status, Versions versions) { + return testedIn(systemTest, status.get(systemTest), versions) + && testedIn(stagingTest, status.get(stagingTest), versions) + || alreadyTriggered(status, versions); } - public boolean testedIn(Instance instance, JobType testType, Versions versions) { + public boolean testedIn(JobType testType, JobStatus status, Versions versions) { if (testType == systemTest) - return successOn(instance, systemTest, versions).isPresent(); + return successOn(status, versions).isPresent(); if (testType == stagingTest) - return successOn(instance, stagingTest, versions).filter(versions::sourcesMatchIfPresent).isPresent(); + return successOn(status, versions).map(Run::versions).filter(versions::sourcesMatchIfPresent).isPresent(); throw new IllegalArgumentException(testType + " is not a test job!"); } - public boolean alreadyTriggered(Instance instance, Versions versions) { - return instance.deploymentJobs().jobStatus().values().stream() - .filter(job -> job.type().isProduction()) + public boolean alreadyTriggered(Map<JobType, JobStatus> status, Versions versions) { + return status.values().stream() + .filter(job -> job.id().type().isProduction()) .anyMatch(job -> job.lastTriggered() - .filter(versions::targetsMatch) - .filter(versions::sourcesMatchIfPresent) - .isPresent()); + .map(Run::versions) + .filter(versions::targetsMatch) + .filter(versions::sourcesMatchIfPresent) + .isPresent()); } // ---------- Change management o_O ---------- @@ -559,19 +502,22 @@ public class DeploymentTrigger { } private Change remainingChange(Application application) { - DeploymentSteps steps = steps(application.deploymentSpec()); - List<JobType> jobs = steps.production().isEmpty() - ? steps.testJobs() - : steps.productionJobs(); - Change change = application.change(); - for (Instance instance : application.instances().values()) { - if (jobs.stream().allMatch(job -> isComplete(application.change().withoutApplication(), application.change(), instance, job))) - change = change.withoutPlatform(); - - if (jobs.stream().allMatch(job -> isComplete(application.change().withoutPlatform(), application.change(), instance, job))) - change = change.withoutApplication(); - } + if (application.deploymentSpec().instances().stream() + .allMatch(spec -> { + DeploymentSteps steps = new DeploymentSteps(spec, controller::system); + return (steps.productionJobs().isEmpty() ? steps.testJobs() : steps.productionJobs()) + .stream().allMatch(job -> isComplete(application.change().withoutApplication(), application.change(), application.require(spec.name()), job, jobs.jobStatus(new JobId(application.id().instance(spec.name()), job)))); + })) + change = change.withoutPlatform(); + + if (application.deploymentSpec().instances().stream() + .allMatch(spec -> { + DeploymentSteps steps = new DeploymentSteps(spec, controller::system); + return (steps.productionJobs().isEmpty() ? steps.testJobs() : steps.productionJobs()) + .stream().allMatch(job -> isComplete(application.change().withoutPlatform(), application.change(), application.require(spec.name()), job, jobs.jobStatus(new JobId(application.id().instance(spec.name()), job)))); + })) + change = change.withoutApplication(); return change; } @@ -581,44 +527,42 @@ public class DeploymentTrigger { /** * Returns the list of test jobs that should run now, and that need to succeed on the given versions for it to be considered tested. */ - private List<Job> testJobs(DeploymentSpec deploymentSpec, Change change, Instance instance, Versions versions, + private List<Job> testJobs(DeploymentSpec deploymentSpec, Change change, Instance instance, Map<JobType, JobStatus> status, Versions versions, String reason, Instant availableSince) { - return testJobs(deploymentSpec, change, instance, versions, reason, availableSince, - jobType -> canTrigger(jobType, versions, instance, deploymentSpec)); + return testJobs(deploymentSpec, change, instance, status, versions, reason, availableSince, + jobType -> canTrigger(jobType, status, versions, instance, deploymentSpec)); } /** * Returns the list of test jobs that need to succeed on the given versions for it to be considered tested, filtered by the given condition. */ - private List<Job> testJobs(DeploymentSpec deploymentSpec, Change change, Instance instance, Versions versions, + private List<Job> testJobs(DeploymentSpec deploymentSpec, Change change, Instance instance, Map<JobType, JobStatus> status, Versions versions, String reason, Instant availableSince, Predicate<JobType> condition) { List<Job> jobs = new ArrayList<>(); - for (JobType jobType : steps(deploymentSpec).testJobs()) { - Optional<JobRun> completion = successOn(instance, jobType, versions) - .filter(run -> versions.sourcesMatchIfPresent(run) || jobType == systemTest); + for (JobType jobType : new DeploymentSteps(deploymentSpec.requireInstance(instance.name()), controller::system).testJobs()) { // TODO jonmv: Allow cross-instance validation + Optional<Run> completion = successOn(status.get(jobType), versions) + .filter(run -> versions.sourcesMatchIfPresent(run.versions()) || jobType == systemTest); if (completion.isEmpty() && condition.test(jobType)) - jobs.add(deploymentJob(instance, versions, change, jobType, reason, availableSince)); + jobs.add(deploymentJob(instance, versions, change, jobType, status.get(jobType), reason, availableSince)); } return jobs; } - private Job deploymentJob(Instance instance, Versions versions, Change change, JobType jobType, String reason, Instant availableSince) { - boolean isRetry = instance.deploymentJobs().statusOf(jobType) - .map(JobStatus::isOutOfCapacity) - .orElse(false); - if (isRetry) reason += "; retrying on out of capacity"; + private Job deploymentJob(Instance instance, Versions versions, Change change, JobType jobType, JobStatus jobStatus, String reason, Instant availableSince) { + if (jobStatus.isOutOfCapacity()) reason += "; retrying on out of capacity"; - JobRun triggering = JobRun.triggering(versions.targetPlatform(), versions.targetApplication(), - versions.sourcePlatform(), versions.sourceApplication(), - reason, clock.instant()); - return new Job(instance, triggering, jobType, availableSince, isRetry, change.application().isPresent()); + var triggering = JobRun.triggering(versions.targetPlatform(), versions.targetApplication(), + versions.sourcePlatform(), versions.sourceApplication(), + reason, clock.instant()); + return new Job(instance, triggering, jobType, availableSince, jobStatus.isOutOfCapacity(), change.application().isPresent()); } // ---------- Data containers ---------- - private static class Job extends BuildJob { + private static class Job { + private final ApplicationId instanceId; private final JobType jobType; private final JobRun triggering; private final Instant availableSince; @@ -627,7 +571,7 @@ public class DeploymentTrigger { private Job(Instance instance, JobRun triggering, JobType jobType, Instant availableSince, boolean isRetry, boolean isApplicationUpgrade) { - super(instance.id(), 0L, jobType.jobName()); + this.instanceId = instance.id(); this.jobType = jobType; this.triggering = triggering; this.availableSince = availableSince; @@ -635,6 +579,7 @@ public class DeploymentTrigger { this.isApplicationUpgrade = isApplicationUpgrade; } + ApplicationId applicationId() { return instanceId; } JobType jobType() { return jobType; } Instant availableSince() { return availableSince; } // TODO jvenstad: This is 95% broken now. Change.at() can restore it. boolean isRetry() { return isRetry; } 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 9df0dff3966..8cb5b08bcdb 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 @@ -31,6 +31,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationV import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId; import com.yahoo.vespa.hosted.controller.api.integration.organization.DeploymentFailureMails; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.Deployment; @@ -558,14 +559,6 @@ public class InternalStepRunner implements StepRunner { private Optional<RunStatus> report(RunId id, DualLogger logger) { try { controller.jobController().active(id).ifPresent(run -> { - JobReport report = JobReport.ofJob(run.id().application(), - run.id().type(), - run.id().number(), - ! run.hasFailed() ? Optional.empty() - : Optional.of(run.status() == outOfCapacity ? DeploymentJobs.JobError.outOfCapacity - : DeploymentJobs.JobError.unknown)); - controller.applications().deploymentTrigger().notifyOfCompletion(report); - if (run.hasFailed()) sendNotification(run, logger); }); @@ -580,7 +573,7 @@ public class InternalStepRunner implements StepRunner { /** Sends a mail with a notification of a failed run, if one should be sent. */ private void sendNotification(Run run, DualLogger logger) { Application application = controller.applications().requireApplication(TenantAndApplicationId.from(run.id().application())); - Notifications notifications = application.deploymentSpec().notifications(); + Notifications notifications = application.deploymentSpec().requireInstance(run.id().application().instance()).notifications(); boolean newCommit = application.change().application() .map(run.versions().targetApplication()::equals) .orElse(false); @@ -657,7 +650,9 @@ public class InternalStepRunner implements StepRunner { .orElse(zone.region().value().contains("aws-") ? DEFAULT_TESTER_RESOURCES_AWS : DEFAULT_TESTER_RESOURCES)); byte[] testPackage = controller.applications().applicationStore().getTester(id.application().tenant(), id.application().application(), version); - byte[] deploymentXml = deploymentXml(spec.athenzDomain(), spec.athenzService(zone.environment(), zone.region())); + byte[] deploymentXml = deploymentXml(id.tester(), + spec.requireInstance(id.application().instance()).athenzDomain(), + spec.requireInstance(id.application().instance()).athenzService(zone.environment(), zone.region())); try (ZipBuilder zipBuilder = new ZipBuilder(testPackage.length + servicesXml.length + 1000)) { zipBuilder.add(testPackage); @@ -779,13 +774,14 @@ public class InternalStepRunner implements StepRunner { } /** Returns a dummy deployment xml which sets up the service identity for the tester, if present. */ - private static byte[] deploymentXml(Optional<AthenzDomain> athenzDomain, Optional<AthenzService> athenzService) { + private static byte[] deploymentXml(TesterId id, Optional<AthenzDomain> athenzDomain, Optional<AthenzService> athenzService) { String deploymentSpec = "<?xml version='1.0' encoding='UTF-8'?>\n" + "<deployment version=\"1.0\" " + athenzDomain.map(domain -> "athenz-domain=\"" + domain.value() + "\" ").orElse("") + - athenzService.map(service -> "athenz-service=\"" + service.value() + "\" ").orElse("") - + "/>"; + athenzService.map(service -> "athenz-service=\"" + service.value() + "\" ").orElse("") + ">" + + " <instance id=\"" + id.id().instance().value() + "\" />" + + "</deployment>"; return deploymentSpec.getBytes(UTF_8); } 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 c4f394d237b..e6c59b464a6 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 @@ -1,8 +1,8 @@ // Copyright 2019 Oath Inc. 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.google.common.collect.ImmutableMap; import com.yahoo.component.Version; +import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.zone.ZoneId; @@ -10,7 +10,6 @@ import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.Instance; -import com.yahoo.vespa.hosted.controller.LockedApplication; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; import com.yahoo.vespa.hosted.controller.api.integration.LogEntry; import com.yahoo.vespa.hosted.controller.api.integration.configserver.NotFoundException; @@ -38,6 +37,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.NavigableMap; import java.util.Optional; import java.util.Set; import java.util.SortedMap; @@ -54,6 +54,8 @@ 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.endTests; import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toUnmodifiableList; +import static java.util.stream.Collectors.toUnmodifiableMap; /** * A singleton owned by the controller, which contains the state and methods for controlling deployment jobs. @@ -193,7 +195,6 @@ public class JobController { /** Returns a list of all applications which have registered. */ public List<TenantAndApplicationId> applications() { return copyOf(controller.applications().asList().stream() - .filter(Application::internal) .map(Application::id) .iterator()); } @@ -201,7 +202,6 @@ public class JobController { /** Returns a list of all instances of applications which have registered. */ public List<ApplicationId> instances() { return copyOf(controller.applications().asList().stream() - .filter(Application::internal) .flatMap(application -> application.instances().values().stream()) .map(Instance::id) .iterator()); @@ -215,10 +215,15 @@ public class JobController { } /** Returns an immutable map of all known runs for the given application and job type. */ - public Map<RunId, Run> runs(ApplicationId id, JobType type) { - SortedMap<RunId, Run> runs = curator.readHistoricRuns(id, type); + public NavigableMap<RunId, Run> runs(JobId id) { + return runs(id.application(), id.type()); + } + + /** Returns an immutable map of all known runs for the given application and job type. */ + public NavigableMap<RunId, Run> runs(ApplicationId id, JobType type) { + NavigableMap<RunId, Run> runs = curator.readHistoricRuns(id, type); last(id, type).ifPresent(run -> runs.put(run.id(), run)); - return ImmutableMap.copyOf(runs); + return Collections.unmodifiableNavigableMap(runs); } /** Returns the run with the given id, if it exists. */ @@ -238,6 +243,21 @@ public class JobController { return curator.readLastRun(id, type); } + /** Returns the last completed of the given job. */ + public Optional<Run> lastCompleted(JobId id) { + return JobStatus.lastCompleted(runs(id)); + } + + /** Returns the first failing of the given job. */ + public Optional<Run> firstFailing(JobId id) { + return JobStatus.firstFailing(runs(id)); + } + + /** Returns the last success of the given job. */ + public Optional<Run> lastSuccess(JobId id) { + return JobStatus.lastSuccess(runs(id)); + } + /** Returns the run with the given id, provided it is still active. */ public Optional<Run> active(RunId id) { return last(id.application(), id.type()) @@ -247,9 +267,9 @@ public class JobController { /** Returns a list of all active runs. */ public List<Run> active() { - return copyOf(applications().stream() - .flatMap(id -> active(id).stream()) - .iterator()); + return controller.applications().idList().stream() + .flatMap(id -> active(id).stream()) + .collect(toUnmodifiableList()); } /** Returns a list of all active runs for the given instance. */ @@ -262,6 +282,22 @@ public class JobController { .iterator()); } + /** Returns the job status of the given job, possibly empty. */ + public JobStatus jobStatus(JobId id) { + return new JobStatus(id, runs(id)); + } + + /** Returns the job status of all declared jobs for the given instance id, indexed by job type. */ + public DeploymentStatus deploymentStatus(Application application) { + return new DeploymentStatus(application, + application.deploymentSpec().instances().stream() + .flatMap(spec -> new DeploymentSteps(spec, controller::system) + .jobs().stream() + .map(type -> jobStatus(new JobId(application.id().instance(spec.name()), type)))) + .collect(toUnmodifiableMap(status -> status.id(), + status -> status))); + } + /** Changes the status of the given step, for the given run, provided it is still active. */ public void update(RunId id, RunStatus status, LockedStep step) { locked(id, run -> run.with(status, step)); @@ -274,11 +310,19 @@ public class JobController { locked(id.application(), id.type(), runs -> { runs.put(run.id(), finishedRun); long last = id.number(); + long successes = runs.values().stream().filter(old -> old.status() == RunStatus.success).count(); var oldEntries = runs.entrySet().iterator(); for (var old = oldEntries.next(); old.getKey().number() <= last - historyLength || old.getValue().start().isBefore(controller.clock().instant().minus(maxHistoryAge)); old = oldEntries.next()) { + + // Make sure we keep the last success and the first failing + if (successes == 1 && old.getValue().status() == RunStatus.success) { + oldEntries.next(); + continue; + } + logs.delete(old.getKey()); oldEntries.remove(); } @@ -300,9 +344,6 @@ public class JobController { ApplicationPackage applicationPackage, byte[] testPackageBytes) { AtomicReference<ApplicationVersion> version = new AtomicReference<>(); controller.applications().lockApplicationOrThrow(id, application -> { - if ( ! application.get().internal()) - application = registered(application); - long run = 1 + application.get().latestVersion() .map(latestVersion -> latestVersion.buildNumber().getAsLong()) .orElse(0L); @@ -330,33 +371,12 @@ public class JobController { return version.get(); } - /** Registers the given application, copying necessary application packages, and returns the modified version. */ - private LockedApplication registered(LockedApplication application) { - for (Instance instance : application.get().instances().values()) { - // TODO jvenstad: Remove when everyone has migrated off SDv3 pipelines. Real soon now! - // Copy all current packages to the new application store - instance.productionDeployments().values().stream() - .map(Deployment::applicationVersion) - .distinct() - .forEach(appVersion -> { - byte[] content = controller.applications().artifacts().getApplicationPackage(instance.id(), appVersion.id()); - controller.applications().applicationStore().put(instance.id().tenant(), instance.id().application(), appVersion, content); - }); - } - // Make sure any ongoing upgrade is cancelled, since future jobs will require the tester artifact. - return application.withChange(application.get().change().withoutPlatform().withoutApplication()) - .withBuiltInternally(true); - } - /** Orders a run of the given type, or throws an IllegalStateException if that job type is already running. */ public void start(ApplicationId id, JobType type, Versions versions) { if ( ! type.environment().isManuallyDeployed() && versions.targetApplication().isUnknown()) throw new IllegalArgumentException("Target application must be a valid reference."); controller.applications().lockApplicationIfPresent(TenantAndApplicationId.from(id), application -> { - if ( ! application.get().internal()) - throw new IllegalArgumentException(id + " is not built here!"); - locked(id, type, __ -> { Optional<Run> last = last(id, type); if (last.flatMap(run -> active(run.id())).isPresent()) @@ -378,9 +398,6 @@ public class JobController { controller.applications().createApplication(TenantAndApplicationId.from(id), Optional.empty()); controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> { - if ( ! application.get().internal()) - application = registered(application); - if ( ! application.get().instances().containsKey(id.instance())) application = application.withNewInstance(id.instance()); @@ -415,15 +432,6 @@ public class JobController { } } - /** Unregisters the given application and makes all associated data eligible for garbage collection. */ - public void unregister(TenantAndApplicationId id) { - controller.applications().lockApplicationIfPresent(id, application -> { - controller.applications().store(application.withBuiltInternally(false)); - for (InstanceName instance : application.get().instances().keySet()) - jobs(id.instance(instance)).forEach(type -> last(id.instance(instance), type).ifPresent(last -> abort(last.id()))); - }); - } - /** Deletes run data and tester deployments for applications which are unknown, or no longer built internally. */ public void collectGarbage() { Set<ApplicationId> applicationsToBuild = new HashSet<>(instances()); @@ -471,7 +479,9 @@ public class JobController { /** Returns a URI which points at a badge showing current status for all jobs for the given application. */ public URI overviewBadge(ApplicationId id) { - DeploymentSteps steps = new DeploymentSteps(controller.applications().requireApplication(TenantAndApplicationId.from(id)).deploymentSpec(), controller::system); + DeploymentSteps steps = new DeploymentSteps(controller.applications().requireApplication(TenantAndApplicationId.from(id)) + .deploymentSpec().requireInstance(id.instance()), + controller::system); return badges.overview(id, steps.jobs().stream() .map(type -> last(id, type)) diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobList.java new file mode 100644 index 00000000000..1ef83153bef --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobList.java @@ -0,0 +1,141 @@ +// Copyright 2019 Oath Inc. 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.collections.AbstractFilteringList; +import com.yahoo.component.Version; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; + +import java.time.Instant; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * A list of deployment jobs that can be filtered in various ways. + * + * @author jonmv + */ +public class JobList extends AbstractFilteringList<JobStatus, JobList> { + + private JobList(Collection<? extends JobStatus> jobs, boolean negate) { + super(jobs, negate, JobList::new); + } + + // ----------------------------------- Factories + + public static JobList from(Collection<? extends JobStatus> jobs) { + return new JobList(jobs, false); + } + + // ----------------------------------- Basic filters + + /** Returns the subset of jobs which are currently upgrading */ + public JobList upgrading() { + return matching(job -> job.isRunning() + && job.lastSuccess().isPresent() + && job.lastSuccess().get().versions().targetPlatform().isBefore(job.lastTriggered().get().versions().targetPlatform())); + } + + /** Returns the subset of jobs which are currently failing */ + public JobList failing() { + return matching(job -> ! job.isSuccess()); + } + + /** Returns the subset of jobs which must be failing due to an application change */ + public JobList failingApplicationChange() { + return matching(JobList::failingApplicationChange); + } + + /** Returns the subset of jobs which are failing with the given run status */ + public JobList withStatus(RunStatus status) { + return matching(job -> job.lastStatus().map(status::equals).orElse(false)); + } + + /** Returns the subset of jobs of the given type -- most useful when negated */ + public JobList type(JobType... types) { + return matching(job -> List.of(types).contains(job.id().type())); + } + + /** Returns the subset of jobs of which are production jobs */ + public JobList production() { + return matching(job -> job.id().type().isProduction()); + } + + // ----------------------------------- JobRun filtering + + /** Returns the list in a state where the next filter is for the lastTriggered run type */ + public RunFilter lastTriggered() { + return new RunFilter(JobStatus::lastTriggered); + } + + /** Returns the list in a state where the next filter is for the lastCompleted run type */ + public RunFilter lastCompleted() { + return new RunFilter(JobStatus::lastCompleted); + } + + /** Returns the list in a state where the next filter is for the lastSuccess run type */ + public RunFilter lastSuccess() { + return new RunFilter(JobStatus::lastSuccess); + } + + /** Returns the list in a state where the next filter is for the firstFailing run type */ + public RunFilter firstFailing() { + return new RunFilter(JobStatus::firstFailing); + } + + + /** Allows sub-filters for runs of the given kind */ + public class RunFilter { + + private final Function<JobStatus, Optional<Run>> which; + + private RunFilter(Function<JobStatus, Optional<Run>> which) { + this.which = which; + } + + /** Returns the subset of jobs where the run of the given type exists */ + public JobList present() { + return matching(run -> true); + } + + /** Returns the subset of jobs where the run of the given type occurred before the given instant */ + public JobList startedBefore(Instant threshold) { + return matching(run -> run.start().isBefore(threshold)); + } + + /** Returns the subset of jobs where the run of the given type occurred after the given instant */ + public JobList startedAfter(Instant threshold) { + return matching(run -> run.start().isAfter(threshold)); + } + + /** Returns the subset of jobs where the run of the given type was on the given version */ + public JobList on(ApplicationVersion version) { + return matching(run -> run.versions().targetApplication().equals(version)); + } + + /** Returns the subset of jobs where the run of the given type was on the given version */ + public JobList on(Version version) { + return matching(run -> run.versions().targetPlatform().equals(version)); + } + + /** Transforms the JobRun condition to a JobStatus condition, by considering only the JobRun mapped by which, and executes */ + private JobList matching(Predicate<Run> condition) { + return JobList.this.matching(job -> which.apply(job).filter(condition).isPresent()); + } + + } + + // ----------------------------------- Internal helpers + + private static boolean failingApplicationChange(JobStatus job) { + if (job.isSuccess()) return false; + if (job.lastSuccess().isEmpty()) return true; // An application which never succeeded is surely bad. + if ( ! job.firstFailing().get().versions().targetPlatform().equals(job.lastSuccess().get().versions().targetPlatform())) return false; // Version change may be to blame. + return ! job.firstFailing().get().versions().targetApplication().equals(job.lastSuccess().get().versions().targetApplication()); // Return whether there is an application change. + } + +} + diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobStatus.java new file mode 100644 index 00000000000..52d60aca388 --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobStatus.java @@ -0,0 +1,107 @@ +package com.yahoo.vespa.hosted.controller.deployment; + +import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId; + +import java.util.NavigableMap; +import java.util.Objects; +import java.util.Optional; + +/** + * Aggregates information about all known runs of a given job to provide the high level status. + * + * @author jonmv + */ +public class JobStatus { + + private final JobId id; + private final NavigableMap<RunId, Run> runs; + private final Optional<Run> lastTriggered; + private final Optional<Run> lastCompleted; + private final Optional<Run> lastSuccess; + private final Optional<Run> firstFailing; + + public JobStatus(JobId id, NavigableMap<RunId, Run> runs) { + this.id = Objects.requireNonNull(id); + this.runs = Objects.requireNonNull(runs); + this.lastTriggered = runs.descendingMap().values().stream().findFirst(); + this.lastCompleted = lastCompleted(runs); + this.lastSuccess = lastSuccess(runs); + this.firstFailing = firstFailing(runs); + } + + public JobId id() { + return id; + } + + public NavigableMap<RunId, Run> runs() { + return runs; + } + + public Optional<Run> lastTriggered() { + return lastTriggered; + } + + public Optional<Run> lastCompleted() { + return lastCompleted; + } + + public Optional<Run> lastSuccess() { + return lastSuccess; + } + + public Optional<Run> firstFailing() { + return firstFailing; + } + + public Optional<RunStatus> lastStatus() { + return lastCompleted().map(Run::status); + } + + public boolean isSuccess() { + return lastStatus().isPresent() && lastStatus().get() == RunStatus.success; + } + + public boolean isRunning() { + return lastTriggered.isPresent() && ! lastTriggered.get().hasEnded(); + } + + public boolean isOutOfCapacity() { + return lastStatus().isPresent() && lastStatus().get() == RunStatus.outOfCapacity; + } + + @Override + public String toString() { + return "JobStatus{" + + "id=" + id + + ", lastTriggered=" + lastTriggered + + ", lastCompleted=" + lastCompleted + + ", lastSuccess=" + lastSuccess + + ", firstFailing=" + firstFailing + + '}'; + } + + static Optional<Run> lastCompleted(NavigableMap<RunId, Run> runs) { + return runs.descendingMap().values().stream() + .filter(run -> run.hasEnded()) + .findFirst(); + } + + static Optional<Run> lastSuccess(NavigableMap<RunId, Run> runs) { + return runs.descendingMap().values().stream() + .filter(run -> run.status() == RunStatus.success) + .findFirst(); + } + + static Optional<Run> firstFailing(NavigableMap<RunId, Run> runs) { + Run failed = null; + loop: for (Run run : runs.descendingMap().values()) + switch (run.status()) { + case running: continue loop; + case success: break loop; + default: failed = run; + } + return Optional.ofNullable(failed); + } + +} 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 d7d6134ccb9..2f9c5ea9e08 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 @@ -179,7 +179,6 @@ public class Run { ", start=" + start + ", end=" + end + ", status=" + status + - ", steps=" + steps + '}'; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java index ea6cc983b71..4d0b7ef3b90 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java @@ -11,7 +11,7 @@ public enum RunStatus { /** Run is still proceeding normally, i.e., without failures. */ running, - /** Deployment was rejected due to missing capacity. */ + /** Deployment was rejected due to lack of capacity. */ outOfCapacity, /** Deployment of the real application was rejected. */ @@ -29,7 +29,7 @@ public enum RunStatus { /** Everything completed with great success! */ success, - /** Run has been abandoned, due to user intervention or timeout. */ + /** Run was abandoned, due to user intervention or job timeout. */ aborted } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java index 5d4a380411d..b2b217d0814 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java @@ -80,11 +80,6 @@ public class Versions { targetApplication.equals(versions.targetApplication()); } - public boolean targetsMatch(JobStatus.JobRun jobRun) { - return targetPlatform.equals(jobRun.platform()) && - targetApplication.equals(jobRun.application()); - } - @Override public boolean equals(Object o) { if (this == o) return true; 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 fc311e2a2af..ffe90b8c44d 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 @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.google.inject.Inject; import com.yahoo.log.LogLevel; import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; import com.yahoo.vespa.hosted.controller.deployment.InternalStepRunner; import com.yahoo.vespa.hosted.controller.deployment.JobController; import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId; @@ -13,6 +14,7 @@ import com.yahoo.vespa.hosted.controller.deployment.StepRunner; import org.jetbrains.annotations.TestOnly; import java.time.Duration; +import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -21,6 +23,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; +import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.outOfCapacity; + /** * Advances the set of {@link Run}s for a {@link JobController}. * @@ -82,7 +86,17 @@ public class JobRunner extends Maintainer { private void finish(RunId id) { try { jobs.finish(id); - } + controller().jobController().run(id).ifPresent(run -> { + DeploymentJobs.JobReport report = DeploymentJobs.JobReport.ofJob(run.id().application(), + run.id().type(), + run.id().number(), + ! run.hasFailed() ? Optional.empty() + : Optional.of(run.status() == outOfCapacity ? DeploymentJobs.JobError.outOfCapacity + : DeploymentJobs.JobError.unknown)); + controller().applications().deploymentTrigger().notifyOfCompletion(report); + }); + + } catch (Exception e) { log.log(LogLevel.WARNING, "Exception finishing " + id, e); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployer.java index 95e1c53f10c..b130f7107dd 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployer.java @@ -21,8 +21,9 @@ public class OutstandingChangeDeployer extends Maintainer { @Override protected void maintain() { for (Application application : controller().applications().asList()) { - if (application.outstandingChange().hasTargets() - && application.deploymentSpec().canChangeRevisionAt(controller().clock().instant())) { + if ( application.outstandingChange().hasTargets() + && application.deploymentSpec().instances().stream() + .allMatch(instance -> instance.canChangeRevisionAt(controller().clock().instant()))) { controller().applications().deploymentTrigger().triggerChange(application.id(), application.outstandingChange()); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/RoutingPolicies.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/RoutingPolicies.java index d6080bcda6c..98483763a0d 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/RoutingPolicies.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/RoutingPolicies.java @@ -1,6 +1,7 @@ // Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.maintenance; +import com.yahoo.config.application.api.DeploymentInstanceSpec; import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.zone.ZoneId; @@ -106,19 +107,21 @@ public class RoutingPolicies { private void storePoliciesOf(AllocatedLoadBalancers loadBalancers, DeploymentSpec spec, @SuppressWarnings("unused") Lock lock) { Set<RoutingPolicy> policies = new LinkedHashSet<>(get(loadBalancers.application)); for (LoadBalancer loadBalancer : loadBalancers.list) { - RoutingPolicy policy = createPolicy(loadBalancers.application, spec, loadBalancers.zone, loadBalancer); - if (!policies.add(policy)) { - policies.remove(policy); - policies.add(policy); - } + spec.instance(loadBalancer.application().instance()).ifPresent(instanceSpec -> { + RoutingPolicy policy = createPolicy(loadBalancers.application, instanceSpec, loadBalancers.zone, loadBalancer); + if (!policies.add(policy)) { + policies.remove(policy); + policies.add(policy); + } + }); } db.writeRoutingPolicies(loadBalancers.application, policies); } /** Create a policy for given load balancer and register a CNAME for it */ - private RoutingPolicy createPolicy(ApplicationId application, DeploymentSpec deploymentSpec, ZoneId zone, + private RoutingPolicy createPolicy(ApplicationId application, DeploymentInstanceSpec instanceSpec, ZoneId zone, LoadBalancer loadBalancer) { - var endpoints = endpointIdsOf(loadBalancer, zone, deploymentSpec); + var endpoints = endpointIdsOf(loadBalancer, zone, instanceSpec); var routingPolicy = new RoutingPolicy(application, loadBalancer.cluster(), zone, loadBalancer.hostname(), loadBalancer.dnsZone(), endpoints); @@ -162,9 +165,11 @@ public class RoutingPolicies { private static Set<RoutingId> routingIdsFrom(AllocatedLoadBalancers loadBalancers, DeploymentSpec spec) { Set<RoutingId> routingIds = new LinkedHashSet<>(); for (var loadBalancer : loadBalancers.list) { - for (var endpointId : endpointIdsOf(loadBalancer, loadBalancers.zone, spec)) { - routingIds.add(new RoutingId(loadBalancer.application(), endpointId)); - } + spec.instance(loadBalancer.application().instance()).ifPresent(instanceSpec -> { + for (var endpointId : endpointIdsOf(loadBalancer, loadBalancers.zone, instanceSpec)) { + routingIds.add(new RoutingId(loadBalancer.application(), endpointId)); + } + }); } return Collections.unmodifiableSet(routingIds); } @@ -183,7 +188,7 @@ public class RoutingPolicies { } /** Compute all endpoint IDs of given load balancer */ - private static Set<EndpointId> endpointIdsOf(LoadBalancer loadBalancer, ZoneId zone, DeploymentSpec spec) { + private static Set<EndpointId> endpointIdsOf(LoadBalancer loadBalancer, ZoneId zone, DeploymentInstanceSpec spec) { return spec.endpoints().stream() .filter(endpoint -> endpoint.containerId().equals(loadBalancer.cluster().value())) .filter(endpoint -> endpoint.regions().contains(zone.region())) diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java index c20904710ea..28e276c1497 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java @@ -49,29 +49,29 @@ public class Upgrader extends Maintainer { @Override public void maintain() { // Determine target versions for each upgrade policy - Optional<Version> canaryTarget = controller().versionStatus().systemVersion().map(VespaVersion::versionNumber); + Version canaryTarget = controller().systemVersion(); Collection<Version> defaultTargets = targetVersions(Confidence.normal); Collection<Version> conservativeTargets = targetVersions(Confidence.high); // Cancel upgrades to broken targets (let other ongoing upgrades complete to avoid starvation) for (VespaVersion version : controller().versionStatus().versions()) { if (version.confidence() == Confidence.broken) - cancelUpgradesOf(applications().without(UpgradePolicy.canary).upgradingTo(version.versionNumber()), + cancelUpgradesOf(applications().not().with(UpgradePolicy.canary).upgradingTo(version.versionNumber()), version.versionNumber() + " is broken"); } // Canaries should always try the canary target - cancelUpgradesOf(applications().with(UpgradePolicy.canary).upgrading().notUpgradingTo(canaryTarget), + cancelUpgradesOf(applications().with(UpgradePolicy.canary).upgrading().not().upgradingTo(canaryTarget), "Outdated target version for Canaries"); // Cancel *failed* upgrades to earlier versions, as the new version may fix it String reason = "Failing on outdated version"; - cancelUpgradesOf(applications().with(UpgradePolicy.defaultPolicy).upgrading().failing().notUpgradingTo(defaultTargets), reason); - cancelUpgradesOf(applications().with(UpgradePolicy.conservative).upgrading().failing().notUpgradingTo(conservativeTargets), reason); + cancelUpgradesOf(applications().with(UpgradePolicy.defaultPolicy).upgrading().failing().not().upgradingTo(defaultTargets), reason); + cancelUpgradesOf(applications().with(UpgradePolicy.conservative).upgrading().failing().not().upgradingTo(conservativeTargets), reason); // Schedule the right upgrades ApplicationList applications = applications(); - canaryTarget.ifPresent(target -> upgrade(applications.with(UpgradePolicy.canary), target)); + upgrade(applications.with(UpgradePolicy.canary), canaryTarget); defaultTargets.forEach(target -> upgrade(applications.with(UpgradePolicy.defaultPolicy), target)); conservativeTargets.forEach(target -> upgrade(applications.with(UpgradePolicy.conservative), target)); } @@ -98,14 +98,13 @@ public class Upgrader extends Maintainer { applications = applications.withProductionDeployment(); applications = applications.onLowerVersionThan(version); applications = applications.allowMajorVersion(version.getMajor(), targetMajorVersion().orElse(version.getMajor())); - applications = applications.notDeploying(); // wait with applications deploying an application change or already upgrading - applications = applications.notFailingOn(version); // try to upgrade only if it hasn't failed on this version + applications = applications.not().deploying(); // wait with applications deploying an application change or already upgrading + applications = applications.not().failingOn(version); // try to upgrade only if it hasn't failed on this version applications = applications.canUpgradeAt(controller().clock().instant()); // wait with applications that are currently blocking upgrades applications = applications.byIncreasingDeployedVersion(); // start with lowest versions - if (!containsOnlyCanaries(applications)) { // throttle upgrades of non-canaries - applications = applications.first(numberOfApplicationsToUpgrade()); - } - for (Application application : applications.asList()) + for (Application application : applications.with(UpgradePolicy.canary).asList()) + controller().applications().deploymentTrigger().triggerChange(application.id(), Change.of(version)); + for (Application application : applications.not().with(UpgradePolicy.canary).first(numberOfApplicationsToUpgrade()).asList()) controller().applications().deploymentTrigger().triggerChange(application.id(), Change.of(version)); } @@ -173,9 +172,4 @@ public class Upgrader extends Maintainer { controller().removeConfidenceOverride(version::equals); } - /** Returns whether all given applications are canaries */ - private static boolean containsOnlyCanaries(ApplicationList applications) { - return applications.asList().stream().allMatch(application -> application.deploymentSpec().upgradePolicy() == UpgradePolicy.canary); - } - } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java index 356a20f6eba..79296476aaa 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java @@ -2,10 +2,13 @@ package com.yahoo.vespa.hosted.controller.persistence; 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.ValidationOverrides; import com.yahoo.config.provision.ClusterSpec; +import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.security.KeyUtils; import com.yahoo.slime.ArrayTraverser; @@ -42,6 +45,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -175,7 +179,7 @@ public class ApplicationSerializer { application.projectId().ifPresent(projectId -> root.setLong(projectIdField, projectId)); application.deploymentIssueId().ifPresent(jiraIssueId -> root.setString(deploymentIssueField, jiraIssueId.value())); application.ownershipIssueId().ifPresent(issueId -> root.setString(ownershipIssueIdField, issueId.value())); - root.setBool(builtInternallyField, application.internal()); + root.setBool(builtInternallyField, true); // TODO jonmv: remove when the change with this comment has deployed. toSlime(application.change(), root, deployingField); toSlime(application.outstandingChange(), root, outstandingChangeField); application.owner().ifPresent(owner -> root.setString(ownerField, owner.username())); @@ -282,8 +286,7 @@ public class ApplicationSerializer { private void jobStatusToSlime(Collection<JobStatus> jobStatuses, Cursor jobStatusArray) { for (JobStatus jobStatus : jobStatuses) - if (jobStatus.type() != JobType.component) - toSlime(jobStatus, jobStatusArray.addObject()); + toSlime(jobStatus, jobStatusArray.addObject()); } private void toSlime(JobStatus jobStatus, Cursor object) { @@ -366,11 +369,10 @@ public class ApplicationSerializer { List<Instance> instances = instancesFromSlime(id, deploymentSpec, root.field(instancesField)); OptionalLong projectId = Serializers.optionalLong(root.field(projectIdField)); Optional<ApplicationVersion> latestVersion = latestVersionFromSlime(root.field(latestVersionField)); - boolean builtInternally = root.field(builtInternallyField).asBool(); return new Application(id, createdAt, deploymentSpec, validationOverrides, deploying, outstandingChange, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, - deployKeys, projectId, builtInternally, latestVersion, instances); + deployKeys, projectId, latestVersion, instances); } private Optional<ApplicationVersion> latestVersionFromSlime(Inspector latestVersionObject) { @@ -386,7 +388,7 @@ public class ApplicationSerializer { InstanceName instanceName = InstanceName.from(object.field(instanceNameField).asString()); List<Deployment> deployments = deploymentsFromSlime(object.field(deploymentsField)); DeploymentJobs deploymentJobs = deploymentJobsFromSlime(object.field(deploymentJobsField)); - List<AssignedRotation> assignedRotations = assignedRotationsFromSlime(deploymentSpec, object); + List<AssignedRotation> assignedRotations = assignedRotationsFromSlime(deploymentSpec, instanceName, object); RotationStatus rotationStatus = rotationStatusFromSlime(object); instances.add(new Instance(id.instance(instanceName), deployments, @@ -570,21 +572,32 @@ public class ApplicationSerializer { Instant.ofEpochMilli(object.field(atField).asLong()))); } - private List<AssignedRotation> assignedRotationsFromSlime(DeploymentSpec deploymentSpec, Inspector root) { + private List<AssignedRotation> assignedRotationsFromSlime(DeploymentSpec deploymentSpec, InstanceName instance, Inspector root) { var assignedRotations = new LinkedHashMap<EndpointId, AssignedRotation>(); root.field(assignedRotationsField).traverse((ArrayTraverser) (idx, inspector) -> { var clusterId = new ClusterSpec.Id(inspector.field(assignedRotationClusterField).asString()); var endpointId = EndpointId.of(inspector.field(assignedRotationEndpointField).asString()); var rotationId = new RotationId(inspector.field(assignedRotationRotationField).asString()); - var regions = deploymentSpec.endpoints().stream() - .filter(endpoint -> endpoint.endpointId().equals(endpointId.id())) - .flatMap(endpoint -> endpoint.regions().stream()) - .collect(Collectors.toSet()); + var regions = deploymentSpec.instance(instance) + .map(spec -> globalEndpointRegions(spec, endpointId)) + .orElse(Set.of()); assignedRotations.putIfAbsent(endpointId, new AssignedRotation(clusterId, endpointId, rotationId, regions)); }); return List.copyOf(assignedRotations.values()); } + private Set<RegionName> globalEndpointRegions(DeploymentInstanceSpec spec, EndpointId endpointId) { + if (spec.globalServiceId().isPresent()) + return spec.zones().stream() + .flatMap(zone -> zone.region().stream()) + .collect(Collectors.toSet()); + + return spec.endpoints().stream() + .filter(endpoint -> endpoint.endpointId().equals(endpointId.id())) + .flatMap(endpoint -> endpoint.regions().stream()) + .collect(Collectors.toSet()); + } + } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java index dbd52fc6d02..637da842d2f 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java @@ -39,6 +39,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.NavigableMap; import java.util.Optional; import java.util.Set; import java.util.SortedMap; @@ -55,6 +56,7 @@ import java.util.stream.Stream; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toUnmodifiableList; /** * Curator backed database for storing the persistence state of controllers. This maps controller specific operations @@ -351,16 +353,18 @@ public class CuratorDb { } private List<Application> readApplications(Predicate<TenantAndApplicationId> applicationFilter) { - return readApplicationIds().filter(applicationFilter) + return readApplicationIds().stream() + .filter(applicationFilter) .sorted() .map(this::readApplication) .flatMap(Optional::stream) .collect(Collectors.toUnmodifiableList()); } - private Stream<TenantAndApplicationId> readApplicationIds() { + public List<TenantAndApplicationId> readApplicationIds() { return curator.getChildren(applicationRoot).stream() - .map(TenantAndApplicationId::fromSerialized); + .map(TenantAndApplicationId::fromSerialized) + .collect(toUnmodifiableList()); } public void removeApplication(TenantAndApplicationId id) { @@ -381,7 +385,7 @@ public class CuratorDb { return readSlime(lastRunPath(id, type)).map(runSerializer::runFromSlime); } - public SortedMap<RunId, Run> readHistoricRuns(ApplicationId id, JobType type) { + public NavigableMap<RunId, Run> readHistoricRuns(ApplicationId id, JobType type) { return readSlime(runsPath(id, type)).map(runSerializer::runsFromSlime).orElse(new TreeMap<>(comparing(RunId::number))); } 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 dda1fb881a7..b84df02e583 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 @@ -22,6 +22,7 @@ import com.yahoo.vespa.hosted.controller.deployment.Versions; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.EnumMap; +import java.util.NavigableMap; import java.util.Optional; import java.util.SortedMap; import java.util.TreeMap; @@ -90,8 +91,8 @@ class RunSerializer { return runFromSlime(slime.get()); } - SortedMap<RunId, Run> runsFromSlime(Slime slime) { - SortedMap<RunId, Run> runs = new TreeMap<>(comparing(RunId::number)); + NavigableMap<RunId, Run> runsFromSlime(Slime slime) { + NavigableMap<RunId, Run> runs = new TreeMap<>(comparing(RunId::number)); Inspector runArray = slime.get(); runArray.traverse((ArrayTraverser) (__, runObject) -> { Run run = runFromSlime(runObject); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java index e3c048e865a..dd43195f67d 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java @@ -2,9 +2,10 @@ package com.yahoo.vespa.hosted.controller.proxy; import com.google.inject.Inject; -import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.component.AbstractComponent; import com.yahoo.jdisc.http.HttpRequest.Method; import com.yahoo.log.LogLevel; +import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier; import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry; @@ -23,8 +24,10 @@ import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; import java.net.URI; import java.time.Duration; import java.util.ArrayList; @@ -33,7 +36,9 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.logging.Logger; +import java.util.stream.Collectors; import static com.yahoo.yolean.Exceptions.uncheck; @@ -43,33 +48,37 @@ import static com.yahoo.yolean.Exceptions.uncheck; * @author bjorncs */ @SuppressWarnings("unused") // Injected -public class ConfigServerRestExecutorImpl implements ConfigServerRestExecutor { +public class ConfigServerRestExecutorImpl extends AbstractComponent implements ConfigServerRestExecutor { private static final Logger log = Logger.getLogger(ConfigServerRestExecutorImpl.class.getName()); private static final Duration PROXY_REQUEST_TIMEOUT = Duration.ofSeconds(10); private static final Set<String> HEADERS_TO_COPY = Set.of("X-HTTP-Method-Override", "Content-Type"); - private final ZoneRegistry zoneRegistry; - private final ServiceIdentityProvider sslContextProvider; + private final CloseableHttpClient client; @Inject public ConfigServerRestExecutorImpl(ZoneRegistry zoneRegistry, ServiceIdentityProvider sslContextProvider) { - this.zoneRegistry = zoneRegistry; - this.sslContextProvider = sslContextProvider; + RequestConfig config = RequestConfig.custom() + .setConnectTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis()) + .setConnectionRequestTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis()) + .setSocketTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis()).build(); + + this.client = createHttpClient(config, sslContextProvider, + new ControllerOrConfigserverHostnameVerifier(zoneRegistry)); } @Override public ProxyResponse handle(ProxyRequest proxyRequest) throws ProxyException { - HostnameVerifier hostnameVerifier = createHostnameVerifier(proxyRequest.getZoneId()); - List<URI> allServers = getConfigserverEndpoints(proxyRequest.getZoneId()); + // Make a local copy of the list as we want to manipulate it in case of ping problems. + List<URI> allServers = new ArrayList<>(proxyRequest.getTargets()); StringBuilder errorBuilder = new StringBuilder(); - if (queueFirstServerIfDown(allServers, hostnameVerifier)) { + if (queueFirstServerIfDown(allServers)) { errorBuilder.append("Change ordering due to failed ping."); } for (URI uri : allServers) { - Optional<ProxyResponse> proxyResponse = proxyCall(uri, proxyRequest, hostnameVerifier, errorBuilder); + Optional<ProxyResponse> proxyResponse = proxyCall(uri, proxyRequest, errorBuilder); if (proxyResponse.isPresent()) { return proxyResponse.get(); } @@ -79,32 +88,14 @@ public class ConfigServerRestExecutorImpl implements ConfigServerRestExecutor { + errorBuilder.toString())); } - private List<URI> getConfigserverEndpoints(ZoneId zoneId) { - // TODO: Use config server VIP for all zones that have one - // Make a local copy of the list as we want to manipulate it in case of ping problems. - if (zoneId.region().value().startsWith("aws-") || zoneId.region().value().contains("-aws-")) { - return List.of(zoneRegistry.getConfigServerVipUri(zoneId)); - } else { - return new ArrayList<>(zoneRegistry.getConfigServerUris(zoneId)); - } - } - - private Optional<ProxyResponse> proxyCall( - URI uri, ProxyRequest proxyRequest, HostnameVerifier hostnameVerifier, StringBuilder errorBuilder) + private Optional<ProxyResponse> proxyCall(URI uri, ProxyRequest proxyRequest, StringBuilder errorBuilder) throws ProxyException { final HttpRequestBase requestBase = createHttpBaseRequest( proxyRequest.getMethod(), proxyRequest.createConfigServerRequestUri(uri), proxyRequest.getData()); // Empty list of headers to copy for now, add headers when needed, or rewrite logic. copyHeaders(proxyRequest.getHeaders(), requestBase); - RequestConfig config = RequestConfig.custom() - .setConnectTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis()) - .setConnectionRequestTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis()) - .setSocketTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis()).build(); - try ( - CloseableHttpClient client = createHttpClient(config, sslContextProvider, hostnameVerifier); - CloseableHttpResponse response = client.execute(requestBase) - ) { + try (CloseableHttpResponse response = client.execute(requestBase)) { String content = getContent(response); int status = response.getStatusLine().getStatusCode(); if (status / 100 == 5) { @@ -182,7 +173,7 @@ public class ConfigServerRestExecutorImpl implements ConfigServerRestExecutor { * if it is not responding, we try the other servers first. False positive/negatives are not critical, * but will increase latency to some extent. */ - private boolean queueFirstServerIfDown(List<URI> allServers, HostnameVerifier hostnameVerifier) { + private boolean queueFirstServerIfDown(List<URI> allServers) { if (allServers.size() < 2) { return false; } @@ -194,10 +185,8 @@ public class ConfigServerRestExecutorImpl implements ConfigServerRestExecutor { .setConnectTimeout(timeout) .setConnectionRequestTimeout(timeout) .setSocketTimeout(timeout).build(); - try ( - CloseableHttpClient client = createHttpClient(config, sslContextProvider, hostnameVerifier); - CloseableHttpResponse response = client.execute(httpget) - ) { + httpget.setConfig(config); + try (CloseableHttpResponse response = client.execute(httpget)) { if (response.getStatusLine().getStatusCode() == 200) { return false; } @@ -210,8 +199,13 @@ public class ConfigServerRestExecutorImpl implements ConfigServerRestExecutor { return true; } - private HostnameVerifier createHostnameVerifier(ZoneId zoneId) { - return new AthenzIdentityVerifier(Set.of(zoneRegistry.getConfigServerHttpsIdentity(zoneId))); + @Override + public void deconstruct() { + try { + client.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } private static CloseableHttpClient createHttpClient(RequestConfig config, @@ -222,7 +216,30 @@ public class ConfigServerRestExecutorImpl implements ConfigServerRestExecutor { .setSslcontext(sslContextProvider.getIdentitySslContext()) .setSSLHostnameVerifier(hostnameVerifier) .setDefaultRequestConfig(config) + .setMaxConnPerRoute(10) + .setMaxConnTotal(500) + .setConnectionTimeToLive(1, TimeUnit.MINUTES) .build(); } + private static class ControllerOrConfigserverHostnameVerifier implements HostnameVerifier { + + private final HostnameVerifier configserverVerifier; + + ControllerOrConfigserverHostnameVerifier(ZoneRegistry registry) { + this.configserverVerifier = createConfigserverVerifier(registry); + } + + private static HostnameVerifier createConfigserverVerifier(ZoneRegistry registry) { + Set<AthenzIdentity> configserverIdentities = registry.zones().all().zones().stream() + .map(zone -> registry.getConfigServerHttpsIdentity(zone.getId())) + .collect(Collectors.toSet()); + return new AthenzIdentityVerifier(configserverIdentities); + } + + @Override + public boolean verify(String hostname, SSLSession session) { + return "localhost".equals(hostname) || configserverVerifier.verify(hostname, session); + } + } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java index 100292a0bdc..f398683567b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java @@ -1,7 +1,6 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.proxy; -import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.container.jdisc.HttpRequest; import java.io.InputStream; @@ -26,36 +25,36 @@ public class ProxyRequest { private final Map<String, List<String>> headers; private final InputStream requestData; - private final ZoneId zoneId; - private final String proxyPath; + private final List<URI> targets; + private final String targetPath; /** * The constructor calls exception if the request is invalid. * * @param request the request from the jdisc framework. - * @param zoneId the zone to proxy to. - * @param proxyPath the path to proxy to. + * @param targets list of targets this request should be proxied to (targets are tried once in order until a response is returned). + * @param targetPath the path to proxy to. * @throws ProxyException on errors */ - public ProxyRequest(HttpRequest request, ZoneId zoneId, String proxyPath) throws ProxyException { + public ProxyRequest(HttpRequest request, List<URI> targets, String targetPath) throws ProxyException { this(request.getMethod(), request.getUri(), request.getJDiscRequest().headers(), request.getData(), - zoneId, proxyPath); + targets, targetPath); } ProxyRequest(Method method, URI requestUri, Map<String, List<String>> headers, InputStream body, - ZoneId zoneId, String proxyPath) throws ProxyException { + List<URI> targets, String targetPath) throws ProxyException { Objects.requireNonNull(requestUri, "Request must be non-null"); - if (!requestUri.getPath().endsWith(proxyPath)) + if (!requestUri.getPath().endsWith(targetPath)) throw new ProxyException(ErrorResponse.badRequest(String.format( - "Request path '%s' does not end with proxy path '%s'", requestUri.getPath(), proxyPath))); + "Request path '%s' does not end with proxy path '%s'", requestUri.getPath(), targetPath))); this.method = Objects.requireNonNull(method); this.requestUri = Objects.requireNonNull(requestUri); this.headers = Objects.requireNonNull(headers); this.requestData = body; - this.zoneId = Objects.requireNonNull(zoneId); - this.proxyPath = proxyPath.startsWith("/") ? proxyPath : "/" + proxyPath; + this.targets = List.copyOf(targets); + this.targetPath = targetPath.startsWith("/") ? targetPath : "/" + targetPath; } @@ -71,23 +70,23 @@ public class ProxyRequest { return requestData; } - public ZoneId getZoneId() { - return zoneId; + public List<URI> getTargets() { + return targets; } public URI createConfigServerRequestUri(URI baseURI) { try { return new URI(baseURI.getScheme(), baseURI.getUserInfo(), baseURI.getHost(), - baseURI.getPort(), proxyPath, requestUri.getQuery(), requestUri.getFragment()); + baseURI.getPort(), targetPath, requestUri.getQuery(), requestUri.getFragment()); } catch (URISyntaxException e) { throw new RuntimeException(e); } } public URI getControllerPrefixUri() { - String prefixPath = proxyPath.equals("/") && !requestUri.getPath().endsWith("/") ? - requestUri.getPath() + proxyPath : - requestUri.getPath().substring(0, requestUri.getPath().length() - proxyPath.length() + 1); + String prefixPath = targetPath.equals("/") && !requestUri.getPath().endsWith("/") ? + requestUri.getPath() + targetPath : + requestUri.getPath().substring(0, requestUri.getPath().length() - targetPath.length() + 1); try { return new URI(requestUri.getScheme(), requestUri.getUserInfo(), requestUri.getHost(), requestUri.getPort(), prefixPath, null, null); @@ -98,7 +97,7 @@ public class ProxyRequest { @Override public String toString() { - return "[zone: " + zoneId + " request: " + proxyPath + "]"; + return "[targets: " + targets + " request: " + targetPath + "]"; } } 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 e55b08d0b4a..c8f5720327a 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 @@ -57,7 +57,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.resource.CostInfo; import com.yahoo.vespa.hosted.controller.api.integration.resource.MeteringInfo; import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceAllocation; import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceSnapshot; -import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingEndpoint; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.AssignedRotation; import com.yahoo.vespa.hosted.controller.application.Change; @@ -72,6 +71,7 @@ import com.yahoo.vespa.hosted.controller.application.JobStatus; import com.yahoo.vespa.hosted.controller.application.RoutingPolicy; import com.yahoo.vespa.hosted.controller.application.SystemApplication; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; +import com.yahoo.vespa.hosted.controller.deployment.DeploymentSteps; import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger; import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel; import com.yahoo.vespa.hosted.controller.deployment.TestConfigSerializer; @@ -290,11 +290,9 @@ public class ApplicationApiHandler extends LoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying")) return cancelDeploy(path.get("tenant"), path.get("application"), "all"); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/{choice}")) return cancelDeploy(path.get("tenant"), path.get("application"), path.get("choice")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/key")) return removeDeployKey(path.get("tenant"), path.get("application"), request); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/submit")) return JobControllerApiHandlerHelper.unregisterResponse(controller.jobController(), path.get("tenant"), path.get("application")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}")) return deleteInstance(path.get("tenant"), path.get("application"), path.get("instance"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying")) return cancelDeploy(path.get("tenant"), path.get("application"), "all"); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/{choice}")) return cancelDeploy(path.get("tenant"), path.get("application"), path.get("choice")); - if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/submit")) return JobControllerApiHandlerHelper.unregisterResponse(controller.jobController(), path.get("tenant"), path.get("application")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}")) return JobControllerApiHandlerHelper.abortJobResponse(controller.jobController(), appIdFromPath(path), jobTypeFromPath(path)); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}")) return deactivate(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), 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"), true, request); @@ -718,35 +716,38 @@ public class ApplicationApiHandler extends LoggingRequestHandler { private void toSlime(Cursor object, Instance instance, DeploymentSpec deploymentSpec, HttpRequest request) { object.setString("instance", instance.name().value()); - // Jobs sorted according to deployment spec - List<JobStatus> jobStatus = controller.applications().deploymentTrigger() - .steps(deploymentSpec) - .sortedJobs(instance.deploymentJobs().jobStatus().values()); - - - Cursor deploymentJobsArray = object.setArray("deploymentJobs"); - for (JobStatus job : jobStatus) { - Cursor jobObject = deploymentJobsArray.addObject(); - jobObject.setString("type", job.type().jobName()); - jobObject.setBool("success", job.isSuccess()); - job.lastTriggered().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("lastTriggered"))); - job.lastCompleted().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("lastCompleted"))); - job.firstFailing().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("firstFailing"))); - job.lastSuccess().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("lastSuccess"))); - } - // Change blockers - Cursor changeBlockers = object.setArray("changeBlockers"); - deploymentSpec.changeBlocker().forEach(changeBlocker -> { - Cursor changeBlockerObject = changeBlockers.addObject(); - changeBlockerObject.setBool("versions", changeBlocker.blocksVersions()); - changeBlockerObject.setBool("revisions", changeBlocker.blocksRevisions()); - changeBlockerObject.setString("timeZone", changeBlocker.window().zone().getId()); - Cursor days = changeBlockerObject.setArray("days"); - changeBlocker.window().days().stream().map(DayOfWeek::getValue).forEach(days::addLong); - Cursor hours = changeBlockerObject.setArray("hours"); - changeBlocker.window().hours().forEach(hours::addLong); - }); + if (deploymentSpec.instance(instance.name()).isPresent()) { + // Jobs sorted according to deployment spec + List<JobStatus> jobStatus = controller.applications().deploymentTrigger() + .steps(deploymentSpec.requireInstance(instance.name())) + .sortedJobs(instance.deploymentJobs().jobStatus().values()); + + + Cursor deploymentJobsArray = object.setArray("deploymentJobs"); + for (JobStatus job : jobStatus) { + Cursor jobObject = deploymentJobsArray.addObject(); + jobObject.setString("type", job.type().jobName()); + jobObject.setBool("success", job.isSuccess()); + job.lastTriggered().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("lastTriggered"))); + job.lastCompleted().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("lastCompleted"))); + job.firstFailing().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("firstFailing"))); + job.lastSuccess().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("lastSuccess"))); + } + + // Change blockers + Cursor changeBlockers = object.setArray("changeBlockers"); + deploymentSpec.instance(instance.name()).ifPresent(spec -> spec.changeBlocker().forEach(changeBlocker -> { + Cursor changeBlockerObject = changeBlockers.addObject(); + changeBlockerObject.setBool("versions", changeBlocker.blocksVersions()); + changeBlockerObject.setBool("revisions", changeBlocker.blocksRevisions()); + changeBlockerObject.setString("timeZone", changeBlocker.window().zone().getId()); + Cursor days = changeBlockerObject.setArray("days"); + changeBlocker.window().days().stream().map(DayOfWeek::getValue).forEach(days::addLong); + Cursor hours = changeBlockerObject.setArray("hours"); + changeBlocker.window().hours().forEach(hours::addLong); + })); + } // Rotation Cursor globalRotationsArray = object.setArray("globalRotations"); @@ -773,9 +774,10 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } // Deployments sorted according to deployment spec - List<Deployment> deployments = controller.applications().deploymentTrigger() - .steps(deploymentSpec) - .sortedDeployments(instance.deployments().values()); + List<Deployment> deployments = deploymentSpec.instance(instance.name()) + .map(spec -> new DeploymentSteps(spec, controller::system)) + .map(steps -> steps.sortedDeployments(instance.deployments().values())) + .orElse(List.copyOf(instance.deployments().values())); Cursor deploymentsArray = object.setArray("deployments"); for (Deployment deployment : deployments) { @@ -823,36 +825,37 @@ public class ApplicationApiHandler extends LoggingRequestHandler { toSlime(object.setObject("outstandingChange"), application.outstandingChange()); } - // Jobs sorted according to deployment spec - List<JobStatus> jobStatus = controller.applications().deploymentTrigger() - .steps(application.deploymentSpec()) - .sortedJobs(instance.deploymentJobs().jobStatus().values()); - - object.setBool("deployedInternally", application.internal()); - Cursor deploymentsArray = object.setArray("deploymentJobs"); - for (JobStatus job : jobStatus) { - Cursor jobObject = deploymentsArray.addObject(); - jobObject.setString("type", job.type().jobName()); - jobObject.setBool("success", job.isSuccess()); - - job.lastTriggered().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("lastTriggered"))); - job.lastCompleted().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("lastCompleted"))); - job.firstFailing().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("firstFailing"))); - job.lastSuccess().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("lastSuccess"))); - } + if (application.deploymentSpec().instance(instance.name()).isPresent()) { + // Jobs sorted according to deployment spec + List<JobStatus> jobStatus = controller.applications().deploymentTrigger() + .steps(application.deploymentSpec().requireInstance(instance.name())) + .sortedJobs(instance.deploymentJobs().jobStatus().values()); + + Cursor deploymentsArray = object.setArray("deploymentJobs"); + for (JobStatus job : jobStatus) { + Cursor jobObject = deploymentsArray.addObject(); + jobObject.setString("type", job.type().jobName()); + jobObject.setBool("success", job.isSuccess()); + + job.lastTriggered().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("lastTriggered"))); + job.lastCompleted().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("lastCompleted"))); + job.firstFailing().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("firstFailing"))); + job.lastSuccess().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("lastSuccess"))); + } - // Change blockers - Cursor changeBlockers = object.setArray("changeBlockers"); - application.deploymentSpec().changeBlocker().forEach(changeBlocker -> { - Cursor changeBlockerObject = changeBlockers.addObject(); - changeBlockerObject.setBool("versions", changeBlocker.blocksVersions()); - changeBlockerObject.setBool("revisions", changeBlocker.blocksRevisions()); - changeBlockerObject.setString("timeZone", changeBlocker.window().zone().getId()); - Cursor days = changeBlockerObject.setArray("days"); - changeBlocker.window().days().stream().map(DayOfWeek::getValue).forEach(days::addLong); - Cursor hours = changeBlockerObject.setArray("hours"); - changeBlocker.window().hours().forEach(hours::addLong); - }); + // Change blockers + Cursor changeBlockers = object.setArray("changeBlockers"); + application.deploymentSpec().instance(instance.name()).ifPresent(spec -> spec.changeBlocker().forEach(changeBlocker -> { + Cursor changeBlockerObject = changeBlockers.addObject(); + changeBlockerObject.setBool("versions", changeBlocker.blocksVersions()); + changeBlockerObject.setBool("revisions", changeBlocker.blocksRevisions()); + changeBlockerObject.setString("timeZone", changeBlocker.window().zone().getId()); + Cursor days = changeBlockerObject.setArray("days"); + changeBlocker.window().days().stream().map(DayOfWeek::getValue).forEach(days::addLong); + Cursor hours = changeBlockerObject.setArray("hours"); + changeBlocker.window().hours().forEach(hours::addLong); + })); + } // Compile version. The version that should be used when building an application object.setString("compileVersion", compileVersion(application.id()).toFullString()); @@ -884,9 +887,11 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } // Deployments sorted according to deployment spec - List<Deployment> deployments = controller.applications().deploymentTrigger() - .steps(application.deploymentSpec()) - .sortedDeployments(instance.deployments().values()); + List<Deployment> deployments = + application.deploymentSpec().instance(instance.name()) + .map(spec -> new DeploymentSteps(spec, controller::system)) + .map(steps -> steps.sortedDeployments(instance.deployments().values())) + .orElse(List.copyOf(instance.deployments().values())); Cursor instancesArray = object.setArray("instances"); for (Deployment deployment : deployments) { Cursor deploymentObject = instancesArray.addObject(); @@ -1116,16 +1121,15 @@ public class ApplicationApiHandler extends LoggingRequestHandler { Slime slime = new Slime(); Cursor array = slime.setObject().setArray("globalrotationoverride"); - Map<RoutingEndpoint, EndpointStatus> status = controller.applications().globalRotationStatus(deploymentId); - for (RoutingEndpoint endpoint : status.keySet()) { - EndpointStatus currentStatus = status.get(endpoint); - array.addString(endpoint.upstreamName()); - Cursor statusObject = array.addObject(); - statusObject.setString("status", currentStatus.getStatus().name()); - statusObject.setString("reason", currentStatus.getReason() == null ? "" : currentStatus.getReason()); - statusObject.setString("agent", currentStatus.getAgent() == null ? "" : currentStatus.getAgent()); - statusObject.setLong("timestamp", currentStatus.getEpoch()); - } + controller.applications().globalRotationStatus(deploymentId) + .forEach((endpoint, status) -> { + array.addString(endpoint.upstreamName()); + Cursor statusObject = array.addObject(); + statusObject.setString("status", status.getStatus().name()); + statusObject.setString("reason", status.getReason() == null ? "" : status.getReason()); + statusObject.setString("agent", status.getAgent() == null ? "" : status.getAgent()); + statusObject.setLong("timestamp", status.getEpoch()); + }); return new SlimeJsonResponse(slime); } @@ -1472,7 +1476,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler { applicationVersion = Optional.of(ApplicationVersion.from(toSourceRevision(sourceRevision), buildNumber.asLong())); applicationPackage = Optional.of(controller.applications().getApplicationPackage(applicationId, - application.get().internal(), applicationVersion.get())); } @@ -1496,7 +1499,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler { applicationVersion = Optional.of(version); vespaVersion = Optional.of(deployment.get().version()); applicationPackage = Optional.of(controller.applications().getApplicationPackage(applicationId, - application.get().internal(), applicationVersion.get())); } @@ -1569,12 +1571,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler { private HttpResponse notifyJobCompletion(String tenant, String application, HttpRequest request) { try { DeploymentJobs.JobReport report = toJobReport(tenant, application, toSlime(request.getData()).get()); - if ( report.jobType() == JobType.component - && controller.applications().requireApplication(TenantAndApplicationId.from(report.applicationId())).internal()) - throw new IllegalArgumentException(report.applicationId() + " is set up to be deployed from internally, and no " + - "longer accepts submissions from Screwdriver v3 jobs. If you need to revert " + - "to the old pipeline, please file a ticket at yo/vespa-support and request this."); - controller.applications().deploymentTrigger().notifyOfCompletion(report); return new MessageResponse("ok"); } catch (IllegalStateException e) { @@ -1613,14 +1609,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { ApplicationId id = ApplicationId.from(tenantName, applicationName, report.field("instance").asString()); JobType type = JobType.fromJobName(report.field("jobName").asString()); long buildNumber = report.field("buildNumber").asLong(); - if (type == JobType.component) - return DeploymentJobs.JobReport.ofComponent(id, - report.field("projectId").asLong(), - buildNumber, - jobError, - toSourceRevision(report.field("sourceRevision"))); - else - return DeploymentJobs.JobReport.ofJob(id, type, buildNumber, jobError); + return DeploymentJobs.JobReport.ofJob(id, type, buildNumber, jobError); } private static SourceRevision toSourceRevision(Inspector object) { @@ -1940,8 +1929,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler { ApplicationPackage applicationPackage = new ApplicationPackage(dataParts.get(EnvironmentResource.APPLICATION_ZIP)); if (DeploymentSpec.empty.equals(applicationPackage.deploymentSpec())) throw new IllegalArgumentException("Missing required file 'deployment.xml'"); - if (applicationPackage.deploymentSpec().instances().size() != 1) - throw new IllegalArgumentException("Only single-instance deployment specs are currently supported"); controller.applications().verifyApplicationIdentityConfiguration(TenantName.from(tenant), applicationPackage, 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 23d2646acd7..9a9a9798c6d 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 @@ -22,10 +22,10 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevisi import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.Deployment; -import com.yahoo.vespa.hosted.controller.application.JobStatus; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.deployment.DeploymentSteps; import com.yahoo.vespa.hosted.controller.deployment.JobController; +import com.yahoo.vespa.hosted.controller.deployment.JobStatus; import com.yahoo.vespa.hosted.controller.deployment.Run; import com.yahoo.vespa.hosted.controller.deployment.RunLog; import com.yahoo.vespa.hosted.controller.deployment.RunStatus; @@ -47,7 +47,6 @@ import java.util.stream.Collectors; import static com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy.conservative; import static com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy.defaultPolicy; -import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.component; import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.stagingTest; import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.systemTest; import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.unfinished; @@ -77,12 +76,13 @@ class JobControllerApiHandlerHelper { Application application = controller.applications().requireApplication(TenantAndApplicationId.from(id)); Instance instance = application.require(id.instance()); Change change = application.change(); - DeploymentSteps steps = new DeploymentSteps(application.deploymentSpec(), controller::system); + DeploymentSteps steps = new DeploymentSteps(application.deploymentSpec().requireInstance(id.instance()), controller::system); + Map<JobType, JobStatus> status = controller.jobController().deploymentStatus(application).instanceJobs(id.instance()); // The logic for pending runs imitates DeploymentTrigger logic; not good, but the trigger wiring must be re-written to reuse :S Map<JobType, Versions> pendingProduction = steps.productionJobs().stream() - .filter(type -> ! controller.applications().deploymentTrigger().isComplete(change, change, instance, type)) + .filter(type -> ! controller.applications().deploymentTrigger().isComplete(change, change, instance, type, status.get(type))) .collect(Collectors.toMap(type -> type, type -> Versions.from(change, application, @@ -103,8 +103,8 @@ class JobControllerApiHandlerHelper { Cursor lastVersionsObject = responseObject.setObject("lastVersions"); if (application.latestVersion().isPresent()) { - lastPlatformToSlime(lastVersionsObject.setObject("platform"), controller, application, instance, change, steps); - lastApplicationToSlime(lastVersionsObject.setObject("application"), application, instance, change, steps, controller); + lastPlatformToSlime(lastVersionsObject.setObject("platform"), controller, application, instance, status, change, steps); + lastApplicationToSlime(lastVersionsObject.setObject("application"), application, instance, status, change, steps, controller); } Cursor deployingObject = responseObject.setObject("deploying"); @@ -126,6 +126,7 @@ class JobControllerApiHandlerHelper { pendingProduction, running, type, + status.get(type), deployment); }); }); @@ -136,6 +137,7 @@ class JobControllerApiHandlerHelper { controller, application, instance, + status, type, steps, pendingProduction, @@ -159,11 +161,11 @@ class JobControllerApiHandlerHelper { return new SlimeJsonResponse(slime); } - private static void lastPlatformToSlime(Cursor lastPlatformObject, Controller controller, Application application, Instance instance, Change change, DeploymentSteps steps) { + private static void lastPlatformToSlime(Cursor lastPlatformObject, Controller controller, Application application, Instance instance, Map<JobType, JobStatus> status, Change change, DeploymentSteps steps) { VespaVersion lastVespa = controller.versionStatus().version(controller.systemVersion()); VespaVersion.Confidence targetConfidence = Map.of(defaultPolicy, normal, conservative, high) - .getOrDefault(application.deploymentSpec().upgradePolicy(), broken); + .getOrDefault(application.deploymentSpec().requireInstance(instance.name()).upgradePolicy(), broken); for (VespaVersion version : controller.versionStatus().versions()) if ( ! version.versionNumber().isAfter(controller.systemVersion()) && version.confidence().equalOrHigherThan(targetConfidence)) @@ -172,13 +174,15 @@ class JobControllerApiHandlerHelper { Version lastPlatform = lastVespa.versionNumber(); lastPlatformObject.setString("platform", lastPlatform.toString()); lastPlatformObject.setLong("at", lastVespa.committedAt().toEpochMilli()); - long completed = steps.productionJobs().stream().filter(type -> controller.applications().deploymentTrigger().isComplete(Change.of(lastPlatform), change, instance, type)).count(); + long completed = steps.productionJobs().stream().filter(type -> controller.applications().deploymentTrigger().isComplete(Change.of(lastPlatform), change, instance, type, status.get(type))).count(); if (Optional.of(lastPlatform).equals(change.platform())) lastPlatformObject.setString("deploying", completed + " of " + steps.productionJobs().size() + " complete"); else if (completed == steps.productionJobs().size()) lastPlatformObject.setString("completed", completed + " of " + steps.productionJobs().size() + " complete"); - else if ( ! application.deploymentSpec().canUpgradeAt(controller.clock().instant())) { - lastPlatformObject.setString("blocked", application.deploymentSpec().changeBlocker().stream() + else if ( ! application.deploymentSpec().instances().stream() + .allMatch(spec -> spec.canUpgradeAt(controller.clock().instant()))) { + lastPlatformObject.setString("blocked", application.deploymentSpec().instances().stream() + .flatMap(spec -> spec.changeBlocker().stream()) .filter(blocker -> blocker.blocksVersions()) .filter(blocker -> blocker.window().includes(controller.clock().instant())) .findAny().map(blocker -> blocker.window().toString()).get()); @@ -190,18 +194,20 @@ class JobControllerApiHandlerHelper { : "Waiting for " + application.change() + " to complete"); } - private static void lastApplicationToSlime(Cursor lastApplicationObject, Application application, Instance instance, Change change, DeploymentSteps steps, Controller controller) { + private static void lastApplicationToSlime(Cursor lastApplicationObject, Application application, Instance instance, Map<JobType, JobStatus> status, Change change, DeploymentSteps steps, Controller controller) { long completed; ApplicationVersion lastApplication = application.latestVersion().get(); applicationVersionToSlime(lastApplicationObject.setObject("application"), lastApplication); lastApplicationObject.setLong("at", lastApplication.buildTime().get().toEpochMilli()); - completed = steps.productionJobs().stream().filter(type -> controller.applications().deploymentTrigger().isComplete(Change.of(lastApplication), change, instance, type)).count(); + completed = steps.productionJobs().stream().filter(type -> controller.applications().deploymentTrigger().isComplete(Change.of(lastApplication), change, instance, type, status.get(type))).count(); if (Optional.of(lastApplication).equals(change.application())) lastApplicationObject.setString("deploying", completed + " of " + steps.productionJobs().size() + " complete"); else if (completed == steps.productionJobs().size()) lastApplicationObject.setString("completed", completed + " of " + steps.productionJobs().size() + " complete"); - else if ( ! application.deploymentSpec().canChangeRevisionAt(controller.clock().instant())) { - lastApplicationObject.setString("blocked", application.deploymentSpec().changeBlocker().stream() + else if ( ! application.deploymentSpec().instances().stream() + .allMatch(spec -> spec.canChangeRevisionAt(controller.clock().instant()))) { + lastApplicationObject.setString("blocked", application.deploymentSpec().instances().stream() + .flatMap(spec -> spec.changeBlocker().stream()) .filter(blocker -> blocker.blocksRevisions()) .filter(blocker -> blocker.window().includes(controller.clock().instant())) .findAny().map(blocker -> blocker.window().toString()).get()); @@ -212,32 +218,32 @@ class JobControllerApiHandlerHelper { private static void deploymentToSlime(Cursor deploymentObject, Instance instance, Change change, Map<JobType, Versions> pendingProduction, Map<JobType, Run> running, - JobType type, Deployment deployment) { + JobType type, JobStatus jobStatus, Deployment deployment) { deploymentObject.setLong("at", deployment.at().toEpochMilli()); deploymentObject.setString("platform", deployment.version().toString()); applicationVersionToSlime(deploymentObject.setObject("application"), deployment.applicationVersion()); - deploymentObject.setBool("verified", instance.deploymentJobs().statusOf(type) - .flatMap(JobStatus::lastSuccess) - .filter(run -> run.platform().equals(deployment.version()) - && run.application().equals(deployment.applicationVersion())) - .isPresent()); + deploymentObject.setBool("verified", jobStatus.lastSuccess() + .map(Run::versions) + .filter(run -> run.targetPlatform().equals(deployment.version()) + && run.targetApplication().equals(deployment.applicationVersion())) + .isPresent()); if (running.containsKey(type)) deploymentObject.setString("status", running.get(type).steps().get(deployReal) == unfinished ? "deploying" : "verifying"); else if (change.hasTargets()) deploymentObject.setString("status", pendingProduction.containsKey(type) ? "pending" : "completed"); } - private static void jobTypeToSlime(Cursor jobObject, Controller controller, Application application, Instance instance, JobType type, DeploymentSteps steps, + private static void jobTypeToSlime(Cursor jobObject, Controller controller, Application application, Instance instance, Map<JobType, JobStatus> status, JobType type, DeploymentSteps steps, Map<JobType, Versions> pendingProduction, Map<JobType, Run> running, URI baseUriForJob) { - instance.deploymentJobs().statusOf(type).ifPresent(status -> status.pausedUntil().ifPresent(until -> + instance.deploymentJobs().statusOf(type).ifPresent(jobStatus -> jobStatus.pausedUntil().ifPresent(until -> jobObject.setLong("pausedUntil", until))); int runs = 0; Cursor runArray = jobObject.setArray("runs"); if (type.isTest()) { Deque<List<JobType>> pending = new ArrayDeque<>(); pendingProduction.entrySet().stream() - .filter(typeVersions -> ! controller.applications().deploymentTrigger().testedIn(instance, type, typeVersions.getValue())) - .filter(typeVersions -> ! controller.applications().deploymentTrigger().alreadyTriggered(instance, typeVersions.getValue())) + .filter(typeVersions -> ! controller.applications().deploymentTrigger().testedIn(type, status.get(type), typeVersions.getValue())) + .filter(typeVersions -> ! controller.applications().deploymentTrigger().alreadyTriggered(status, typeVersions.getValue())) .collect(groupingBy(Map.Entry::getValue, LinkedHashMap::new, Collectors.mapping(Map.Entry::getKey, toList()))) @@ -251,7 +257,7 @@ class JobControllerApiHandlerHelper { Cursor runObject = runArray.addObject(); runObject.setString("status", "pending"); versionsToSlime(runObject, versions); - if ( ! controller.applications().deploymentTrigger().triggerAt(controller.clock().instant(), type, versions, instance, application.deploymentSpec())) + if ( ! controller.applications().deploymentTrigger().triggerAt(controller.clock().instant(), type, status.get(type), versions, instance, application.deploymentSpec())) runObject.setObject("tasks").setString("cooldown", "failed"); else runObject.setObject("tasks").setString("capacity", "running"); @@ -267,18 +273,18 @@ class JobControllerApiHandlerHelper { runObject.setString("status", "pending"); versionsToSlime(runObject, pendingProduction.get(type)); Cursor pendingObject = runObject.setObject("tasks"); - if (instance.deploymentJobs().statusOf(type).map(status -> status.pausedUntil().isPresent()).orElse(false)) + if (instance.deploymentJobs().statusOf(type).map(jobStatus -> jobStatus.pausedUntil().isPresent()).orElse(false)) pendingObject.setString("paused", "pending"); - else if ( ! controller.applications().deploymentTrigger().triggerAt(controller.clock().instant(), type, versions, instance, application.deploymentSpec())) + else if ( ! controller.applications().deploymentTrigger().triggerAt(controller.clock().instant(), type, status.get(type), versions, instance, application.deploymentSpec())) pendingObject.setString("cooldown", "failed"); else { int pending = 0; - if ( ! controller.applications().deploymentTrigger().alreadyTriggered(instance, versions)) { - if ( ! controller.applications().deploymentTrigger().testedIn(instance, systemTest, versions)) { + if ( ! controller.applications().deploymentTrigger().alreadyTriggered(status, versions)) { + if ( ! controller.applications().deploymentTrigger().testedIn(systemTest, status.get(systemTest), versions)) { pending++; pendingObject.setString(shortNameOf(systemTest, controller.system()), statusOf(controller, instance.id(), systemTest, versions)); } - if ( ! controller.applications().deploymentTrigger().testedIn(instance, stagingTest, versions)) { + if ( ! controller.applications().deploymentTrigger().testedIn(stagingTest, status.get(stagingTest), versions)) { pending++; pendingObject.setString(shortNameOf(stagingTest, controller.system()), statusOf(controller, instance.id(), stagingTest, versions)); } @@ -459,15 +465,6 @@ class JobControllerApiHandlerHelper { return new SlimeJsonResponse(slime); } - /** Unregisters the application from the internal deployment pipeline. */ - static HttpResponse unregisterResponse(JobController jobs, String tenantName, String applicationName) { - TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName); - jobs.unregister(id); - Slime slime = new Slime(); - slime.setObject().setString("message", "Unregistered '" + id + "' from internal deployment pipeline."); - return new SlimeJsonResponse(slime); - } - private static String nameOf(RunStatus status) { switch (status) { case running: return "running"; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java new file mode 100644 index 00000000000..6ffdea93a1c --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java @@ -0,0 +1,129 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.restapi.configserver; + +import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.config.provision.zone.ZoneList; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.restapi.ErrorResponse; +import com.yahoo.restapi.Path; +import com.yahoo.restapi.SlimeJsonResponse; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.Slime; +import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry; +import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry; +import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler; +import com.yahoo.vespa.hosted.controller.proxy.ConfigServerRestExecutor; +import com.yahoo.vespa.hosted.controller.proxy.ProxyException; +import com.yahoo.vespa.hosted.controller.proxy.ProxyRequest; +import com.yahoo.yolean.Exceptions; + +import java.net.URI; +import java.util.List; +import java.util.logging.Level; +import java.util.stream.Stream; + +/** + * REST API for proxying operator APIs to config servers in a given zone. + * + * @author freva + */ +@SuppressWarnings("unused") +public class ConfigServerApiHandler extends AuditLoggingRequestHandler { + + private static final ZoneId CONTROLLER_ZONE = ZoneId.from("prod", "controller"); + private static final URI CONTROLLER_URI = URI.create("https://localhost:4443/"); + private static final String OPTIONAL_PREFIX = "/api"; + private static final List<String> WHITELISTED_APIS = List.of("/flags/v1/", "/nodes/v2/", "/orchestrator/v1/"); + + private final ZoneRegistry zoneRegistry; + private final ConfigServerRestExecutor proxy; + + public ConfigServerApiHandler(Context parentCtx, ServiceRegistry serviceRegistry, + ConfigServerRestExecutor proxy, Controller controller) { + super(parentCtx, controller.auditLogger()); + this.zoneRegistry = serviceRegistry.zoneRegistry(); + this.proxy = proxy; + } + + @Override + public HttpResponse auditAndHandle(HttpRequest request) { + try { + switch (request.getMethod()) { + case GET: + return get(request); + case POST: + case PUT: + case DELETE: + case PATCH: + return proxy(request); + default: + return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is unsupported"); + } + } catch (IllegalArgumentException e) { + return ErrorResponse.badRequest(Exceptions.toMessageString(e)); + } catch (RuntimeException e) { + log.log(Level.WARNING, "Unexpected error handling '" + request.getUri() + "', " + + Exceptions.toMessageString(e)); + return ErrorResponse.internalServerError(Exceptions.toMessageString(e)); + } + } + + private HttpResponse get(HttpRequest request) { + Path path = new Path(request.getUri(), OPTIONAL_PREFIX); + if (path.matches("/configserver/v1")) { + return root(request); + } + return proxy(request); + } + + private HttpResponse proxy(HttpRequest request) { + Path path = new Path(request.getUri(), OPTIONAL_PREFIX); + if ( ! path.matches("/configserver/v1/{environment}/{region}/{*}")) { + return ErrorResponse.notFoundError("Nothing at " + path); + } + + ZoneId zoneId = ZoneId.from(path.get("environment"), path.get("region")); + if (! zoneRegistry.hasZone(zoneId) && ! CONTROLLER_ZONE.equals(zoneId)) { + throw new IllegalArgumentException("No such zone: " + zoneId.value()); + } + + String cfgPath = "/" + path.getRest(); + if (WHITELISTED_APIS.stream().noneMatch(cfgPath::startsWith)) { + return ErrorResponse.forbidden("Cannot access '" + cfgPath + + "' through /configserver/v1, following APIs are permitted: " + String.join(", ", WHITELISTED_APIS)); + } + + try { + return proxy.handle(new ProxyRequest(request, List.of(getEndpoint(zoneId)), cfgPath)); + } catch (ProxyException e) { + throw new RuntimeException(e); + } + } + + private HttpResponse root(HttpRequest request) { + Slime slime = new Slime(); + Cursor root = slime.setObject(); + ZoneList zoneList = zoneRegistry.zones().reachable(); + + Cursor zones = root.setArray("zones"); + Stream.concat(Stream.of(CONTROLLER_ZONE), zoneRegistry.zones().reachable().ids().stream()) + .forEach(zone -> { + Cursor object = zones.addObject(); + object.setString("environment", zone.environment().value()); + object.setString("region", zone.region().value()); + object.setString("uri", request.getUri().resolve( + "/configserver/v1/" + zone.environment().value() + "/" + zone.region().value()).toString()); + }); + return new SlimeJsonResponse(slime); + } + + private HttpResponse notFound(Path path) { + return ErrorResponse.notFoundError("Nothing at " + path); + } + + private URI getEndpoint(ZoneId zoneId) { + return CONTROLLER_ZONE.equals(zoneId) ? CONTROLLER_URI : zoneRegistry.getConfigServerVipUri(zoneId); + } +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/package-info.java new file mode 100644 index 00000000000..9949c2d17bf --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * @author freva + */ +package com.yahoo.vespa.hosted.controller.restapi.configserver; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java index 2adf6ce95e1..4ac7ff4d6d4 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java @@ -142,7 +142,7 @@ public class DeploymentApiHandler extends LoggingRequestHandler { "/application/" + instance.id().application().value()).toString()); object.setString("upgradePolicy", toString(controller.applications().requireApplication(TenantAndApplicationId.from(instance.id())) - .deploymentSpec().upgradePolicy())); + .deploymentSpec().requireInstance(instance.name()).upgradePolicy())); } private static String toString(DeploymentSpec.UpgradePolicy upgradePolicy) { 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 8ee95675465..2a75c7953ca 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 @@ -11,6 +11,7 @@ import com.yahoo.restapi.Path; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.athenz.api.AthenzPrincipal; +import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.athenz.client.zms.ZmsClientException; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.TenantController; @@ -96,9 +97,14 @@ public class AthenzRoleFilter extends JsonSecurityRequestFilterBase { roleMemberships.add(Role.tenantPipeline(tenant.get().name(), application.get())); if ( tenant.isPresent() && application.isPresent() && instance.isPresent() + && principal.getIdentity() instanceof AthenzUser && instance.get().value().equals(principal.getIdentity().getName())) roleMemberships.add(Role.athenzUser(tenant.get().name(), application.get(), instance.get())); + if (athenz.hasSystemFlagsDeployAccess(identity)) { + roleMemberships.add(Role.systemFlagsDeployer()); + } + return roleMemberships.isEmpty() ? Set.of(Role.everyone()) : Set.copyOf(roleMemberships); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java index 72336b4accf..ec95f0bbb4c 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java @@ -39,7 +39,7 @@ public class ControllerAuthorizationFilter extends JsonSecurityRequestFilterBase Optional<SecurityContext> securityContext = Optional.ofNullable((SecurityContext)request.getAttribute(SecurityContext.ATTRIBUTE_NAME)); if (securityContext.isEmpty()) - return Optional.of(new ErrorResponse(Response.Status.FORBIDDEN, "Access denied")); + return Optional.of(new ErrorResponse(Response.Status.UNAUTHORIZED, "Access denied - not authenticated")); Action action = Action.from(HttpRequest.Method.valueOf(request.getMethod())); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java index 752409d5694..8a3adcce30e 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java @@ -8,8 +8,10 @@ import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; import com.yahoo.io.IOUtils; +import com.yahoo.restapi.ErrorResponse; +import com.yahoo.restapi.MessageResponse; import com.yahoo.restapi.Path; -import com.yahoo.slime.ArrayTraverser; +import com.yahoo.restapi.SlimeJsonResponse; import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; @@ -23,9 +25,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.user.UserId; import com.yahoo.vespa.hosted.controller.api.integration.user.UserManagement; import com.yahoo.vespa.hosted.controller.api.role.Role; import com.yahoo.vespa.hosted.controller.api.role.RoleDefinition; -import com.yahoo.restapi.ErrorResponse; -import com.yahoo.restapi.MessageResponse; -import com.yahoo.restapi.SlimeJsonResponse; import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal; import com.yahoo.vespa.hosted.controller.restapi.application.EmptyResponse; import com.yahoo.yolean.Exceptions; @@ -191,8 +190,9 @@ public class UserApiHandler extends LoggingRequestHandler { var user = new UserId(require("user", Inspector::asString, requestObject)); var roles = SlimeStream.fromArray(requestObject.field("roles"), Inspector::asString) .map(roleName -> Roles.toRole(tenant, roleName)) - .peek(role -> users.addUsers(role, List.of(user))) .collect(Collectors.toUnmodifiableList()); + + users.addToRoles(user, roles); return new MessageResponse(user + " is now a member of " + roles.stream().map(Role::toString).collect(Collectors.joining(", "))); } @@ -217,11 +217,13 @@ public class UserApiHandler extends LoggingRequestHandler { TenantName tenant = TenantName.from(tenantName); String roleName = require("roleName", Inspector::asString, requestObject); UserId user = new UserId(require("user", Inspector::asString, requestObject)); - Role role = Roles.toRole(tenant, roleName); + List<Role> roles = Collections.singletonList(Roles.toRole(tenant, roleName)); - removeTenantRoleMember(tenant, user, role); + enforceLastAdminOfTenant(tenant, user, roles); + removeDeveloperKey(tenant, user, roles); + users.removeFromRoles(user, roles); - return new MessageResponse(user+" is no longer a member of "+role); + return new MessageResponse(user + " is no longer a member of " + roles.stream().map(Role::toString).collect(Collectors.joining(", "))); } private HttpResponse removeMultipleTenantRoleMembers(String tenantName, Inspector requestObject) { @@ -231,24 +233,35 @@ public class UserApiHandler extends LoggingRequestHandler { .map(roleName -> Roles.toRole(tenant, roleName)) .collect(Collectors.toUnmodifiableList()); - roles.forEach(role -> removeTenantRoleMember(tenant, user, role)); + enforceLastAdminOfTenant(tenant, user, roles); + removeDeveloperKey(tenant, user, roles); + users.removeFromRoles(user, roles); return new MessageResponse(user + " is no longer a member of " + roles.stream().map(Role::toString).collect(Collectors.joining(", "))); } - private void removeTenantRoleMember(TenantName tenantName, UserId user, Role role) { - if ( role.definition() == RoleDefinition.administrator - && Set.of(user.value()).equals(users.listUsers(role).stream().map(User::email).collect(Collectors.toSet()))) - throw new IllegalArgumentException("Can't remove the last administrator of a tenant."); - - if (role.definition().equals(RoleDefinition.developer)) - controller.tenants().lockIfPresent(tenantName, LockedTenant.Cloud.class, tenant -> { - PublicKey key = tenant.get().developerKeys().inverse().get(new SimplePrincipal(user.value())); - if (key != null) - controller.tenants().store(tenant.withoutDeveloperKey(key)); - }); + private void enforceLastAdminOfTenant(TenantName tenantName, UserId user, List<Role> roles) { + for (Role role : roles) { + if (role.definition().equals(RoleDefinition.administrator)) { + if (Set.of(user.value()).equals(users.listUsers(role).stream().map(User::email).collect(Collectors.toSet()))) { + throw new IllegalArgumentException("Can't remove the last administrator of a tenant."); + } + break; + } + } + } - users.removeUsers(role, List.of(user)); + private void removeDeveloperKey(TenantName tenantName, UserId user, List<Role> roles) { + for (Role role : roles) { + if (role.definition().equals(RoleDefinition.developer)) { + controller.tenants().lockIfPresent(tenantName, LockedTenant.Cloud.class, tenant -> { + PublicKey key = tenant.get().developerKeys().inverse().get(new SimplePrincipal(user.value())); + if (key != null) + controller.tenants().store(tenant.withoutDeveloperKey(key)); + }); + break; + } + } } private HttpResponse removeApplicationRoleMember(String tenantName, String applicationName, HttpRequest request) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java index 53373bb228a..abbbbef82c7 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java @@ -9,6 +9,7 @@ import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; import com.yahoo.slime.Cursor; import com.yahoo.slime.Slime; +import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry; import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry; import com.yahoo.restapi.ErrorResponse; import com.yahoo.restapi.Path; @@ -32,9 +33,9 @@ public class ZoneApiHandler extends LoggingRequestHandler { private final ZoneRegistry zoneRegistry; - public ZoneApiHandler(LoggingRequestHandler.Context parentCtx, ZoneRegistry zoneRegistry) { + public ZoneApiHandler(LoggingRequestHandler.Context parentCtx, ServiceRegistry serviceRegistry) { super(parentCtx); - this.zoneRegistry = zoneRegistry; + this.zoneRegistry = serviceRegistry.zoneRegistry(); } @Override diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java index be601511763..a127a44efb2 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java @@ -12,6 +12,7 @@ import com.yahoo.restapi.SlimeJsonResponse; import com.yahoo.slime.Cursor; import com.yahoo.slime.Slime; import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry; import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry; import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler; import com.yahoo.vespa.hosted.controller.proxy.ConfigServerRestExecutor; @@ -19,6 +20,8 @@ import com.yahoo.vespa.hosted.controller.proxy.ProxyException; import com.yahoo.vespa.hosted.controller.proxy.ProxyRequest; import com.yahoo.yolean.Exceptions; +import java.net.URI; +import java.util.List; import java.util.logging.Level; /** @@ -34,10 +37,10 @@ public class ZoneApiHandler extends AuditLoggingRequestHandler { private final ZoneRegistry zoneRegistry; private final ConfigServerRestExecutor proxy; - public ZoneApiHandler(LoggingRequestHandler.Context parentCtx, ZoneRegistry zoneRegistry, + public ZoneApiHandler(LoggingRequestHandler.Context parentCtx, ServiceRegistry serviceRegistry, ConfigServerRestExecutor proxy, Controller controller) { super(parentCtx, controller.auditLogger()); - this.zoneRegistry = zoneRegistry; + this.zoneRegistry = serviceRegistry.zoneRegistry(); this.proxy = proxy; } @@ -82,7 +85,7 @@ public class ZoneApiHandler extends AuditLoggingRequestHandler { throw new IllegalArgumentException("No such zone: " + zoneId.value()); } try { - return proxy.handle(new ProxyRequest(request, zoneId, path.getRest())); + return proxy.handle(new ProxyRequest(request, getConfigserverEndpoints(zoneId), path.getRest())); } catch (ProxyException e) { throw new RuntimeException(e); } @@ -110,4 +113,14 @@ public class ZoneApiHandler extends AuditLoggingRequestHandler { private HttpResponse notFound(Path path) { return ErrorResponse.notFoundError("Nothing at " + path); } + + private List<URI> getConfigserverEndpoints(ZoneId zoneId) { + // TODO: Use config server VIP for all zones that have one + if (zoneId.region().value().startsWith("aws-") || zoneId.region().value().contains("-aws-")) { + return List.of(zoneRegistry.getConfigServerVipUri(zoneId)); + } else { + return zoneRegistry.getConfigServerUris(zoneId); + } + } + } 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 d0cf5aac2d9..296639245a3 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 @@ -45,9 +45,7 @@ public class VespaVersion implements Comparable<VespaVersion> { public static Confidence confidenceFrom(DeploymentStatistics statistics, Controller controller) { // 'production on this': All deployment jobs upgrading to this version have completed without failure - ApplicationList productionOnThis = ApplicationList.from(statistics.production(), controller.applications()) - .notUpgradingTo(statistics.version()) - .notFailingUpgrade(); + ApplicationList productionOnThis = ApplicationList.from(statistics.production(), controller.applications()).not().upgradingTo(statistics.version()).not().failingUpgrade(); ApplicationList failingOnThis = ApplicationList.from(statistics.failing(), controller.applications()); ApplicationList all = ApplicationList.from(controller.applications().asList()) .withProductionDeployment(); @@ -162,8 +160,8 @@ public class VespaVersion implements Comparable<VespaVersion> { private static boolean nonCanaryApplicationsBroken(Version version, ApplicationList failingOnThis, ApplicationList productionOnThis) { - ApplicationList failingNonCanaries = failingOnThis.without(UpgradePolicy.canary).startedFailingOn(version); - ApplicationList productionNonCanaries = productionOnThis.without(UpgradePolicy.canary); + ApplicationList failingNonCanaries = failingOnThis.not().with(UpgradePolicy.canary).startedFailingOn(version); + ApplicationList productionNonCanaries = productionOnThis.not().with(UpgradePolicy.canary); if (productionNonCanaries.size() + failingNonCanaries.size() == 0) return false; 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 c374787aaa4..dbe451fd433 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 @@ -30,7 +30,6 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError; import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; import com.yahoo.vespa.hosted.controller.application.JobStatus; import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder; -import com.yahoo.vespa.hosted.controller.deployment.BuildJob; import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext; import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester; import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock; @@ -81,7 +80,7 @@ public class ControllerTest { // staging job - succeeding Version version1 = tester.configServer().initialVersion(); - var context = tester.deploymentContext(); + var context = tester.newDeploymentContext(); context.submit(applicationPackage); assertEquals("Application version is known from completion of initial job", ApplicationVersion.from(DeploymentContext.defaultSourceRevision, 1, "a@b", new Version("6.1"), Instant.ofEpochSecond(1)), @@ -384,7 +383,6 @@ public class ControllerTest { var west = ZoneId.from("prod", "us-west-1"); var central = ZoneId.from("prod", "us-central-1"); var east = ZoneId.from("prod", "us-east-3"); - var buildNumber = BuildJob.defaultBuildNumber; // Application is deployed with endpoint pointing to 2/3 zones ApplicationPackage applicationPackage = new ApplicationPackageBuilder() @@ -503,7 +501,7 @@ public class ControllerTest { @Test public void testUnassignRotations() { - var context = tester.deploymentContext(); + var context = tester.newDeploymentContext(); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .endpoint("default", "qrs", "us-west-1", "us-central-1") @@ -635,7 +633,7 @@ public class ControllerTest { .build(); // Create application - var context = tester.deploymentContext(); + var context = tester.newDeploymentContext(); // Direct deploy is allowed when deployDirectly is true ZoneId zone = ZoneId.from("prod", "cd-us-central-1"); @@ -666,7 +664,7 @@ public class ControllerTest { .build(); // Create application - var context = tester.deploymentContext(); + var context = tester.newDeploymentContext(); ZoneId zone = ZoneId.from("dev", "us-east-1"); // Deploy @@ -680,7 +678,7 @@ public class ControllerTest { @Test public void testSuspension() { - var context = tester.deploymentContext(); + var context = tester.newDeploymentContext(); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("us-west-1") @@ -704,7 +702,7 @@ public class ControllerTest { // second time will not fail @Test public void testDeletingApplicationThatHasAlreadyBeenDeleted() { - var context = tester.deploymentContext(); + var context = tester.newDeploymentContext(); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("us-east-3") @@ -723,12 +721,12 @@ public class ControllerTest { .environment(Environment.prod) .region("us-west-1") .build(true); - tester.deploymentContext().submit(applicationPackage); + tester.newDeploymentContext().submit(applicationPackage); } @Test public void testDeployApplicationWithWarnings() { - var context = tester.deploymentContext(); + var context = tester.newDeploymentContext(); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("us-west-1") @@ -790,7 +788,7 @@ public class ControllerTest { ZoneApiMock.fromId("prod.us-west-1"), ZoneApiMock.newBuilder().with(CloudName.from("aws")).withId("prod.aws-us-east-1").build() ); - var context = tester.deploymentContext(); + var context = tester.newDeploymentContext(); var applicationPackage = new ApplicationPackageBuilder() .region("aws-us-east-1") .region("us-west-1") diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java index 351b139f747..ff6a5d3795f 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java @@ -19,10 +19,8 @@ import com.yahoo.vespa.flags.InMemoryFlagSource; import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions; 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.BuildService; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.dns.MemoryNameService; import com.yahoo.vespa.hosted.controller.api.integration.dns.Record; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName; @@ -37,6 +35,7 @@ import com.yahoo.vespa.hosted.controller.integration.ServiceRegistryMock; import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb; +import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.security.AthenzCredentials; import com.yahoo.vespa.hosted.controller.security.AthenzTenantSpec; import com.yahoo.vespa.hosted.controller.security.Credentials; @@ -73,9 +72,9 @@ public final class ControllerTester { public static final int availableRotations = 10; + private final boolean inContainer; private final AthenzDbMock athenzDb; private final ManualClock clock; - private final ZoneRegistryMock zoneRegistry; private final ServiceRegistryMock serviceRegistry; private final CuratorDb curator; private final RotationsConfig rotationsConfig; @@ -88,7 +87,6 @@ public final class ControllerTester { public ControllerTester(RotationsConfig rotationsConfig, MockCuratorDb curatorDb) { this(new AthenzDbMock(), - new ZoneRegistryMock(), curatorDb, rotationsConfig, new ServiceRegistryMock()); @@ -106,13 +104,12 @@ public final class ControllerTester { this(defaultRotationsConfig(), new MockCuratorDb()); } - private ControllerTester(AthenzDbMock athenzDb, - ZoneRegistryMock zoneRegistry, + private ControllerTester(AthenzDbMock athenzDb, boolean inContainer, CuratorDb curator, RotationsConfig rotationsConfig, ServiceRegistryMock serviceRegistry, Controller controller) { this.athenzDb = athenzDb; + this.inContainer = inContainer; this.clock = serviceRegistry.clock(); - this.zoneRegistry = zoneRegistry; this.serviceRegistry = serviceRegistry; this.curator = curator; this.rotationsConfig = rotationsConfig; @@ -127,13 +124,23 @@ public final class ControllerTester { } private ControllerTester(AthenzDbMock athenzDb, - ZoneRegistryMock zoneRegistry, CuratorDb curator, RotationsConfig rotationsConfig, ServiceRegistryMock serviceRegistry) { - this(athenzDb, zoneRegistry, curator, rotationsConfig, serviceRegistry, - createController(curator, rotationsConfig, zoneRegistry, athenzDb, serviceRegistry)); + this(athenzDb, false, curator, rotationsConfig, serviceRegistry, + createController(curator, rotationsConfig, athenzDb, serviceRegistry)); + } + + /** Creates a ControllerTester built on the ContainerTester's controller. This controller can not be recreated. */ + public ControllerTester(ContainerTester tester) { + this(tester.athenzClientFactory().getSetup(), + true, + tester.controller().curator(), + null, + tester.serviceRegistry(), + tester.controller()); } + public void configureDefaultLogHandler(Consumer<Handler> configureFunc) { Arrays.stream(Logger.getLogger("").getHandlers()) // Do not mess with log configuration if a custom one has been set @@ -141,13 +148,6 @@ public final class ControllerTester { .forEach(configureFunc); } - public static BuildService.BuildJob buildJob(ApplicationId id, JobType jobType) { - if (jobType == JobType.component) - throw new AssertionError("Not supposed to happen"); - - return BuildService.BuildJob.of(id, 0, jobType.jobName()); - } - public Controller controller() { return controller; } public CuratorDb curator() { return curator; } @@ -158,7 +158,7 @@ public final class ControllerTester { public MemoryNameService nameService() { return serviceRegistry.nameServiceMock(); } - public ZoneRegistryMock zoneRegistry() { return zoneRegistry; } + public ZoneRegistryMock zoneRegistry() { return serviceRegistry.zoneRegistry(); } public ConfigServerMock configServer() { return serviceRegistry.configServerMock(); } @@ -179,7 +179,9 @@ public final class ControllerTester { /** Create a new controller instance. Useful to verify that controller state is rebuilt from persistence */ public final void createNewController() { - controller = createController(curator, rotationsConfig, zoneRegistry, athenzDb, serviceRegistry); + if (inContainer) + throw new UnsupportedOperationException("Cannot recreate this controller"); + controller = createController(curator, rotationsConfig, athenzDb, serviceRegistry); } /** Creates the given tenant and application and deploys it */ @@ -353,12 +355,10 @@ public final class ControllerTester { } private static Controller createController(CuratorDb curator, RotationsConfig rotationsConfig, - ZoneRegistryMock zoneRegistryMock, AthenzDbMock athensDb, ServiceRegistryMock serviceRegistry) { Controller controller = new Controller(curator, rotationsConfig, - zoneRegistryMock, new AthenzFacade(new AthenzClientFactoryMock(athensDb)), () -> "test-controller", new InMemoryFlagSource(), 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 3e96d2f6972..ea97e3e6c71 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 @@ -41,6 +41,10 @@ public class EndpointTest { "https://cd--a1--t1.global.vespa.oath.cloud:4443/", Endpoint.of(app1).named(endpointId).on(Port.tls(4443)).in(SystemName.cd), + // Main endpoint in CD + "https://cd--i2--a2--t2.global.vespa.oath.cloud:4443/", + Endpoint.of(app2).named(endpointId).on(Port.tls(4443)).in(SystemName.cd), + // Main endpoint with direct routing and default TLS port "https://a1.t1.global.vespa.oath.cloud/", Endpoint.of(app1).named(endpointId).on(Port.tls()).directRouting().in(SystemName.main), @@ -50,11 +54,11 @@ public class EndpointTest { Endpoint.of(app1).named(EndpointId.of("r1")).on(Port.tls()).directRouting().in(SystemName.main), // Main endpoint for custom instance in default rotation - "https://a2.t2.global.vespa.oath.cloud/", + "https://i2.a2.t2.global.vespa.oath.cloud/", Endpoint.of(app2).named(endpointId).on(Port.tls()).directRouting().in(SystemName.main), // Main endpoint for custom instance with custom rotation name - "https://r2.a2.t2.global.vespa.oath.cloud/", + "https://r2.i2.a2.t2.global.vespa.oath.cloud/", Endpoint.of(app2).named(EndpointId.of("r2")).on(Port.tls()).directRouting().in(SystemName.main), // Main endpoint in public system @@ -82,6 +86,10 @@ public class EndpointTest { Endpoint.of(app1).named(endpointId).on(Port.tls(4443)).in(SystemName.main), // Main endpoint in CD + "https://cd--i2--a2--t2.global.vespa.oath.cloud:4443/", + Endpoint.of(app2).named(endpointId).on(Port.tls(4443)).in(SystemName.cd), + + // Main endpoint in CD "https://cd--a1--t1.global.vespa.oath.cloud:4443/", Endpoint.of(app1).named(endpointId).on(Port.tls(4443)).in(SystemName.cd), @@ -94,11 +102,11 @@ public class EndpointTest { Endpoint.of(app1).named(EndpointId.of("r1")).on(Port.tls()).directRouting().in(SystemName.main), // Main endpoint for custom instance in default rotation - "https://a2.t2.global.vespa.oath.cloud/", + "https://i2.a2.t2.global.vespa.oath.cloud/", Endpoint.of(app2).named(endpointId).on(Port.tls()).directRouting().in(SystemName.main), // Main endpoint for custom instance with custom rotation name - "https://r2.a2.t2.global.vespa.oath.cloud/", + "https://r2.i2.a2.t2.global.vespa.oath.cloud/", Endpoint.of(app2).named(EndpointId.of("r2")).on(Port.tls()).directRouting().in(SystemName.main), // Main endpoint in public system diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/BuildJob.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/BuildJob.java deleted file mode 100644 index 63d84926144..00000000000 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/BuildJob.java +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2018 Yahoo Holdings. 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.config.provision.ApplicationId; -import com.yahoo.vespa.hosted.controller.Application; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision; -import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; -import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; -import com.yahoo.vespa.hosted.controller.integration.ArtifactRepositoryMock; -import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; - -import java.util.Objects; -import java.util.Optional; -import java.util.function.Consumer; - -import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.component; - -public class BuildJob { - - public static final long defaultBuildNumber = 42; - - private JobType job; - private ApplicationId applicationId; - private Optional<DeploymentJobs.JobError> jobError = Optional.empty(); - private Optional<SourceRevision> sourceRevision = Optional.of(DeploymentContext.defaultSourceRevision); - private long projectId; - private long buildNumber = defaultBuildNumber; - - private final Consumer<DeploymentJobs.JobReport> reportConsumer; - private final ArtifactRepositoryMock artifactRepository; - - public BuildJob(Consumer<DeploymentJobs.JobReport> reportConsumer, ArtifactRepositoryMock artifactRepository) { - Objects.requireNonNull(reportConsumer, "reportConsumer cannot be null"); - Objects.requireNonNull(artifactRepository, "artifactRepository cannot be null"); - this.reportConsumer = reportConsumer; - this.artifactRepository = artifactRepository; - } - - public BuildJob type(JobType job) { - this.job = job; - return this; - } - - public BuildJob application(Application application) { - if (application.projectId().isPresent()) - this.projectId = application.projectId().getAsLong(); - - return application(application.id().defaultInstance()); - } - - public BuildJob application(ApplicationId applicationId) { - this.applicationId = applicationId; - return this; - } - - public BuildJob error(DeploymentJobs.JobError jobError) { - this.jobError = Optional.of(jobError); - return this; - } - - public BuildJob sourceRevision(SourceRevision sourceRevision) { - this.sourceRevision = Optional.of(sourceRevision); - return this; - } - - public BuildJob buildNumber(long buildNumber) { - this.buildNumber = requireBuildNumber(buildNumber); - return this; - } - - public BuildJob nextBuildNumber(int increment) { - return buildNumber(buildNumber + requireBuildNumber(increment)); - } - - public BuildJob nextBuildNumber() { - return nextBuildNumber(1); - } - - public BuildJob projectId(long projectId) { - this.projectId = projectId; - return this; - } - - public BuildJob success(boolean success) { - this.jobError = success ? Optional.empty() : Optional.of(DeploymentJobs.JobError.unknown); - return this; - } - - public BuildJob unsuccessful() { - return success(false); - } - - /** Create a job report for this build job */ - public DeploymentJobs.JobReport report() { - return job == component ? DeploymentJobs.JobReport.ofComponent(applicationId, projectId, buildNumber, jobError, sourceRevision.get()) - : DeploymentJobs.JobReport.ofJob(applicationId, job, buildNumber, jobError); - } - - /** Upload given application package to artifact repository as part of this job */ - public BuildJob uploadArtifact(ApplicationPackage applicationPackage) { - Objects.requireNonNull(job, "job cannot be null"); - Objects.requireNonNull(applicationId, "applicationId cannot be null"); - if (job != component) { - throw new IllegalStateException(job + " cannot upload artifact"); - } - artifactRepository.put(applicationId, applicationPackage, ApplicationVersion.from(sourceRevision.get(), buildNumber).id()); - return this; - } - - /** Send report for this build job to the controller */ - public void submit() { - if (job == component && - !artifactRepository.contains(applicationId, ApplicationVersion.from(sourceRevision.get(), buildNumber).id())) { - throw new IllegalStateException(job + " must upload artifact before reporting completion"); - } - reportConsumer.accept(report()); - } - - private static long requireBuildNumber(long n) { - if (n <= 0) { - throw new IllegalArgumentException("Build number must be positive"); - } - return n; - } - -} 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 a5840cea3bd..d181ab8d38f 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 @@ -57,12 +57,13 @@ import static org.junit.Assert.assertTrue; * A deployment context for an application. This allows fine-grained control of the deployment of an application's * instances. * - * References to this should be acquired through {@link DeploymentTester#deploymentContext}. + * References to this should be acquired through {@link DeploymentTester#newDeploymentContext}. * * Tester code that is not specific to deployments should be added to either {@link ControllerTester} or * {@link DeploymentTester} instead of this class. * * @author mpolden + * @author jonmv */ public class DeploymentContext { @@ -118,9 +119,7 @@ public class DeploymentContext { try { var tenant = tester.createTenant(instanceId.tenant().value()); tester.createApplication(tenant, instanceId.application().value(), instanceId.instance().value()); - } catch (IllegalArgumentException ignored) { - // TODO(mpolden): Application already exists. Remove this once InternalDeploymentTester stops implicitly creating applications - } + } catch (IllegalArgumentException ignored) { } // Tenant and or application may already exist with custom setup. } public Application application() { @@ -139,6 +138,8 @@ public class DeploymentContext { return instanceId; } + public TesterId testerId() { return testerId; } + public DeploymentId deploymentIdIn(ZoneId zone) { return new DeploymentId(instanceId, zone); } @@ -171,10 +172,11 @@ public class DeploymentContext { .allMatch(deployments -> deployments.stream() .allMatch(deployment -> deployment.version().equals(version)))); - for (JobType type : new DeploymentSteps(application().deploymentSpec(), tester.controller()::system).productionJobs()) - assertTrue(tester.configServer().nodeRepository() - .list(type.zone(tester.controller().system()), applicationId.defaultInstance()).stream() // TODO jonmv: support more - .allMatch(node -> node.currentVersion().equals(version))); + for (var spec : application().deploymentSpec().instances()) + for (JobType type : new DeploymentSteps(spec, tester.controller()::system).productionJobs()) + assertTrue(tester.configServer().nodeRepository() + .list(type.zone(tester.controller().system()), applicationId.defaultInstance()).stream() // TODO jonmv: support more + .allMatch(node -> node.currentVersion().equals(version))); assertFalse(application().change().hasTargets()); return this; @@ -205,7 +207,7 @@ public class DeploymentContext { var projectId = tester.controller().applications() .requireApplication(applicationId) .projectId() - .orElseThrow(() -> new IllegalArgumentException("No project ID set for " + applicationId)); + .orElse(1000); // These are really set through submission, so just pick one if it hasn't been set. lastSubmission = jobs.submit(applicationId, sourceRevision, "a@b", projectId, applicationPackage, new byte[0]); return this; } @@ -266,7 +268,7 @@ public class DeploymentContext { triggerJobs(); } else - throw new AssertionError("Job '" + run.id().type() + "' was run twice for '" + instanceId + "'"); + throw new AssertionError("Job '" + run.id() + "' was run twice"); assertFalse("Change should have no targets, but was " + application().change(), application().change().hasTargets()); if (!deferDnsUpdates) { @@ -355,7 +357,6 @@ public class DeploymentContext { /** Deploy default application package, start a run for that change and return its ID */ public RunId newRun(JobType type) { - assertFalse(application().internal()); // Use this only once per test. submit(); readyJobsTrigger.maintain(); @@ -455,7 +456,7 @@ public class DeploymentContext { zone.region().value(), zone.environment().value()), "host1", - false, + true, String.format("cluster1.%s.%s.%s.%s", id.application().value(), id.tenant().value(), 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 0ca035b85b2..af58f8e825e 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 @@ -1,7 +1,6 @@ // Copyright 2018 Yahoo Holdings. 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.zone.ZoneId; import com.yahoo.log.LogLevel; @@ -14,13 +13,9 @@ import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision; import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId; import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingGeneratorMock; import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockTesterCloud; -import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock; import com.yahoo.vespa.hosted.controller.maintenance.JobControl; @@ -36,12 +31,8 @@ import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.Collections; -import java.util.Optional; import java.util.logging.Logger; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; /** @@ -59,7 +50,6 @@ public class DeploymentTester { public static final ApplicationId instanceId = appId.defaultInstance(); public static final TesterId testerId = TesterId.of(instanceId); - private final DeploymentContext defaultContext; private final ControllerTester tester; private final JobController jobs; private final RoutingGeneratorMock routing; @@ -104,7 +94,6 @@ public class DeploymentTester { outstandingChangeDeployer = new OutstandingChangeDeployer(tester.controller(), maintenanceInterval, jobControl); nameServiceDispatcher = new NameServiceDispatcher(tester.controller(), maintenanceInterval, jobControl, Integer.MAX_VALUE); - defaultContext = newDeploymentContext(instanceId); routing.putEndpoints(new DeploymentId(null, null), Collections.emptyList()); // Turn off default behaviour for the mock. // Get deployment job logs to stderr. @@ -139,9 +128,9 @@ public class DeploymentTester { return this; } - /** Returns the default deployment context owned by this */ - public DeploymentContext deploymentContext() { - return defaultContext; + /** Create the deployment context for the default instance id */ + public DeploymentContext newDeploymentContext() { + return newDeploymentContext(instanceId); } /** Create a new deployment context for given application */ @@ -159,29 +148,6 @@ public class DeploymentTester { return newDeploymentContext(tenantName, applicationName, instanceName).application(); } - /** Submits a new application, and returns the version of the new submission. */ - public ApplicationVersion newSubmission(TenantAndApplicationId id, ApplicationPackage applicationPackage, SourceRevision sourceRevision) { - return newDeploymentContext(id.defaultInstance()).submit(applicationPackage, sourceRevision).lastSubmission().get(); - } - - public ApplicationVersion newSubmission(TenantAndApplicationId id, ApplicationPackage applicationPackage) { - return newSubmission(id, applicationPackage, DeploymentContext.defaultSourceRevision); - } - - /** - * Submits a new application package, and returns the version of the new submission. - */ - public ApplicationVersion newSubmission(ApplicationPackage applicationPackage) { - return newSubmission(appId, applicationPackage); - } - - /** - * Submits a new application, and returns the version of the new submission. - */ - public ApplicationVersion newSubmission() { - return defaultContext.submit().lastSubmission().get(); - } - /** * Sets a single endpoint in the routing mock; this matches that required for the tester. */ @@ -189,34 +155,6 @@ public class DeploymentTester { newDeploymentContext(id).setEndpoints(zone); } - /** Completely deploys the given application version, assuming it is the last to be submitted. */ - public void deployNewSubmission(ApplicationVersion version) { - deployNewSubmission(appId, version); - } - - /** Completely deploys the given application version, assuming it is the last to be submitted. */ - public void deployNewSubmission(TenantAndApplicationId id, ApplicationVersion version) { - var context = newDeploymentContext(id.defaultInstance()); - var application = context.application(); - assertFalse(application.instances().values().stream() - .anyMatch(instance -> instance.deployments().values().stream() - .anyMatch(deployment -> deployment.applicationVersion().equals(version)))); - assertEquals(version, application.change().application().get()); - assertFalse(application.change().platform().isPresent()); - context.completeRollout(); - assertFalse(context.application().change().hasTargets()); - } - - /** Completely deploys the given, new platform. */ - public void deployNewPlatform(Version version) { - deployNewPlatform(appId, version); - } - - /** Completely deploys the given, new platform. */ - public void deployNewPlatform(TenantAndApplicationId id, Version version) { - newDeploymentContext(id.defaultInstance()).deployPlatform(version); - } - /** Aborts and finishes all running jobs. */ public void abortAll() { triggerJobs(); @@ -234,63 +172,4 @@ public class DeploymentTester { return triggered; } - /** Starts a manual deployment of the given package, and then runs the whole of the given job, successfully. */ - public void runJob(ApplicationId instanceId, JobType type, ApplicationPackage applicationPackage) { - jobs.deploy(instanceId, type, Optional.empty(), applicationPackage); - newDeploymentContext(instanceId).runJob(type); - } - - /** Pulls the ready job trigger, and then runs the whole of the given job, successfully. */ - public void runJob(JobType type) { - defaultContext.runJob(type); - } - - /** Pulls the ready job trigger, and then runs the whole of the given job, successfully. */ - public void runJob(ApplicationId instanceId, JobType type) { - if (type.environment().isManuallyDeployed()) - throw new IllegalArgumentException("Use overload with application package for dev/perf jobs"); - newDeploymentContext(instanceId).runJob(type); - } - - public void failDeployment(JobType type) { - defaultContext.failDeployment(type); - } - - public void failDeployment(ApplicationId instanceId, JobType type) { - newDeploymentContext(instanceId).failDeployment(type); - } - - public void timeOutUpgrade(JobType type) { - defaultContext.timeOutUpgrade(type); - } - - public void timeOutUpgrade(ApplicationId instanceId, JobType type) { - newDeploymentContext(instanceId).timeOutConvergence(type); - } - - public void timeOutConvergence(JobType type) { - defaultContext.timeOutConvergence(type); - } - - public void timeOutConvergence(ApplicationId instanceId, JobType type) { - newDeploymentContext(instanceId).timeOutConvergence(type); - } - - public RunId startSystemTestTests() { - return defaultContext.startSystemTestTests(); - } - - /** Creates and submits a new application, and then starts the job of the given type. Use only once per test. */ - public RunId newRun(JobType type) { - return defaultContext.newRun(type); - } - - public void assertRunning(JobType type) { - assertRunning(instanceId, type); - } - - public void assertRunning(ApplicationId id, JobType type) { - assertTrue(jobs.active().stream().anyMatch(run -> run.id().application().equals(id) && run.id().type() == type)); - } - } 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 07cddbe2c7e..8fb766164c2 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 @@ -59,7 +59,7 @@ public class DeploymentTriggerTest { .build(); // Deploy completely once - var app = tester.deploymentContext().submit(applicationPackage).deploy(); + var app = tester.newDeploymentContext().submit(applicationPackage).deploy(); // New version is released Version version = Version.fromString("6.3"); @@ -90,6 +90,7 @@ public class DeploymentTriggerTest { tester.applications().lockApplicationOrThrow(app.application().id(), locked -> tester.applications().store(locked.withProjectId(OptionalLong.empty()))); app.timeOutConvergence(productionUsWest1); + tester.triggerJobs(); assertEquals("Job is not triggered when no projectId is present", 0, tester.jobs().active().size()); } @@ -141,7 +142,7 @@ public class DeploymentTriggerTest { @Test public void abortsJobsOnNewApplicationChange() { - var app = tester.deploymentContext(); + var app = tester.newDeploymentContext(); app.submit() .runJob(systemTest) .runJob(stagingTest); @@ -188,7 +189,7 @@ public class DeploymentTriggerTest { .region("us-central-1") .delay(Duration.ofMinutes(10)) // Delays after last region are valid, but have no effect .build(); - var app = tester.deploymentContext().submit(applicationPackage); + var app = tester.newDeploymentContext().submit(applicationPackage); // Test jobs pass app.runJob(systemTest).runJob(stagingTest); @@ -242,7 +243,7 @@ public class DeploymentTriggerTest { .region("eu-west-1") .build(); - var app = tester.deploymentContext().submit(applicationPackage); + var app = tester.newDeploymentContext().submit(applicationPackage); // Test jobs pass app.runJob(systemTest).runJob(stagingTest); @@ -279,7 +280,7 @@ public class DeploymentTriggerTest { .region("us-central-1") .parallel("us-west-1", "us-east-3") .build(); - var application = tester.deploymentContext().submit().deploy(); + var application = tester.newDeploymentContext().submit().deploy(); // The first production zone is suspended: tester.configServer().setSuspended(application.deploymentIdIn(ZoneId.from("prod", "us-central-1")), true); @@ -317,7 +318,7 @@ public class DeploymentTriggerTest { .region("us-east-3") .build(); - var app = tester.deploymentContext().submit(applicationPackage).deploy(); + var app = tester.newDeploymentContext().submit(applicationPackage).deploy(); tester.clock().advance(Duration.ofHours(1)); // --------------- Enter block window: 18:30 @@ -352,7 +353,7 @@ public class DeploymentTriggerTest { .region("us-west-1") .region("us-east-3") .build(); - var app = tester.deploymentContext().submit(applicationPackage).deploy(); + var app = tester.newDeploymentContext().submit(applicationPackage).deploy(); // Application on (6.1, 1.0.1) Version v1 = Version.fromString("6.1"); @@ -399,7 +400,7 @@ public class DeploymentTriggerTest { .region("us-west-1") .region("us-east-3") .build(); - var app = tester.deploymentContext().submit(applicationPackage).deploy(); + var app = tester.newDeploymentContext().submit(applicationPackage).deploy(); tester.controllerTester().upgradeSystem(new Version("9.8.7")); tester.upgrader().maintain(); @@ -441,7 +442,7 @@ public class DeploymentTriggerTest { .region("us-central-1") .region("eu-west-1") .build(); - var app = tester.deploymentContext().submit(applicationPackage).deploy(); + var app = tester.newDeploymentContext().submit(applicationPackage).deploy(); // productionUsCentral1 fails after deployment, causing a mismatch between deployed and successful state. app.submit(applicationPackage) @@ -561,7 +562,7 @@ public class DeploymentTriggerTest { .parallel("eu-west-1", "us-east-3") .build(); // Application version 1 and platform version 6.1. - var app = tester.deploymentContext().submit(applicationPackage).deploy(); + var app = tester.newDeploymentContext().submit(applicationPackage).deploy(); // Success in first prod zone, change cancelled between triggering and completion of eu west job. // One of the parallel zones get a deployment, but both fail their jobs. @@ -613,7 +614,7 @@ public class DeploymentTriggerTest { .build(); // Deploy completely on default application and platform versions - var app = tester.deploymentContext().submit(applicationPackage).deploy(); + var app = tester.newDeploymentContext().submit(applicationPackage).deploy(); // New application change is deployed and fails in system-test for a while app.submit(applicationPackage).runJob(stagingTest).failDeployment(systemTest); @@ -666,7 +667,7 @@ public class DeploymentTriggerTest { // Initial failure Instant initialFailure = tester.clock().instant().truncatedTo(MILLIS); - var app = tester.deploymentContext().submit(applicationPackage); + var app = tester.newDeploymentContext().submit(applicationPackage); app.failDeployment(systemTest); assertEquals("Failure age is right at initial failure", initialFailure, app.instance().deploymentJobs().jobStatus().get(systemTest).firstFailing().get().at()); @@ -710,7 +711,7 @@ public class DeploymentTriggerTest { .region("us-west-1") .build(); Version version1 = tester.controller().versionStatus().systemVersion().get().versionNumber(); - var app1 = tester.deploymentContext(); + var app1 = tester.newDeploymentContext(); // First deployment: An application change app1.submit(applicationPackage).deploy(); @@ -855,21 +856,22 @@ public class DeploymentTriggerTest { @Test public void testUserInstancesNotInDeploymentSpec() { - var app = tester.deploymentContext(); + var app = tester.newDeploymentContext(); tester.controller().applications().createInstance(app.application().id().instance("user")); app.submit().deploy(); } @Test - @Ignore public void testMultipleInstances() { ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .instances("instance1,instance2") .environment(Environment.prod) .region("us-east-3") .build(); - var app = tester.deploymentContext().submit(applicationPackage); // TODO jonmv: support instances in deployment context> - app.deploy(); + var app = tester.newDeploymentContext("tenant1", "application1", "instance1").submit(applicationPackage); // TODO jonmv: support instances in deployment context> + var otherInstance = tester.newDeploymentContext("tenant1", "application1", "instance2"); + app.runJob(systemTest).runJob(stagingTest).runJob(productionUsEast3); + otherInstance.runJob(systemTest).runJob(stagingTest).runJob(productionUsEast3); assertEquals(2, app.application().instances().size()); assertEquals(2, app.application().productionDeployments().values().stream() .mapToInt(Collection::size) 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 7be2b6a9797..a1f0f924dcf 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 @@ -53,7 +53,6 @@ import static com.yahoo.vespa.hosted.controller.api.integration.LogEntry.Type.wa import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTester.instanceId; import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.applicationPackage; import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.publicCdApplicationPackage; -import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTester.testerId; 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; @@ -70,10 +69,12 @@ import static org.junit.Assert.fail; public class InternalStepRunnerTest { private DeploymentTester tester; + private DeploymentContext app; @Before public void setup() { tester = new DeploymentTester(); + app = tester.newDeploymentContext(); } private SystemName system() { @@ -82,38 +83,39 @@ public class InternalStepRunnerTest { @Test public void canRegisterAndRunDirectly() { - tester.deploymentContext().submit().deploy(); + app.submit().deploy(); } @Test public void testerHasAthenzIdentity() { - tester.newRun(JobType.stagingTest); + app.submit(); + tester.triggerJobs(); tester.runner().run(); DeploymentSpec spec = tester.configServer() - .application(DeploymentTester.testerId.id(), JobType.stagingTest.zone(system())).get() + .application(app.testerId().id(), JobType.stagingTest.zone(system())).get() .applicationPackage().deploymentSpec(); + assertTrue(spec.instance(app.testerId().id().instance()).isPresent()); assertEquals("domain", spec.athenzDomain().get().value()); - ZoneId zone = JobType.stagingTest.zone(system()); - assertEquals("service", spec.athenzService(zone.environment(), zone.region()).get().value()); + assertEquals("service", spec.athenzService().get().value()); } @Test public void refeedRequirementBlocksDeployment() { - RunId id = tester.newRun(JobType.stagingTest); + RunId id = app.newRun(JobType.stagingTest); - tester.setEndpoints(testerId.id(), JobType.stagingTest.zone(system())); + tester.setEndpoints(app.testerId().id(), JobType.stagingTest.zone(system())); tester.runner().run(); assertEquals(unfinished, tester.jobs().run(id).get().steps().get(Step.installInitialReal)); - tester.setEndpoints(instanceId, JobType.stagingTest.zone(system())); - tester.configServer().convergeServices(instanceId, JobType.stagingTest.zone(system())); + tester.setEndpoints(app.instanceId(), JobType.stagingTest.zone(system())); + tester.configServer().convergeServices(app.instanceId(), JobType.stagingTest.zone(system())); tester.configServer().setConfigChangeActions(new ConfigChangeActions(Collections.emptyList(), - singletonList(new RefeedAction("Refeed", - false, - "doctype", - "cluster", - Collections.emptyList(), - singletonList("Refeed it!"))))); + singletonList(new RefeedAction("Refeed", + false, + "doctype", + "cluster", + Collections.emptyList(), + singletonList("Refeed it!"))))); tester.runner().run(); assertEquals(failed, tester.jobs().run(id).get().steps().get(Step.deployReal)); @@ -121,11 +123,11 @@ public class InternalStepRunnerTest { @Test public void restartsServicesAndWaitsForRestartAndReboot() { - RunId id = tester.newRun(JobType.productionUsCentral1); + RunId id = app.newRun(JobType.productionUsCentral1); ZoneId zone = id.type().zone(system()); HostName host = tester.configServer().hostFor(instanceId, zone); - tester.setEndpoints(testerId.id(), JobType.productionUsCentral1.zone(system())); + tester.setEndpoints(app.testerId().id(), JobType.productionUsCentral1.zone(system())); tester.runner().run(); tester.configServer().setConfigChangeActions(new ConfigChangeActions(singletonList(new RestartAction("cluster", @@ -140,11 +142,11 @@ public class InternalStepRunnerTest { tester.runner().run(); assertEquals(succeeded, tester.jobs().run(id).get().steps().get(Step.deployReal)); - tester.configServer().convergeServices(instanceId, zone); + tester.configServer().convergeServices(app.instanceId(), zone); assertEquals(unfinished, tester.jobs().run(id).get().steps().get(Step.installReal)); - tester.configServer().nodeRepository().doRestart(new DeploymentId(instanceId, zone), Optional.of(host)); - tester.configServer().nodeRepository().requestReboot(new DeploymentId(instanceId, zone), Optional.of(host)); + 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().steps().get(Step.installReal)); @@ -155,85 +157,85 @@ public class InternalStepRunnerTest { @Test public void waitsForEndpointsAndTimesOut() { - tester.newRun(JobType.systemTest); + app.newRun(JobType.systemTest); // Tester fails to show up for staging tests, and the real deployment for system tests. - tester.setEndpoints(testerId.id(), JobType.systemTest.zone(system())); - tester.setEndpoints(instanceId, JobType.stagingTest.zone(system())); + tester.setEndpoints(app.testerId().id(), JobType.systemTest.zone(system())); + tester.setEndpoints(app.instanceId(), JobType.stagingTest.zone(system())); tester.runner().run(); - tester.configServer().convergeServices(instanceId, JobType.stagingTest.zone(system())); + tester.configServer().convergeServices(app.instanceId(), JobType.stagingTest.zone(system())); tester.runner().run(); - tester.configServer().convergeServices(instanceId, JobType.systemTest.zone(system())); - tester.configServer().convergeServices(testerId.id(), JobType.systemTest.zone(system())); - tester.configServer().convergeServices(instanceId, JobType.stagingTest.zone(system())); - tester.configServer().convergeServices(testerId.id(), JobType.stagingTest.zone(system())); + tester.configServer().convergeServices(app.instanceId(), JobType.systemTest.zone(system())); + tester.configServer().convergeServices(app.testerId().id(), JobType.systemTest.zone(system())); + tester.configServer().convergeServices(app.instanceId(), JobType.stagingTest.zone(system())); + tester.configServer().convergeServices(app.testerId().id(), JobType.stagingTest.zone(system())); tester.runner().run(); tester.clock().advance(InternalStepRunner.endpointTimeout.plus(Duration.ofSeconds(1))); tester.runner().run(); - assertEquals(failed, tester.jobs().last(instanceId, JobType.systemTest).get().steps().get(Step.installReal)); - assertEquals(failed, tester.jobs().last(instanceId, JobType.stagingTest).get().steps().get(Step.installTester)); + assertEquals(failed, tester.jobs().last(app.instanceId(), JobType.systemTest).get().steps().get(Step.installReal)); + assertEquals(failed, tester.jobs().last(app.instanceId(), JobType.stagingTest).get().steps().get(Step.installTester)); } @Test public void installationFailsIfDeploymentExpires() { - tester.newRun(JobType.systemTest); + app.newRun(JobType.systemTest); tester.runner().run(); - tester.configServer().convergeServices(instanceId, JobType.systemTest.zone(system())); - tester.setEndpoints(instanceId, JobType.systemTest.zone(system())); + tester.configServer().convergeServices(app.instanceId(), JobType.systemTest.zone(system())); + tester.setEndpoints(app.instanceId(), JobType.systemTest.zone(system())); tester.runner().run(); - assertEquals(succeeded, tester.jobs().last(instanceId, JobType.systemTest).get().steps().get(Step.installReal)); + assertEquals(succeeded, tester.jobs().last(app.instanceId(), JobType.systemTest).get().steps().get(Step.installReal)); - tester.applications().deactivate(instanceId, JobType.systemTest.zone(system())); + tester.applications().deactivate(app.instanceId(), JobType.systemTest.zone(system())); tester.runner().run(); - assertEquals(failed, tester.jobs().last(instanceId, JobType.systemTest).get().steps().get(Step.installTester)); - assertTrue(tester.jobs().last(instanceId, JobType.systemTest).get().hasEnded()); - assertTrue(tester.jobs().last(instanceId, JobType.systemTest).get().hasFailed()); + assertEquals(failed, tester.jobs().last(app.instanceId(), JobType.systemTest).get().steps().get(Step.installTester)); + assertTrue(tester.jobs().last(app.instanceId(), JobType.systemTest).get().hasEnded()); + assertTrue(tester.jobs().last(app.instanceId(), JobType.systemTest).get().hasFailed()); } @Test public void startTestsFailsIfDeploymentExpires() { - tester.newRun(JobType.systemTest); + app.newRun(JobType.systemTest); tester.runner().run(); - tester.configServer().convergeServices(instanceId, JobType.systemTest.zone(system())); - tester.configServer().convergeServices(testerId.id(), JobType.systemTest.zone(system())); + tester.configServer().convergeServices(app.instanceId(), JobType.systemTest.zone(system())); + tester.configServer().convergeServices(app.testerId().id(), JobType.systemTest.zone(system())); tester.runner().run(); - tester.applications().deactivate(instanceId, JobType.systemTest.zone(system())); + tester.applications().deactivate(app.instanceId(), JobType.systemTest.zone(system())); tester.runner().run(); - assertEquals(unfinished, tester.jobs().last(instanceId, JobType.systemTest).get().steps().get(Step.startTests)); + assertEquals(unfinished, tester.jobs().last(app.instanceId(), JobType.systemTest).get().steps().get(Step.startTests)); } @Test public void alternativeEndpointsAreDetected() { - tester.newRun(JobType.systemTest); + app.newRun(JobType.systemTest); tester.runner().run();; - tester.configServer().convergeServices(instanceId, JobType.systemTest.zone(system())); - tester.configServer().convergeServices(testerId.id(), JobType.systemTest.zone(system())); - assertEquals(unfinished, tester.jobs().last(instanceId, JobType.systemTest).get().steps().get(Step.installReal)); - assertEquals(unfinished, tester.jobs().last(instanceId, JobType.systemTest).get().steps().get(Step.installTester)); - - tester.controller().curator().writeRoutingPolicies(instanceId, Set.of(new RoutingPolicy(instanceId, + tester.configServer().convergeServices(app.instanceId(), JobType.systemTest.zone(system())); + tester.configServer().convergeServices(app.testerId().id(), JobType.systemTest.zone(system())); + assertEquals(unfinished, tester.jobs().last(app.instanceId(), JobType.systemTest).get().steps().get(Step.installReal)); + assertEquals(unfinished, tester.jobs().last(app.instanceId(), JobType.systemTest).get().steps().get(Step.installTester)); + + tester.controller().curator().writeRoutingPolicies(app.instanceId(), Set.of(new RoutingPolicy(app.instanceId(), + ClusterSpec.Id.from("default"), + JobType.systemTest.zone(system()), + HostName.from("host"), + Optional.empty(), + emptySet()))); + tester.controller().curator().writeRoutingPolicies(app.testerId().id(), Set.of(new RoutingPolicy(app.testerId().id(), ClusterSpec.Id.from("default"), JobType.systemTest.zone(system()), HostName.from("host"), Optional.empty(), emptySet()))); - tester.controller().curator().writeRoutingPolicies(testerId.id(), Set.of(new RoutingPolicy(testerId.id(), - ClusterSpec.Id.from("default"), - JobType.systemTest.zone(system()), - HostName.from("host"), - Optional.empty(), - emptySet()))); tester.runner().run();; - assertEquals(succeeded, tester.jobs().last(instanceId, JobType.systemTest).get().steps().get(Step.installReal)); - assertEquals(succeeded, tester.jobs().last(instanceId, JobType.systemTest).get().steps().get(Step.installTester)); + assertEquals(succeeded, tester.jobs().last(app.instanceId(), JobType.systemTest).get().steps().get(Step.installReal)); + assertEquals(succeeded, tester.jobs().last(app.instanceId(), JobType.systemTest).get().steps().get(Step.installTester)); } @Test public void testsFailIfTesterRestarts() { - RunId id = tester.startSystemTestTests(); + RunId id = app.startSystemTestTests(); tester.cloud().set(TesterCloud.Status.NOT_STARTED); tester.runner().run(); assertEquals(failed, tester.jobs().run(id).get().steps().get(Step.endTests)); @@ -241,7 +243,7 @@ public class InternalStepRunnerTest { @Test public void testsFailIfTestsFailRemotely() { - RunId id = tester.startSystemTestTests(); + RunId id = app.startSystemTestTests(); tester.cloud().add(new LogEntry(123, Instant.ofEpochMilli(321), error, "Failure!")); tester.cloud().set(TesterCloud.Status.FAILURE); @@ -255,7 +257,7 @@ public class InternalStepRunnerTest { @Test public void testsFailIfTestsErr() { - RunId id = tester.startSystemTestTests(); + RunId id = app.startSystemTestTests(); tester.cloud().add(new LogEntry(0, Instant.ofEpochMilli(123), error, "Error!")); tester.cloud().set(TesterCloud.Status.ERROR); @@ -269,13 +271,13 @@ public class InternalStepRunnerTest { @Test public void testsSucceedWhenTheyDoRemotely() { - RunId id = tester.startSystemTestTests(); + RunId id = app.startSystemTestTests(); tester.runner().run(); assertEquals(unfinished, tester.jobs().run(id).get().steps().get(Step.endTests)); - assertEquals(URI.create(tester.routing().endpoints(new DeploymentId(testerId.id(), JobType.systemTest.zone(system()))).get(0).endpoint()), + assertEquals(URI.create(tester.routing().endpoints(new DeploymentId(app.testerId().id(), JobType.systemTest.zone(system()))).get(0).endpoint()), tester.cloud().testerUrl()); Inspector configObject = SlimeUtils.jsonToSlime(tester.cloud().config()).get(); - assertEquals(instanceId.serializedForm(), configObject.field("application").asString()); + assertEquals(app.instanceId().serializedForm(), configObject.field("application").asString()); assertEquals(JobType.systemTest.zone(system()).value(), configObject.field("zone").asString()); assertEquals(system().value(), configObject.field("system").asString()); assertEquals(1, configObject.field("endpoints").children()); @@ -308,36 +310,36 @@ public class InternalStepRunnerTest { @Test public void deployToDev() { ZoneId zone = JobType.devUsEast1.zone(system()); - tester.jobs().deploy(instanceId, JobType.devUsEast1, Optional.empty(), applicationPackage); + tester.jobs().deploy(app.instanceId(), JobType.devUsEast1, Optional.empty(), applicationPackage); tester.runner().run(); - RunId id = tester.jobs().last(instanceId, JobType.devUsEast1).get().id(); + RunId id = tester.jobs().last(app.instanceId(), JobType.devUsEast1).get().id(); assertEquals(unfinished, tester.jobs().run(id).get().steps().get(Step.installReal)); Version version = new Version("7.8.9"); Future<?> concurrentDeployment = Executors.newSingleThreadExecutor().submit(() -> { - tester.jobs().deploy(instanceId, JobType.devUsEast1, Optional.of(version), applicationPackage); + tester.jobs().deploy(app.instanceId(), JobType.devUsEast1, Optional.of(version), applicationPackage); }); while ( ! concurrentDeployment.isDone()) tester.runner().run(); - assertEquals(id.number() + 1, tester.jobs().last(instanceId, JobType.devUsEast1).get().id().number()); + assertEquals(id.number() + 1, tester.jobs().last(app.instanceId(), JobType.devUsEast1).get().id().number()); ApplicationPackage otherPackage = new ApplicationPackageBuilder().region("us-central-1").build(); - tester.jobs().deploy(instanceId, JobType.perfUsEast3, Optional.empty(), otherPackage); + tester.jobs().deploy(app.instanceId(), JobType.perfUsEast3, Optional.empty(), otherPackage); tester.runner().run(); // Job run order determined by JobType enum order per application. - tester.configServer().convergeServices(instanceId, zone); - tester.setEndpoints(instanceId, zone); + tester.configServer().convergeServices(app.instanceId(), zone); + tester.setEndpoints(app.instanceId(), zone); assertEquals(unfinished, tester.jobs().run(id).get().steps().get(Step.installReal)); - assertEquals(applicationPackage.hash(), tester.configServer().application(instanceId, zone).get().applicationPackage().hash()); - assertEquals(otherPackage.hash(), tester.configServer().application(instanceId, JobType.perfUsEast3.zone(system())).get().applicationPackage().hash()); + assertEquals(applicationPackage.hash(), tester.configServer().application(app.instanceId(), zone).get().applicationPackage().hash()); + assertEquals(otherPackage.hash(), tester.configServer().application(app.instanceId(), JobType.perfUsEast3.zone(system())).get().applicationPackage().hash()); - tester.configServer().setVersion(instanceId, zone, version); + tester.configServer().setVersion(app.instanceId(), zone, version); tester.runner().run(); assertEquals(1, tester.jobs().active().size()); - assertEquals(version, tester.instance(instanceId).deployments().get(zone).version()); + assertEquals(version, tester.instance(app.instanceId()).deployments().get(zone).version()); try { - tester.jobs().deploy(instanceId, JobType.productionApNortheast1, Optional.empty(), applicationPackage); + tester.jobs().deploy(app.instanceId(), JobType.productionApNortheast1, Optional.empty(), applicationPackage); fail("Deployments outside dev should not be allowed."); } catch (IllegalArgumentException expected) { } @@ -345,10 +347,10 @@ public class InternalStepRunnerTest { @Test public void notificationIsSent() { - tester.startSystemTestTests(); + app.startSystemTestTests(); tester.cloud().set(TesterCloud.Status.NOT_STARTED); tester.runner().run(); - MockMailer mailer = ((MockMailer) tester.controller().serviceRegistry().mailer()); + MockMailer mailer = tester.controllerTester().serviceRegistry().mailer(); assertEquals(1, mailer.inbox("a@b").size()); assertEquals("Vespa application tenant.application: System test failing due to system error", mailer.inbox("a@b").get(0).subject()); @@ -359,7 +361,7 @@ public class InternalStepRunnerTest { @Test public void vespaLogIsCopied() { - RunId id = tester.startSystemTestTests(); + RunId id = app.startSystemTestTests(); tester.cloud().set(TesterCloud.Status.ERROR); tester.configServer().setLogStream(vespaLog); long lastId = tester.jobs().details(id).get().lastId().getAsLong(); @@ -384,11 +386,11 @@ public class InternalStepRunnerTest { public void certificateTimeoutAbortsJob() { tester.controllerTester().zoneRegistry().setSystemName(SystemName.PublicCd); tester.controllerTester().zoneRegistry().setZones(ZoneApiMock.fromId("prod.aws-us-east-1c")); - RunId id = tester.startSystemTestTests(); + RunId id = app.startSystemTestTests(); List<X509Certificate> trusted = new ArrayList<>(publicCdApplicationPackage.trustedCertificates()); trusted.add(tester.jobs().run(id).get().testerCertificate().get()); - assertEquals(trusted, tester.configServer().application(instanceId, id.type().zone(system())).get().applicationPackage().trustedCertificates()); + assertEquals(trusted, tester.configServer().application(app.instanceId(), id.type().zone(system())).get().applicationPackage().trustedCertificates()); tester.clock().advance(InternalStepRunner.certificateTimeout.plus(Duration.ofSeconds(1))); tester.runner().run(); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java index d915fe06720..3c33051e98b 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java @@ -36,7 +36,11 @@ public class ApplicationStoreMock implements ApplicationStore { @Override public byte[] get(TenantName tenant, ApplicationName application, ApplicationVersion applicationVersion) { - return requireNonNull(store.get(appId(tenant, application)).get(applicationVersion)); + byte[] bytes = store.get(appId(tenant, application)).get(applicationVersion); + if (bytes == null) + throw new IllegalArgumentException("No application package found for " + tenant + "." + application + + " with version " + applicationVersion.id()); + return bytes; } @Override 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 a3c06c75a26..eced161cebc 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,50 +1,33 @@ // Copyright 2019 Oath Inc. 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.google.inject.Inject; +import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.component.AbstractComponent; +import com.yahoo.config.provision.SystemName; import com.yahoo.test.ManualClock; -import com.yahoo.vespa.hosted.controller.api.integration.BuildService; -import com.yahoo.vespa.hosted.controller.api.integration.RunDataStore; import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry; -import com.yahoo.vespa.hosted.controller.api.integration.aws.AwsEventFetcher; import com.yahoo.vespa.hosted.controller.api.integration.aws.MockAwsEventFetcher; import com.yahoo.vespa.hosted.controller.api.integration.certificates.ApplicationCertificateMock; -import com.yahoo.vespa.hosted.controller.api.integration.certificates.ApplicationCertificateProvider; import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationStore; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.ArtifactRepository; import com.yahoo.vespa.hosted.controller.api.integration.dns.MemoryNameService; -import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService; -import com.yahoo.vespa.hosted.controller.api.integration.entity.EntityService; import com.yahoo.vespa.hosted.controller.api.integration.entity.MemoryEntityService; -import com.yahoo.vespa.hosted.controller.api.integration.organization.Billing; -import com.yahoo.vespa.hosted.controller.api.integration.organization.ContactRetriever; -import com.yahoo.vespa.hosted.controller.api.integration.organization.DeploymentIssues; -import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueHandler; -import com.yahoo.vespa.hosted.controller.api.integration.organization.Mailer; import com.yahoo.vespa.hosted.controller.api.integration.organization.MockBilling; import com.yahoo.vespa.hosted.controller.api.integration.organization.MockContactRetriever; import com.yahoo.vespa.hosted.controller.api.integration.organization.MockIssueHandler; -import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues; -import com.yahoo.vespa.hosted.controller.api.integration.resource.CostReportConsumer; import com.yahoo.vespa.hosted.controller.api.integration.resource.CostReportConsumerMock; -import com.yahoo.vespa.hosted.controller.api.integration.resource.MeteringClient; import com.yahoo.vespa.hosted.controller.api.integration.resource.MockTenantCost; -import com.yahoo.vespa.hosted.controller.api.integration.resource.TenantCost; import com.yahoo.vespa.hosted.controller.api.integration.routing.GlobalRoutingService; import com.yahoo.vespa.hosted.controller.api.integration.routing.MemoryGlobalRoutingService; import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingGenerator; import com.yahoo.vespa.hosted.controller.api.integration.routing.RoutingGeneratorMock; import com.yahoo.vespa.hosted.controller.api.integration.stubs.DummyOwnershipIssues; import com.yahoo.vespa.hosted.controller.api.integration.stubs.LoggingDeploymentIssues; -import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockBuildService; import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer; import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMeteringClient; import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockRunDataStore; import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockTesterCloud; -import java.time.Clock; - /** * A mock implementation of a {@link ServiceRegistry} for testing purposes. * @@ -53,8 +36,8 @@ import java.time.Clock; public class ServiceRegistryMock extends AbstractComponent implements ServiceRegistry { private final ManualClock clock = new ManualClock(); - private final ZoneRegistryMock zoneRegistryMock = new ZoneRegistryMock(); - private final ConfigServerMock configServerMock = new ConfigServerMock(zoneRegistryMock); + private final ZoneRegistryMock zoneRegistryMock; + private final ConfigServerMock configServerMock; private final MemoryNameService memoryNameService = new MemoryNameService(); private final MemoryGlobalRoutingService memoryGlobalRoutingService = new MemoryGlobalRoutingService(); private final RoutingGeneratorMock routingGeneratorMock = new RoutingGeneratorMock(RoutingGeneratorMock.TEST_ENDPOINTS); @@ -73,9 +56,22 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg private final MockTesterCloud mockTesterCloud = new MockTesterCloud(); private final ApplicationStoreMock applicationStoreMock = new ApplicationStoreMock(); private final MockRunDataStore mockRunDataStore = new MockRunDataStore(); - private final MockBuildService mockBuildService = new MockBuildService(); private final MockTenantCost mockTenantCost = new MockTenantCost(); + public ServiceRegistryMock(SystemName system) { + this.zoneRegistryMock = new ZoneRegistryMock(system); + this.configServerMock = new ConfigServerMock(zoneRegistryMock); + } + + @Inject + public ServiceRegistryMock(ConfigserverConfig config) { + this(SystemName.from(config.system())); + } + + public ServiceRegistryMock() { + this(SystemName.main); + } + @Override public ConfigServer configServer() { return configServerMock; @@ -97,62 +93,62 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg } @Override - public Mailer mailer() { + public MockMailer mailer() { return mockMailer; } @Override - public ApplicationCertificateProvider applicationCertificateProvider() { + public ApplicationCertificateMock applicationCertificateProvider() { return applicationCertificateMock; } @Override - public MeteringClient meteringService() { + public MockMeteringClient meteringService() { return mockMeteringClient; } @Override - public ContactRetriever contactRetriever() { + public MockContactRetriever contactRetriever() { return mockContactRetriever; } @Override - public IssueHandler issueHandler() { + public MockIssueHandler issueHandler() { return mockIssueHandler; } @Override - public OwnershipIssues ownershipIssues() { + public DummyOwnershipIssues ownershipIssues() { return dummyOwnershipIssues; } @Override - public DeploymentIssues deploymentIssues() { + public LoggingDeploymentIssues deploymentIssues() { return loggingDeploymentIssues; } @Override - public EntityService entityService() { + public MemoryEntityService entityService() { return memoryEntityService; } @Override - public CostReportConsumer costReportConsumer() { + public CostReportConsumerMock costReportConsumer() { return costReportConsumerMock; } @Override - public Billing billingService() { + public MockBilling billingService() { return mockBilling; } @Override - public AwsEventFetcher eventFetcherService() { + public MockAwsEventFetcher eventFetcherService() { return mockAwsEventFetcher; } @Override - public ArtifactRepository artifactRepository() { + public ArtifactRepositoryMock artifactRepository() { return artifactRepositoryMock; } @@ -162,29 +158,25 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg } @Override - public ApplicationStore applicationStore() { + public ApplicationStoreMock applicationStore() { return applicationStoreMock; } @Override - public RunDataStore runDataStore() { + public MockRunDataStore runDataStore() { return mockRunDataStore; } @Override - public BuildService buildService() { - return mockBuildService; - } - - @Override - public NameService nameService() { + public MemoryNameService nameService() { return memoryNameService; } @Override - public TenantCost tenantCost() { return mockTenantCost;} + public MockTenantCost tenantCost() { return mockTenantCost;} - public ZoneRegistryMock zoneRegistryMock() { + @Override + public ZoneRegistryMock zoneRegistry() { return zoneRegistryMock; } @@ -212,10 +204,6 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg return artifactRepositoryMock; } - public MockBuildService buildServiceMock() { - return mockBuildService; - } - public ApplicationCertificateMock applicationCertificateMock() { return applicationCertificateMock; } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneRegistryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneRegistryMock.java index 32bbf3ceb9b..07db06164c6 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneRegistryMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneRegistryMock.java @@ -1,7 +1,6 @@ // Copyright 2018 Yahoo Holdings. 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.google.common.collect.ImmutableList; import com.google.inject.Inject; import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.component.AbstractComponent; @@ -24,8 +23,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry; import java.net.URI; import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -38,20 +35,11 @@ public class ZoneRegistryMock extends AbstractComponent implements ZoneRegistry private final Map<ZoneId, Duration> deploymentTimeToLive = new HashMap<>(); private final Map<Environment, RegionName> defaultRegionForEnvironment = new HashMap<>(); - private List<ZoneApi> zones = new ArrayList<>(); + private List<ZoneApi> zones = List.of(); private SystemName system; private UpgradePolicy upgradePolicy = null; private Map<CloudName, UpgradePolicy> osUpgradePolicies = new HashMap<>(); - @Inject - public ZoneRegistryMock(ConfigserverConfig config) { - this(SystemName.from(config.system())); - } - - public ZoneRegistryMock() { - this(SystemName.main); - } - /** * This sets the default list of zones contained in this. If your test need a particular set of zones, use * {@link #setZones(List)} instead of changing the default set.} @@ -136,7 +124,7 @@ public class ZoneRegistryMock extends AbstractComponent implements ZoneRegistry @Override public List<UpgradePolicy> osUpgradePolicies() { - return ImmutableList.copyOf(osUpgradePolicies.values()); + return List.copyOf(osUpgradePolicies.values()); } @Override @@ -176,7 +164,9 @@ public class ZoneRegistryMock extends AbstractComponent implements ZoneRegistry @Override public List<URI> getConfigServerUris(ZoneId zoneId) { - return Collections.singletonList(URI.create(String.format("https://cfg.%s.test:4443/", zoneId.value()))); + return List.of( + URI.create(String.format("https://cfg1.%s.test:4443/", zoneId.value())), + URI.create(String.format("https://cfg2.%s.test:4443/", zoneId.value()))); } @Override @@ -186,11 +176,9 @@ public class ZoneRegistryMock extends AbstractComponent implements ZoneRegistry @Override public List<URI> getConfigServerApiUris(ZoneId zoneId) { - List<URI> uris = new ArrayList<URI>(); - uris.add(URI.create(String.format("https://cfg.%s.test:4443/", zoneId.value()))); - uris.add(URI.create(String.format("https://cfg.%s.test.vip:4443/", zoneId.value()))); - - return uris; + return List.of( + URI.create(String.format("https://cfg.%s.test:4443/", zoneId.value())), + URI.create(String.format("https://cfg.%s.test.vip:4443/", zoneId.value()))); } @Override diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java index 88d1626b834..8997f34fb98 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java @@ -2,15 +2,12 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.config.provision.InstanceName; -import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.LockedTenant; import com.yahoo.vespa.hosted.controller.api.integration.organization.ApplicationSummary; import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact; import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues; import com.yahoo.vespa.hosted.controller.api.integration.organization.User; -import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; -import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext; import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester; import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb; import com.yahoo.vespa.hosted.controller.tenant.UserTenant; @@ -20,7 +17,6 @@ import org.junit.Test; import java.time.Duration; import java.util.List; import java.util.Optional; -import java.util.function.Supplier; import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTester.appId; import static org.junit.Assert.assertEquals; @@ -46,20 +42,18 @@ public class ApplicationOwnershipConfirmerTest { @Test public void testConfirmation() { Optional<Contact> contact = Optional.of(tester.controllerTester().serviceRegistry().contactRetrieverMock().contact()); + var propertyApp = tester.newDeploymentContext(); tester.controller().tenants().lockOrThrow(appId.tenant(), LockedTenant.Athenz.class, tenant -> tester.controller().tenants().store(tenant.with(contact.get()))); - Supplier<Application> propertyApp = tester::application; - tester.deployNewSubmission(tester.newSubmission()); + propertyApp.submit().deploy(); UserTenant user = UserTenant.create("by-user", contact); tester.controller().tenants().createUser(user); - tester.createApplication(user.name().value(), "application", "default"); - TenantAndApplicationId userAppId = TenantAndApplicationId.from("by-user", "application"); - Supplier<Application> userApp = () -> tester.controller().applications().requireApplication(userAppId); - tester.deployNewSubmission(userAppId, tester.newSubmission(userAppId, DeploymentContext.applicationPackage)); + var userApp = tester.newDeploymentContext("by-user", "application", "default"); + userApp.submit().deploy(); - assertFalse("No issue is initially stored for a new application.", propertyApp.get().ownershipIssueId().isPresent()); - assertFalse("No issue is initially stored for a new application.", userApp.get().ownershipIssueId().isPresent()); + assertFalse("No issue is initially stored for a new application.", propertyApp.application().ownershipIssueId().isPresent()); + assertFalse("No issue is initially stored for a new application.", userApp.application().ownershipIssueId().isPresent()); assertFalse("No escalation has been attempted for a new application", issues.escalatedToContact || issues.escalatedToTerminator); // Set response from the issue mock, which will be obtained by the maintainer on issue filing. @@ -67,14 +61,14 @@ public class ApplicationOwnershipConfirmerTest { issues.response = issueId; confirmer.maintain(); - assertFalse("No issue is stored for an application newer than 3 months.", propertyApp.get().ownershipIssueId().isPresent()); - assertFalse("No issue is stored for an application newer than 3 months.", userApp.get().ownershipIssueId().isPresent()); + assertFalse("No issue is stored for an application newer than 3 months.", propertyApp.application().ownershipIssueId().isPresent()); + assertFalse("No issue is stored for an application newer than 3 months.", userApp.application().ownershipIssueId().isPresent()); tester.clock().advance(Duration.ofDays(91)); confirmer.maintain(); - assertEquals("Confirmation issue has been filed for property owned application.", issueId, propertyApp.get().ownershipIssueId()); - assertEquals("Confirmation issue has been filed for user owned application.", issueId, userApp.get().ownershipIssueId()); + assertEquals("Confirmation issue has been filed for property owned application.", issueId, propertyApp.application().ownershipIssueId()); + assertEquals("Confirmation issue has been filed for user owned application.", issueId, userApp.application().ownershipIssueId()); assertTrue(issues.escalatedToTerminator); assertTrue("Both applications have had their responses ensured.", issues.escalatedToContact); @@ -82,14 +76,14 @@ public class ApplicationOwnershipConfirmerTest { issues.response = Optional.empty(); confirmer.maintain(); - assertEquals("Confirmation issue reference is not updated when no issue id is returned.", issueId, propertyApp.get().ownershipIssueId()); - assertEquals("Confirmation issue reference is not updated when no issue id is returned.", issueId, userApp.get().ownershipIssueId()); + assertEquals("Confirmation issue reference is not updated when no issue id is returned.", issueId, propertyApp.application().ownershipIssueId()); + assertEquals("Confirmation issue reference is not updated when no issue id is returned.", issueId, userApp.application().ownershipIssueId()); // The user deletes all production deployments — see that the issue is forgotten. - assertEquals("Confirmation issue for user is still open.", issueId, userApp.get().ownershipIssueId()); - userApp.get().productionDeployments().values().stream().flatMap(List::stream) - .forEach(deployment -> tester.controller().applications().deactivate(userAppId.defaultInstance(), deployment.zone())); - assertTrue("No production deployments are listed for user.", userApp.get().require(InstanceName.defaultName()).productionDeployments().isEmpty()); + assertEquals("Confirmation issue for user is still open.", issueId, userApp.application().ownershipIssueId()); + userApp.application().productionDeployments().values().stream().flatMap(List::stream) + .forEach(deployment -> tester.controller().applications().deactivate(userApp.instanceId(), deployment.zone())); + assertTrue("No production deployments are listed for user.", userApp.application().require(InstanceName.defaultName()).productionDeployments().isEmpty()); confirmer.maintain(); // Time has passed, and a new confirmation issue is in order for the property which is still in production. @@ -97,13 +91,13 @@ public class ApplicationOwnershipConfirmerTest { issues.response = issueId2; confirmer.maintain(); - assertEquals("A new confirmation issue id is stored when something is returned to the maintainer.", issueId2, propertyApp.get().ownershipIssueId()); - assertEquals("Confirmation issue for application without production deployments has not been filed.", issueId, userApp.get().ownershipIssueId()); + assertEquals("A new confirmation issue id is stored when something is returned to the maintainer.", issueId2, propertyApp.application().ownershipIssueId()); + assertEquals("Confirmation issue for application without production deployments has not been filed.", issueId, userApp.application().ownershipIssueId()); - assertFalse("No owner is stored for application", propertyApp.get().owner().isPresent()); + assertFalse("No owner is stored for application", propertyApp.application().owner().isPresent()); issues.owner = Optional.of(User.from("username")); confirmer.maintain(); - assertEquals("Owner has been added to application", propertyApp.get().owner().get().username(), "username"); + assertEquals("Owner has been added to application", propertyApp.application().owner().get().username(), "username"); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java index 4df1336ac1f..258dad3b6d6 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java @@ -35,35 +35,32 @@ public class DeploymentExpirerTest { ); DeploymentExpirer expirer = new DeploymentExpirer(tester.controller(), Duration.ofDays(10), new JobControl(new MockCuratorDb())); - Application devApp = tester.createApplication("tenant1", "app1", "default"); - Application prodApp = tester.createApplication("tenant2", "app2", "default"); + var devApp = tester.newDeploymentContext("tenant1", "app1", "default"); + var prodApp = tester.newDeploymentContext("tenant2", "app2", "default"); ApplicationPackage appPackage = new ApplicationPackageBuilder() .region("us-west-1") .build(); - Instance devInstance = tester.instance(devApp.id().defaultInstance()); - Instance prodInstance = tester.instance(prodApp.id().defaultInstance()); - // Deploy dev - tester.runJob(devInstance.id(), JobType.devUsEast1, appPackage); + devApp.runJob(JobType.devUsEast1, appPackage); // Deploy prod - tester.deployNewSubmission(prodApp.id(), tester.newSubmission(prodApp.id(), appPackage)); + prodApp.submit(appPackage).deploy(); - assertEquals(1, permanentDeployments(devInstance).size()); - assertEquals(1, permanentDeployments(prodInstance).size()); + assertEquals(1, permanentDeployments(devApp.instance()).size()); + assertEquals(1, permanentDeployments(prodApp.instance()).size()); // Not expired at first expirer.maintain(); - assertEquals(1, permanentDeployments(devInstance).size()); - assertEquals(1, permanentDeployments(prodInstance).size()); + assertEquals(1, permanentDeployments(devApp.instance()).size()); + assertEquals(1, permanentDeployments(prodApp.instance()).size()); // The dev application is removed tester.clock().advance(Duration.ofDays(15)); expirer.maintain(); - assertEquals(0, permanentDeployments(devInstance).size()); - assertEquals(1, permanentDeployments(prodInstance).size()); + assertEquals(0, permanentDeployments(devApp.instance()).size()); + assertEquals(1, permanentDeployments(prodApp.instance()).size()); } private List<Deployment> permanentDeployments(Instance instance) { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java index 070171ad399..0afe7377d40 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java @@ -13,6 +13,7 @@ import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; 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.persistence.MockCuratorDb; import com.yahoo.vespa.hosted.controller.versions.VespaVersion; @@ -65,33 +66,28 @@ public class DeploymentIssueReporterTest { tester.controllerTester().upgradeSystem(Version.fromString("6.2")); // Create and deploy one application for each of three tenants. - Application app1 = tester.createApplication("application1", "tenant1", "default"); - Application app2 = tester.createApplication("application2", "tenant2", "default"); - Application app3 = tester.createApplication("application3", "tenant3", "default"); + var app1 = tester.newDeploymentContext("application1", "tenant1", "default"); + var app2 = tester.newDeploymentContext("application2", "tenant2", "default"); + var app3 = tester.newDeploymentContext("application3", "tenant3", "default"); Contact contact = tester.controllerTester().serviceRegistry().contactRetrieverMock().contact(); - tester.controller().tenants().lockOrThrow(app1.id().tenant(), LockedTenant.Athenz.class, tenant -> + tester.controller().tenants().lockOrThrow(app1.instanceId().tenant(), LockedTenant.Athenz.class, tenant -> tester.controller().tenants().store(tenant.with(contact))); - tester.controller().tenants().lockOrThrow(app2.id().tenant(), LockedTenant.Athenz.class, tenant -> + tester.controller().tenants().lockOrThrow(app2.instanceId().tenant(), LockedTenant.Athenz.class, tenant -> tester.controller().tenants().store(tenant.with(contact))); - tester.controller().tenants().lockOrThrow(app3.id().tenant(), LockedTenant.Athenz.class, tenant -> + tester.controller().tenants().lockOrThrow(app3.instanceId().tenant(), LockedTenant.Athenz.class, tenant -> tester.controller().tenants().store(tenant.with(contact))); // NOTE: All maintenance should be idempotent within a small enough time interval, so maintain is called twice in succession throughout. // app 1 fails staging tests. - tester.newSubmission(app1.id(), applicationPackage); - tester.runJob(app1.id().defaultInstance(), systemTest); - tester.timeOutConvergence(app1.id().defaultInstance(), stagingTest); + app1.submit(applicationPackage).runJob(systemTest).timeOutConvergence(stagingTest); // app2 is successful, but will fail later. - tester.deployNewSubmission(app2.id(), tester.newSubmission(app2.id(), applicationPackage)); + app2.submit(applicationPackage).deploy(); // app 3 fails a production job. - tester.newSubmission(app3.id(), applicationPackage); - tester.runJob(app3.id().defaultInstance(), systemTest); - tester.runJob(app3.id().defaultInstance(), stagingTest); - tester.failDeployment(app3.id().defaultInstance(), productionUsWest1); + app3.submit(applicationPackage).runJob(systemTest).runJob(stagingTest).failDeployment(productionUsWest1); reporter.maintain(); reporter.maintain(); @@ -103,53 +99,52 @@ public class DeploymentIssueReporterTest { reporter.maintain(); reporter.maintain(); - assertTrue("One issue is produced for app1.", issues.isOpenFor(app1.id())); - assertFalse("No issues are produced for app2.", issues.isOpenFor(app2.id())); - assertTrue("One issue is produced for app3.", issues.isOpenFor(app3.id())); + assertTrue("One issue is produced for app1.", issues.isOpenFor(app1.application().id())); + assertFalse("No issues are produced for app2.", issues.isOpenFor(app2.application().id())); + assertTrue("One issue is produced for app3.", issues.isOpenFor(app3.application().id())); // app3 closes their issue prematurely; see that it is refiled. - issues.closeFor(app3.id()); - assertFalse("No issue is open for app3.", issues.isOpenFor(app3.id())); + issues.closeFor(app3.application().id()); + assertFalse("No issue is open for app3.", issues.isOpenFor(app3.application().id())); reporter.maintain(); reporter.maintain(); - assertTrue("Issue is re-filed for app3.", issues.isOpenFor(app3.id())); + assertTrue("Issue is re-filed for app3.", issues.isOpenFor(app3.application().id())); // Some time passes; tenant1 leaves her issue unattended, while tenant3 starts work and updates the issue. tester.clock().advance(maxInactivity.plus(maxFailureAge)); - issues.touchFor(app3.id()); + issues.touchFor(app3.application().id()); reporter.maintain(); reporter.maintain(); - assertEquals("The issue for app1 is escalated once.", 1, issues.escalationLevelFor(app1.id())); + assertEquals("The issue for app1 is escalated once.", 1, issues.escalationLevelFor(app1.application().id())); // app3 fixes their problems, but the ticket for app3 is left open; see the resolved ticket is not escalated when another escalation period has passed. - tester.runJob(app3.id().defaultInstance(), productionUsWest1); + app3.runJob(productionUsWest1); tester.clock().advance(maxInactivity.plus(Duration.ofDays(1))); reporter.maintain(); reporter.maintain(); assertFalse("We no longer have a platform issue.", issues.platformIssue()); - assertEquals("The issue for app1 is escalated once more.", 2, issues.escalationLevelFor(app1.id())); - assertEquals("The issue for app3 is not escalated.", 0, issues.escalationLevelFor(app3.id())); + assertEquals("The issue for app1 is escalated once more.", 2, issues.escalationLevelFor(app1.application().id())); + assertEquals("The issue for app3 is not escalated.", 0, issues.escalationLevelFor(app3.application().id())); // app3 now has a new failure past max failure age; see that a new issue is filed. - tester.newSubmission(app3.id(), applicationPackage); - tester.failDeployment(app3.id().defaultInstance(), systemTest); + app3.submit(applicationPackage).failDeployment(systemTest); tester.clock().advance(maxInactivity.plus(maxFailureAge)); reporter.maintain(); reporter.maintain(); - assertTrue("A new issue is filed for app3.", issues.isOpenFor(app3.id())); + assertTrue("A new issue is filed for app3.", issues.isOpenFor(app3.application().id())); - // App2 is changed to be a canary - tester.deployNewSubmission(app2.id(), tester.newSubmission(app2.id(), canaryPackage)); - assertEquals(canary, tester.applications().requireApplication(app2.id()).deploymentSpec().requireInstance("default").upgradePolicy()); - assertEquals(Change.empty(), tester.applications().requireApplication(app2.id()).change()); + // app2 is changed to be a canary + app2.submit(canaryPackage).deploy(); + assertEquals(canary, app2.application().deploymentSpec().requireInstance("default").upgradePolicy()); + assertEquals(Change.empty(), app2.application().change()); // Bump system version to upgrade canary app2. Version version = Version.fromString("6.3"); @@ -157,7 +152,7 @@ public class DeploymentIssueReporterTest { tester.upgrader().maintain(); assertEquals(version, tester.controller().versionStatus().systemVersion().get().versionNumber()); - tester.timeOutUpgrade(app2.id().defaultInstance(), systemTest); + app2.timeOutUpgrade(systemTest); tester.controllerTester().upgradeSystem(version); assertEquals(VespaVersion.Confidence.broken, tester.controller().versionStatus().systemVersion().get().confidence()); @@ -169,7 +164,7 @@ public class DeploymentIssueReporterTest { reporter.maintain(); reporter.maintain(); assertTrue("We get a platform issue when confidence is broken", issues.platformIssue()); - assertFalse("No deployment issue is filed for app2, which has a version upgrade failure.", issues.isOpenFor(app2.id())); + assertFalse("No deployment issue is filed for app2, which has a version upgrade failure.", issues.isOpenFor(app2.application().id())); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java index 4e4fbe00bb7..06a815819f4 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java @@ -35,7 +35,7 @@ public class DeploymentMetricsMaintainerTest { @Test public void updates_metrics() { - var application = tester.deploymentContext(); + var application = tester.newDeploymentContext(); application.runJob(JobType.devUsEast1, new ApplicationPackage(new byte[0]), Version.fromString("7.1")); DeploymentMetricsMaintainer maintainer = maintainer(tester.controller()); 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 0f6aec804e2..f7d451ab931 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 @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; @@ -189,7 +190,7 @@ public class JobRunnerTest { // Start a third run, then unregister and wait for data to be deleted. jobs.start(id, systemTest, versions); - jobs.unregister(appId); + tester.applications().deleteInstance(id); runner.maintain(); assertFalse(jobs.last(id, systemTest).isPresent()); assertTrue(jobs.runs(id, systemTest).isEmpty()); @@ -242,24 +243,74 @@ public class JobRunnerTest { inThreadExecutor(), (id, step) -> Optional.of(running)); TenantAndApplicationId appId = tester.createApplication("tenant", "real", "default").id(); - ApplicationId id = appId.defaultInstance(); + ApplicationId instanceId = appId.defaultInstance(); + JobId jobId = new JobId(instanceId, systemTest); jobs.submit(appId, versions.targetApplication().source().get(), "a@b", 2, applicationPackage, new byte[0]); + assertFalse(jobs.lastSuccess(jobId).isPresent()); for (int i = 0; i < jobs.historyLength(); i++) { - jobs.start(id, systemTest, versions); + jobs.start(instanceId, systemTest, versions); runner.run(); } - assertEquals(256, jobs.runs(id, systemTest).size()); - assertTrue(jobs.details(new RunId(id, systemTest, 1)).isPresent()); + assertEquals(256, jobs.runs(jobId).size()); + assertTrue(jobs.details(new RunId(instanceId, systemTest, 1)).isPresent()); - jobs.start(id, systemTest, versions); + jobs.start(instanceId, systemTest, versions); runner.run(); - assertEquals(256, jobs.runs(id, systemTest).size()); - assertEquals(2, jobs.runs(id, systemTest).keySet().iterator().next().number()); - assertFalse(jobs.details(new RunId(id, systemTest, 1)).isPresent()); - assertTrue(jobs.details(new RunId(id, systemTest, 257)).isPresent()); + assertEquals(256, jobs.runs(jobId).size()); + assertEquals(2, jobs.runs(jobId).keySet().iterator().next().number()); + assertFalse(jobs.details(new RunId(instanceId, systemTest, 1)).isPresent()); + assertTrue(jobs.details(new RunId(instanceId, systemTest, 257)).isPresent()); + + JobRunner failureRunner = new JobRunner(tester.controller(), Duration.ofDays(1), new JobControl(tester.controller().curator()), + inThreadExecutor(), (id, step) -> Optional.of(error)); + + // Make all but the oldest of the 256 jobs a failure. + for (int i = 0; i < jobs.historyLength() - 1; i++) { + jobs.start(instanceId, systemTest, versions); + failureRunner.run(); + } + assertEquals(256, jobs.runs(jobId).size()); + assertEquals(257, jobs.runs(jobId).keySet().iterator().next().number()); + assertEquals(257, jobs.lastSuccess(jobId).get().id().number()); + assertEquals(258, jobs.firstFailing(jobId).get().id().number()); + + // Oldest success is kept even though it would normally overflow. + jobs.start(instanceId, systemTest, versions); + failureRunner.run(); + assertEquals(257, jobs.runs(jobId).size()); + assertEquals(257, jobs.runs(jobId).keySet().iterator().next().number()); + assertEquals(257, jobs.lastSuccess(jobId).get().id().number()); + assertEquals(258, jobs.firstFailing(jobId).get().id().number()); + + // First failure after the last success is also kept. + jobs.start(instanceId, systemTest, versions); + failureRunner.run(); + assertEquals(258, jobs.runs(jobId).size()); + assertEquals(257, jobs.runs(jobId).keySet().iterator().next().number()); + assertEquals(258, jobs.runs(jobId).keySet().stream().skip(1).iterator().next().number()); + assertEquals(257, jobs.lastSuccess(jobId).get().id().number()); + assertEquals(258, jobs.firstFailing(jobId).get().id().number()); + + // No other jobs are kept with repeated failures. + jobs.start(instanceId, systemTest, versions); + failureRunner.run(); + assertEquals(258, jobs.runs(jobId).size()); + assertEquals(257, jobs.runs(jobId).keySet().iterator().next().number()); + assertEquals(258, jobs.runs(jobId).keySet().stream().skip(1).iterator().next().number()); + assertEquals(260, jobs.runs(jobId).keySet().stream().skip(2).iterator().next().number()); + assertEquals(257, jobs.lastSuccess(jobId).get().id().number()); + assertEquals(258, jobs.firstFailing(jobId).get().id().number()); + + // history length returns to 256 when a new success is recorded. + jobs.start(instanceId, systemTest, versions); + runner.run(); + assertEquals(256, jobs.runs(jobId).size()); + assertEquals(261, jobs.runs(jobId).keySet().iterator().next().number()); + assertEquals(516, jobs.lastSuccess(jobId).get().id().number()); + assertFalse(jobs.firstFailing(jobId).isPresent()); } @Test diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java index 31f55be92a9..7c87cbf3610 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java @@ -78,7 +78,7 @@ public class MetricsReporterTest { MetricsReporter reporter = createReporter(tester.controller()); - var context = tester.deploymentContext() + var context = tester.newDeploymentContext() .submit(applicationPackage) .deploy(); reporter.maintain(); @@ -122,7 +122,7 @@ public class MetricsReporterTest { .build(); MetricsReporter reporter = createReporter(tester.controller()); - var context = tester.deploymentContext(); + var context = tester.newDeploymentContext(); // Initial deployment without failures context.submit(applicationPackage).deploy(); @@ -174,7 +174,7 @@ public class MetricsReporterTest { .region("us-east-3") .build(); MetricsReporter reporter = createReporter(tester.controller()); - var context = tester.deploymentContext(); + var context = tester.newDeploymentContext(); tester.configServer().generateWarnings(context.deploymentIdIn(ZoneId.from("prod", "us-west-1")), 3); tester.configServer().generateWarnings(context.deploymentIdIn(ZoneId.from("prod", "us-west-1")), 4); context.submit(applicationPackage).deploy(); @@ -186,7 +186,7 @@ public class MetricsReporterTest { public void build_time_reporting() { var tester = new DeploymentTester(); var applicationPackage = new ApplicationPackageBuilder().region("us-west-1").build(); - var context = tester.deploymentContext() + var context = tester.newDeploymentContext() .submit(applicationPackage) .deploy(); assertEquals(1000, context.lastSubmission().get().buildTime().get().toEpochMilli()); @@ -207,7 +207,7 @@ public class MetricsReporterTest { .region("us-east-3") .build(); MetricsReporter reporter = createReporter(tester.controller()); - var context = tester.deploymentContext() + var context = tester.newDeploymentContext() .deferDnsUpdates(); reporter.maintain(); assertEquals("Queue is empty initially", 0, metrics.getMetric(MetricsReporter.NAME_SERVICE_REQUESTS_QUEUED).intValue()); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java index 0be873f80ed..11c77304dd0 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java @@ -37,45 +37,40 @@ public class OutstandingChangeDeployerTest { .region("us-west-1") .build(); - Application app1 = tester.createApplication("tenant", "app1", "default"); - tester.deployNewSubmission(app1.id(), tester.newSubmission(app1.id(), applicationPackage)); + var app1 = tester.newDeploymentContext("tenant", "app1", "default").submit(applicationPackage).deploy(); Version version = new Version(6, 2); - tester.deploymentTrigger().triggerChange(app1.id(), Change.of(version)); + tester.deploymentTrigger().triggerChange(app1.application().id(), Change.of(version)); tester.deploymentTrigger().triggerReadyJobs(); - assertEquals(Change.of(version), tester.application(app1.id()).change()); - assertFalse(tester.application(app1.id()).outstandingChange().hasTargets()); + assertEquals(Change.of(version), app1.application().change()); + assertFalse(app1.application().outstandingChange().hasTargets()); - assertEquals(1, tester.application(app1.id()).latestVersion().get().buildNumber().getAsLong()); - tester.newSubmission(app1.id(), applicationPackage, new SourceRevision("repository1", "master", "cafed00d")); + assertEquals(1, app1.application().latestVersion().get().buildNumber().getAsLong()); + app1.submit(applicationPackage, new SourceRevision("repository1", "master", "cafed00d")); - ApplicationId instanceId = app1.id().defaultInstance(); - assertTrue(tester.application(app1.id()).outstandingChange().hasTargets()); - assertEquals("1.0.2-cafed00d", tester.application(app1.id()).outstandingChange().application().get().id()); - tester.assertRunning(instanceId, JobType.systemTest); - tester.assertRunning(instanceId, JobType.stagingTest); + assertTrue(app1.application().outstandingChange().hasTargets()); + assertEquals("1.0.2-cafed00d", app1.application().outstandingChange().application().get().id()); + app1.assertRunning(JobType.systemTest); + app1.assertRunning(JobType.stagingTest); assertEquals(2, tester.jobs().active().size()); deployer.maintain(); - tester.deploymentTrigger().triggerReadyJobs(); + tester.triggerJobs(); assertEquals("No effect as job is in progress", 2, tester.jobs().active().size()); - assertEquals("1.0.2-cafed00d", tester.application(app1.id()).outstandingChange().application().get().id()); + assertEquals("1.0.2-cafed00d", app1.application().outstandingChange().application().get().id()); - tester.runJob(instanceId, JobType.systemTest); - tester.runJob(instanceId, JobType.stagingTest); - tester.runJob(instanceId, JobType.productionUsWest1); - tester.runJob(instanceId, JobType.systemTest); - tester.runJob(instanceId, JobType.stagingTest); + app1.runJob(JobType.systemTest).runJob(JobType.stagingTest).runJob(JobType.productionUsWest1) + .runJob(JobType.stagingTest).runJob(JobType.systemTest); assertEquals("Upgrade done", 0, tester.jobs().active().size()); deployer.maintain(); - tester.deploymentTrigger().triggerReadyJobs(); - assertEquals("1.0.2-cafed00d", tester.application(app1.id()).change().application().get().id()); + tester.triggerJobs(); + assertEquals("1.0.2-cafed00d", app1.application().change().application().get().id()); List<Run> runs = tester.jobs().active(); assertEquals(1, runs.size()); - tester.assertRunning(instanceId, JobType.productionUsWest1); - assertFalse(tester.application(app1.id()).outstandingChange().hasTargets()); + app1.assertRunning(JobType.productionUsWest1); + assertFalse(app1.application().outstandingChange().hasTargets()); } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RoutingPoliciesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RoutingPoliciesTest.java index f5f1605a699..bc184715589 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RoutingPoliciesTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RoutingPoliciesTest.java @@ -14,7 +14,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.RoutingPolicy; import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder; -import com.yahoo.vespa.hosted.controller.deployment.BuildJob; import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext; import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester; import org.junit.Test; @@ -53,7 +52,6 @@ public class RoutingPoliciesTest { @Test public void maintains_global_routing_policies() { - long buildNumber = BuildJob.defaultBuildNumber; int clustersPerZone = 2; int numberOfDeployments = 2; var applicationPackage = new ApplicationPackageBuilder() diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java index 58a49307733..36039c47025 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java @@ -461,7 +461,7 @@ public class UpgraderTest { .region("us-west-1") .build(); - var app = tester.deploymentContext().submit(applicationPackage).deploy(); + var app = tester.newDeploymentContext().submit(applicationPackage).deploy(); // New version is released version = Version.fromString("6.3"); @@ -504,7 +504,7 @@ public class UpgraderTest { .region("us-east-3") .build(); - var app = tester.deploymentContext().submit(applicationPackage).deploy(); + var app = tester.newDeploymentContext().submit(applicationPackage).deploy(); // New version is released version = Version.fromString("6.3"); @@ -541,7 +541,7 @@ public class UpgraderTest { .region("us-east-3") .build(); - var app = tester.deploymentContext().submit(applicationPackage).deploy(); + var app = tester.newDeploymentContext().submit(applicationPackage).deploy(); // New version is released version = Version.fromString("6.3"); @@ -662,7 +662,7 @@ public class UpgraderTest { // Setup applications var canary0 = createAndDeploy("canary0", "canary"); - var default0 = tester.deploymentContext().submit(version6ApplicationPackage).deploy(); + var default0 = tester.newDeploymentContext().submit(version6ApplicationPackage).deploy(); // New major version is released version = Version.fromString("7.0"); @@ -690,7 +690,7 @@ public class UpgraderTest { // Setup applications var canary0 = createAndDeploy("canary", "canary"); - var default0 = tester.deploymentContext().submit().deploy(); + var default0 = tester.newDeploymentContext().submit().deploy(); tester.applications().lockApplicationOrThrow(default0.application().id(), a -> tester.applications().store(a.withMajorVersion(6))); assertEquals(OptionalInt.of(6), default0.application().majorVersion()); @@ -819,7 +819,7 @@ public class UpgraderTest { .region("us-east-3") .build(); - var app = tester.deploymentContext().submit(applicationPackage).deploy(); + var app = tester.newDeploymentContext().submit(applicationPackage).deploy(); // Application upgrade starts. app.submit(applicationPackage); @@ -861,7 +861,7 @@ public class UpgraderTest { .region("us-east-3") .build(); - var app = tester.deploymentContext().submit(applicationPackage).deploy(); + var app = tester.newDeploymentContext().submit(applicationPackage).deploy(); // Application revision starts rolling out. @@ -895,7 +895,7 @@ public class UpgraderTest { tester.controllerTester().upgradeSystem(version0); // Create an application with pinned platform version. - var context = tester.deploymentContext(); + var context = tester.newDeploymentContext(); tester.deploymentTrigger().forceChange(context.application().id(), Change.empty().withPin()); context.submit().deploy(); @@ -993,7 +993,7 @@ public class UpgraderTest { .region("us-west-1") .region("us-east-3") .build(); - var application = tester.deploymentContext().submit(applicationPackage).deploy(); + var application = tester.newDeploymentContext().submit(applicationPackage).deploy(); // Next version is released and 2/3 deployments upgrade Version v2 = Version.fromString("6.2"); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/metric/ConfigServerMetricsTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/metric/ConfigServerMetricsTest.java index 23a6c6286c0..23e86320a78 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/metric/ConfigServerMetricsTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/metric/ConfigServerMetricsTest.java @@ -1,6 +1,7 @@ package com.yahoo.vespa.hosted.controller.metric; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.api.application.v4.model.ClusterMetrics; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; @@ -31,7 +32,7 @@ public class ConfigServerMetricsTest { @Before public void before() { - configServer = new ConfigServerMock(new ZoneRegistryMock()); + configServer = new ConfigServerMock(new ZoneRegistryMock(SystemName.main)); service = new ConfigServerMetrics(configServer); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java index 186e982fd75..7536272d6d9 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java @@ -6,6 +6,7 @@ import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterSpec; +import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.security.KeyUtils; import com.yahoo.vespa.config.SlimeUtils; @@ -74,8 +75,13 @@ public class ApplicationSerializerTest { @Test public void testSerialization() { - DeploymentSpec deploymentSpec = DeploymentSpec.fromXml("<deployment version='1.0'>" + - " <staging/>" + + DeploymentSpec deploymentSpec = DeploymentSpec.fromXml("<deployment version='1.0'>\n" + + " <staging/>\n" + + " <instance id=\"i1\">\n" + + " <prod global-service-id=\"default\">\n" + + " <region active=\"true\">us-west-1</region>\n" + + " </prod>\n" + + " </instance>\n" + "</deployment>"); ValidationOverrides validationOverrides = ValidationOverrides.fromXml("<validation-overrides version='1.0'>" + " <allow until='2017-06-15'>deployment-removal</allow>" + @@ -100,9 +106,6 @@ public class ApplicationSerializerTest { List<JobStatus> statusList = new ArrayList<>(); - JobStatus.JobRun componentJob = JobStatus.JobRun.triggering(Version.emptyVersion, applicationVersion1, empty(), - empty(), "New commit", Instant.ofEpochMilli(400)) - .completion(100, Instant.ofEpochMilli(500)); statusList.add(JobStatus.initial(JobType.systemTest) .withTriggering(Version.fromString("5.6.7"), ApplicationVersion.unknown, empty(), "Test", Instant.ofEpochMilli(7)) .withCompletion(30, empty(), Instant.ofEpochMilli(8)) @@ -127,7 +130,7 @@ public class ApplicationSerializerTest { List<Instance> instances = List.of(new Instance(id1, deployments, deploymentJobs, - List.of(AssignedRotation.fromStrings("foo", "default", "my-rotation", Set.of())), + List.of(AssignedRotation.fromStrings("foo", "default", "my-rotation", Set.of("us-west-1"))), rotationStatus), new Instance(id3, List.of(), @@ -148,7 +151,6 @@ public class ApplicationSerializerTest { new ApplicationMetrics(0.5, 0.9), Set.of(publicKey, otherPublicKey), projectId, - true, Optional.of(applicationVersion1), instances); @@ -162,7 +164,6 @@ public class ApplicationSerializerTest { assertEquals(original.validationOverrides().xmlForm(), serialized.validationOverrides().xmlForm()); assertEquals(original.projectId(), serialized.projectId()); - assertEquals(original.internal(), serialized.internal()); assertEquals(original.deploymentIssueId(), serialized.deploymentIssueId()); assertEquals(0, serialized.require(id3.instance()).deployments().size()); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequestTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequestTest.java index b9d68c2a3da..d8373cb8928 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequestTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequestTest.java @@ -1,13 +1,13 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.proxy; -import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.jdisc.http.HttpRequest; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import java.net.URI; +import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; @@ -23,7 +23,7 @@ public class ProxyRequestTest { @Test public void testEmpty() throws Exception { exception.expectMessage("Request must be non-null"); - new ProxyRequest(HttpRequest.Method.GET, null, Map.of(), null, ZoneId.from("dev", "us-north-1"), "/zone/v2"); + new ProxyRequest(HttpRequest.Method.GET, null, Map.of(), null, List.of(), "/zone/v2"); } @Test @@ -69,6 +69,6 @@ public class ProxyRequestTest { private static ProxyRequest testRequest(String url, String pathPrefix) throws ProxyException { return new ProxyRequest( - HttpRequest.Method.GET, URI.create(url), Map.of(), null, ZoneId.from("dev", "us-north-1"), pathPrefix); + HttpRequest.Method.GET, URI.create(url), Map.of(), null, List.of(), pathPrefix); } } 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 efe7e17c58e..0aac59321b5 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 @@ -1,13 +1,13 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.proxy; -import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.jdisc.http.HttpRequest; import org.junit.Test; import java.io.ByteArrayOutputStream; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; @@ -20,7 +20,7 @@ public class ProxyResponseTest { @Test public void testRewriteUrl() throws Exception { ProxyRequest request = new ProxyRequest(HttpRequest.Method.GET, URI.create("http://domain.tld/zone/v2/dev/us-north-1/configserver"), - Map.of(), null, ZoneId.from("dev", "us-north-1"), "configserver"); + Map.of(), null, List.of(), "configserver"); ProxyResponse proxyResponse = new ProxyResponse( request, "response link is http://configserver:1234/bla/bla/", @@ -38,7 +38,7 @@ public class ProxyResponseTest { @Test public void testRewriteSecureUrl() throws Exception { ProxyRequest request = new ProxyRequest(HttpRequest.Method.GET, URI.create("https://domain.tld/zone/v2/prod/eu-south-3/configserver"), - Map.of(), null, ZoneId.from("prod", "eu-south-3"), "configserver"); + Map.of(), null, List.of(), "configserver"); ProxyResponse proxyResponse = new ProxyResponse( request, "response link is http://configserver:1234/bla/bla/", diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java deleted file mode 100644 index 110aaf2b1a6..00000000000 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.controller.restapi; - -import com.yahoo.application.container.JDisc; -import com.yahoo.application.container.handler.Request; -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.Environment; -import com.yahoo.config.provision.TenantName; -import com.yahoo.config.provision.zone.ZoneId; -import com.yahoo.vespa.athenz.api.AthenzDomain; -import com.yahoo.vespa.athenz.api.AthenzPrincipal; -import com.yahoo.vespa.athenz.api.AthenzUser; -import com.yahoo.vespa.athenz.api.OktaAccessToken; -import com.yahoo.vespa.athenz.api.OktaIdentityToken; -import com.yahoo.vespa.hosted.controller.Application; -import com.yahoo.vespa.hosted.controller.Controller; -import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeployOptions; -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.identifiers.ScrewdriverId; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; -import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockBuildService; -import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; -import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; -import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction; -import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; -import com.yahoo.vespa.hosted.controller.athenz.HostedAthenzIdentities; -import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock; -import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock; -import com.yahoo.vespa.hosted.controller.deployment.BuildJob; -import com.yahoo.vespa.hosted.controller.deployment.DeploymentSteps; -import com.yahoo.vespa.hosted.controller.maintenance.JobControl; -import com.yahoo.vespa.hosted.controller.maintenance.Upgrader; -import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; -import com.yahoo.vespa.hosted.controller.security.AthenzCredentials; -import com.yahoo.vespa.hosted.controller.security.AthenzTenantSpec; - -import java.io.File; -import java.time.Duration; -import java.util.Optional; - -import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.component; -import static org.junit.Assert.assertFalse; - -/** - * Provides testing of controller functionality accessed through the container - * - * @author bratseth - */ -public class ContainerControllerTester { - - private final ContainerTester containerTester; - private final Upgrader upgrader; - - public ContainerControllerTester(JDisc container, String responseFilePath) { - containerTester = new ContainerTester(container, responseFilePath); - CuratorDb curatorDb = controller().curator(); - upgrader = new Upgrader(controller(), Duration.ofDays(1), new JobControl(curatorDb), curatorDb); - upgrader.setUpgradesPerMinute(100); // Anything to make it more than one per maintenance interval. - } - - public Controller controller() { return containerTester.controller(); } - - public Upgrader upgrader() { return upgrader; } - - /** Returns the wrapped generic container tester */ - public ContainerTester containerTester() { return containerTester; } - - public Application createApplication() { - return createApplication("domain1","tenant1", "application1", "default"); - } - - public Application createApplication(String athensDomain, String tenant, String application, String instance) { - AthenzDomain domain1 = addTenantAthenzDomain(athensDomain, "user"); - AthenzPrincipal user = new AthenzPrincipal(new AthenzUser("user")); - AthenzCredentials credentials = new AthenzCredentials( - user, domain1, new OktaIdentityToken("okta-identity-token"), new OktaAccessToken("okta-access-token")); - AthenzTenantSpec tenantSpec = new AthenzTenantSpec(TenantName.from(tenant), - domain1, - new Property("property1"), - Optional.of(new PropertyId("1234"))); - controller().tenants().create(tenantSpec, credentials); - - TenantAndApplicationId id = TenantAndApplicationId.from(tenant, application); - controller().applications().createApplication(id, Optional.of(credentials)); - controller().applications().createInstance(id.instance(instance)); - return controller().applications().requireApplication(id); - } - - public void deploy(ApplicationId id, ApplicationPackage applicationPackage, ZoneId zone) { - controller().applications().deploy(id, zone, Optional.of(applicationPackage), - new DeployOptions(false, Optional.empty(), false, false)); - } - - public void deployCompletely(Application application, ApplicationPackage applicationPackage, long projectId, - boolean failStaging) { - jobCompletion(JobType.component).application(application) - .projectId(projectId) - .uploadArtifact(applicationPackage) - .submit(); - DeploymentSteps steps = controller().applications().deploymentTrigger().steps(applicationPackage.deploymentSpec()); - // TODO jonmv: Connect instances from deployment spec to deployments below. - boolean succeeding = true; - for (var job : steps.jobs()) { - if (!succeeding) return; - var zone = job.zone(controller().system()); - deploy(application.id().defaultInstance(), applicationPackage, zone); - if (failStaging && zone.environment() == Environment.staging) { - succeeding = false; - } - if (zone.environment().isTest()) { - controller().applications().deactivate(application.id().defaultInstance(), zone); - } - jobCompletion(job).application(application).success(succeeding).projectId(projectId).submit(); - } - } - - /** Notify the controller about a job completing */ - public BuildJob jobCompletion(JobType job) { - return new BuildJob(this::notifyJobCompletion, containerTester.serviceRegistry().artifactRepositoryMock()).type(job); - } - - // ---- Delegators: - - public void assertResponse(Request request, File expectedResponse) { - containerTester.assertResponse(request, expectedResponse); - } - - public void assertResponse(Request request, String expectedResponse, int expectedStatusCode) { - containerTester.assertResponse(() -> request, expectedResponse, expectedStatusCode); - } - - /* - * Authorize action on tenantDomain/application for a given screwdriverId - */ - public void authorize(AthenzDomain tenantDomain, ScrewdriverId screwdriverId, ApplicationAction action, TenantAndApplicationId id) { - AthenzClientFactoryMock mock = (AthenzClientFactoryMock) containerTester.container().components() - .getComponent(AthenzClientFactoryMock.class.getName()); - - mock.getSetup() - .domains.get(tenantDomain) - .applications.get(new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(id.application().value())) - .addRoleMember(action, HostedAthenzIdentities.from(screwdriverId)); - } - - private void notifyJobCompletion(DeploymentJobs.JobReport report) { - MockBuildService buildService = containerTester.serviceRegistry().buildServiceMock(); - if (report.jobType() != component && ! buildService.remove(report.buildJob())) - throw new IllegalArgumentException(report.jobType() + " is not running for " + report.applicationId()); - assertFalse("Unexpected entry '" + report.jobType() + "@" + report.projectId() + " in: " + buildService.jobs(), - buildService.remove(report.buildJob())); - controller().applications().deploymentTrigger().notifyOfCompletion(report); - controller().applications().deploymentTrigger().triggerReadyJobs(); - } - - private AthenzDomain addTenantAthenzDomain(String domainName, String userName) { - AthenzClientFactoryMock mock = (AthenzClientFactoryMock) containerTester.container().components() - .getComponent(AthenzClientFactoryMock.class.getName()); - AthenzDomain athensDomain = new AthenzDomain(domainName); - AthenzDbMock.Domain domain = mock.getSetup().getOrCreateDomain(athensDomain); - domain.markAsVespaTenant(); - domain.admin(new AthenzUser(userName)); - return athensDomain; - } - -} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java index 300eddd6291..df8787a2a4b 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java @@ -6,12 +6,19 @@ import com.yahoo.application.container.handler.Request; import com.yahoo.application.container.handler.Response; import com.yahoo.component.ComponentSpecification; import com.yahoo.component.Version; +import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.container.http.filter.FilterChainRepository; import com.yahoo.jdisc.http.filter.SecurityRequestFilter; import com.yahoo.jdisc.http.filter.SecurityRequestFilterChain; +import com.yahoo.vespa.athenz.api.AthenzDomain; +import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId; +import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction; +import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock; import com.yahoo.vespa.hosted.controller.application.SystemApplication; +import com.yahoo.vespa.hosted.controller.athenz.HostedAthenzIdentities; import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock; import com.yahoo.vespa.hosted.controller.integration.ServiceRegistryMock; import com.yahoo.vespa.hosted.controller.versions.ControllerVersion; @@ -58,24 +65,19 @@ public class ContainerTester { return serviceRegistry().configServerMock(); } - public ServiceRegistryMock serviceRegistry() { - return (ServiceRegistryMock) container.components().getComponent(ServiceRegistryMock.class.getName()); + public AthenzClientFactoryMock athenzClientFactory() { + return (AthenzClientFactoryMock) container.components().getComponent(AthenzClientFactoryMock.class.getName()); } - public void computeVersionStatus() { - controller().updateVersionStatus(VersionStatus.compute(controller())); + public ServiceRegistryMock serviceRegistry() { + return (ServiceRegistryMock) container.components().getComponent(ServiceRegistryMock.class.getName()); } - public void upgradeSystem(Version version) { - var controllerVersion = new ControllerVersion(version, "badc0ffee", Instant.EPOCH); - controller().curator().writeControllerVersion(controller().hostname(), controllerVersion); - for (ZoneApi zone : controller().zoneRegistry().zones().all().zones()) { - for (SystemApplication application : SystemApplication.all()) { - configServer().setVersion(application.id(), zone.getId(), controllerVersion.version()); - configServer().convergeServices(application.id(), zone.getId()); - } - } - computeVersionStatus(); + public void authorize(AthenzDomain tenantDomain, AthenzIdentity identity, ApplicationAction action, ApplicationName application) { + athenzClientFactory().getSetup() + .domains.get(tenantDomain) + .applications.get(new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(application.value())) + .addRoleMember(action, identity); } public void assertResponse(Supplier<Request> request, File responseFile) { @@ -128,6 +130,10 @@ public class ContainerTester { assertResponse(() -> request, expectedResponse, 200); } + public void assertResponse(Request request, String expectedResponse, int expectedStatusCode) { + assertResponse(() -> request, expectedResponse, expectedStatusCode); + } + public void assertResponse(Supplier<Request> request, String expectedResponse, int expectedStatusCode) { assertResponse(request, (response) -> assertEquals(expectedResponse, new String(response.getBody(), StandardCharsets.UTF_8)), @@ -139,8 +145,8 @@ public class ContainerTester { FilterResult filterResult = invokeSecurityFilters(request); request = filterResult.request; Response response = filterResult.response != null ? filterResult.response : container.handleRequest(request); - assertEquals("Status code", expectedStatusCode, response.getStatus()); responseAssertion.accept(response); + assertEquals("Status code", expectedStatusCode, response.getStatus()); } // Hack to run request filters as part of the request processing chain. diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java index fb0e92ab7f4..dc6abcb2616 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java @@ -34,12 +34,16 @@ import static org.junit.Assert.assertEquals; */ public class ControllerContainerTest { + private static final AthenzUser hostedOperator = AthenzUser.fromUserId("alice"); private static final AthenzUser defaultUser = AthenzUser.fromUserId("bob"); protected JDisc container; @Before - public void startContainer() { container = JDisc.fromServicesXml(controllerServicesXml(), Networking.disable); } + public void startContainer() { + container = JDisc.fromServicesXml(controllerServicesXml(), Networking.disable); + addUserToHostedOperatorRole(hostedOperator); + } @After public void stopContainer() { container.close(); } @@ -66,7 +70,6 @@ public class ControllerContainerTest { " <component id='com.yahoo.vespa.curator.mock.MockCurator'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock'/>\n" + - " <component id='com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.integration.ServiceRegistryMock'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.Controller'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.integration.ConfigServerProxyMock'/>\n" + @@ -92,6 +95,12 @@ public class ControllerContainerTest { " <binding>http://*/zone/v2</binding>\n" + " <binding>http://*/zone/v2/*</binding>\n" + " </handler>\n" + + " <handler id='com.yahoo.vespa.hosted.controller.restapi.configserver.ConfigServerApiHandler'>\n" + + " <binding>http://*/configserver/v1</binding>\n" + + " <binding>http://*/configserver/v1/*</binding>\n" + + " <binding>http://*/api/configserver/v1</binding>\n" + + " <binding>http://*/api/configserver/v1/*</binding>\n" + + " </handler>\n" + " <handler id='com.yahoo.vespa.hosted.controller.restapi.flags.AuditedFlagsHandler'>\n" + " <binding>http://*/flags/v1</binding>\n" + " <binding>http://*/flags/v1/*</binding>\n" + @@ -147,10 +156,18 @@ public class ControllerContainerTest { return addIdentityToRequest(new Request(uri), defaultUser); } - protected static Request authenticatedRequest(String uri, byte[] body, Request.Method method) { + protected static Request authenticatedRequest(String uri, String body, Request.Method method) { return addIdentityToRequest(new Request(uri, body, method), defaultUser); } + protected static Request operatorRequest(String uri) { + return addIdentityToRequest(new Request(uri), hostedOperator); + } + + protected static Request operatorRequest(String uri, String body, Request.Method method) { + return addIdentityToRequest(new Request(uri, body, method), hostedOperator); + } + protected static Request addIdentityToRequest(Request request, AthenzIdentity identity) { request.getHeaders().put(IDENTITY_HEADER_NAME, identity.getFullName()); return request; 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 cf28845b4e1..b5b7c1d0ad0 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 @@ -7,24 +7,21 @@ import com.yahoo.application.container.handler.Request; import com.yahoo.component.Version; import com.yahoo.config.application.api.ValidationId; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.AthenzService; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.RegionName; -import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; -import com.yahoo.slime.Cursor; -import com.yahoo.slime.Slime; -import com.yahoo.test.ManualClock; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.athenz.api.OktaAccessToken; import com.yahoo.vespa.athenz.api.OktaIdentityToken; -import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.controller.Application; +import com.yahoo.vespa.hosted.controller.ControllerTester; import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.LockedTenant; import com.yahoo.vespa.hosted.controller.api.application.v4.EnvironmentResource; @@ -35,7 +32,6 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId; import com.yahoo.vespa.hosted.controller.api.identifiers.UserId; import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction; -import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock; import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException; import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion; @@ -54,7 +50,6 @@ import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.ClusterInfo; import com.yahoo.vespa.hosted.controller.application.Deployment; -import com.yahoo.vespa.hosted.controller.application.DeploymentJobs; import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; import com.yahoo.vespa.hosted.controller.application.EndpointId; import com.yahoo.vespa.hosted.controller.application.JobStatus; @@ -62,15 +57,13 @@ import com.yahoo.vespa.hosted.controller.application.RoutingPolicy; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.athenz.HostedAthenzIdentities; import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder; -import com.yahoo.vespa.hosted.controller.deployment.BuildJob; import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext; +import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester; import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger; import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock; -import com.yahoo.vespa.hosted.controller.integration.ServiceRegistryMock; import com.yahoo.vespa.hosted.controller.maintenance.JobControl; import com.yahoo.vespa.hosted.controller.maintenance.RotationStatusUpdater; import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics; -import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; @@ -80,8 +73,6 @@ import org.junit.Before; import org.junit.Test; import java.io.File; -import java.io.IOException; -import java.io.UncheckedIOException; import java.math.BigDecimal; import java.net.URI; import java.nio.charset.StandardCharsets; @@ -106,9 +97,7 @@ import static com.yahoo.application.container.handler.Request.Method.POST; import static com.yahoo.application.container.handler.Request.Method.PUT; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; /** @@ -154,24 +143,20 @@ public class ApplicationApiTest extends ControllerContainerTest { private static final UserId HOSTED_VESPA_OPERATOR = new UserId("johnoperator"); private static final OktaIdentityToken OKTA_IT = new OktaIdentityToken("okta-it"); private static final OktaAccessToken OKTA_AT = new OktaAccessToken("okta-at"); - private static final ZoneId TEST_ZONE = ZoneId.from(Environment.test, RegionName.from("us-east-1")); - private static final ZoneId STAGING_ZONE = ZoneId.from(Environment.staging, RegionName.from("us-east-3")); - private ContainerControllerTester controllerTester; private ContainerTester tester; + private DeploymentTester deploymentTester; @Before public void before() { - controllerTester = new ContainerControllerTester(container, responseFiles); - tester = controllerTester.containerTester(); + tester = new ContainerTester(container, responseFiles); + deploymentTester = new DeploymentTester(new ControllerTester(tester)); + deploymentTester.controllerTester().computeVersionStatus(); } @Test public void testApplicationApi() { - tester.computeVersionStatus(); - tester.controller().jobController().setRunner(__ -> { }); // Avoid uncontrollable, multi-threaded job execution - createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); // (Necessary but not provided in this API) // GET API root @@ -253,95 +238,80 @@ public class ApplicationApiTest extends ControllerContainerTest { addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR)); - // POST (deploy) an application to a zone - manual user deployment (includes a content hash for verification) + ApplicationId id = ApplicationId.from("tenant1", "application1", "instance1"); + var app1 = deploymentTester.newDeploymentContext(id); + + // POST (deploy) an application to start a manual deployment to dev MultiPartStreamer entity = createApplicationDeployData(applicationPackageInstance1, true); - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/instance1/deploy", POST) + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploy/dev-us-east-1/", POST) .data(entity) - .header("X-Content-Hash", Base64.getEncoder().encodeToString(Signatures.sha256Digest(entity::data))) .userIdentity(USER_ID), - new File("deploy-result.json")); + "{\"message\":\"Deployment started in run 1 of dev-us-east-1 for tenant1.application1.instance1. This may take about 15 minutes the first time.\",\"run\":1}"); + app1.runJob(JobType.devUsEast1); - // POST (deploy) an application to a zone. This simulates calls done by our tenant pipeline. - ApplicationId id = ApplicationId.from("tenant1", "application1", "instance1"); - long screwdriverProjectId = 123; + // POST an application package is allowed under user instance + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/otheruser/deploy/dev-us-east-1", POST) + .userIdentity(OTHER_USER_ID) + .data(createApplicationDeployData(applicationPackageInstance1, false)), + new File("deployment-job-accepted-2.json")); + + // DELETE a dev deployment is allowed under user instance + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/otheruser/environment/dev/region/us-east-1", DELETE) + .userIdentity(OTHER_USER_ID), + "{\"message\":\"Deactivated tenant1.application1.otheruser in dev.us-east-1\"}"); + + // DELETE a user instance + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/otheruser", DELETE) + .userIdentity(USER_ID) + .oktaAccessToken(OKTA_AT).oktaIdentityToken(OKTA_IT), + "{\"message\":\"Deleted instance tenant1.application1.otheruser\"}"); addScrewdriverUserToDeployRole(SCREWDRIVER_ID, ATHENZ_TENANT_DOMAIN, - new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(id.application().value())); // (Necessary but not provided in this API) - - // Pipeline notifies about completed component job - controllerTester.jobCompletion(JobType.component) - .application(id) - .projectId(screwdriverProjectId) - .uploadArtifact(applicationPackageInstance1) - .submit(); - - // ... systemtest - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/instance1/", POST) - .data(createApplicationDeployData(Optional.empty(), false)) - .screwdriverIdentity(SCREWDRIVER_ID), - new File("deploy-result.json")); - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/instance1", DELETE) - .screwdriverIdentity(SCREWDRIVER_ID), - "{\"message\":\"Deactivated tenant1.application1.instance1 in test.us-east-1\"}"); + id.application()); - controllerTester.jobCompletion(JobType.systemTest) - .application(id) - .projectId(screwdriverProjectId) - .submit(); - - // ... staging - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/staging/region/us-east-3/instance/instance1/", POST) - .data(createApplicationDeployData(Optional.empty(), false)) - .screwdriverIdentity(SCREWDRIVER_ID), - new File("deploy-result.json")); - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/staging/region/us-east-3/instance/instance1", DELETE) - .screwdriverIdentity(SCREWDRIVER_ID), - "{\"message\":\"Deactivated tenant1.application1.instance1 in staging.us-east-3\"}"); - controllerTester.jobCompletion(JobType.stagingTest) - .application(id) - .projectId(screwdriverProjectId) - .submit(); + // POST an application package and a test jar, submitting a new application for production deployment. + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/submit", POST) + .screwdriverIdentity(SCREWDRIVER_ID) + .data(createApplicationSubmissionData(applicationPackageInstance1, 123)), + "{\"message\":\"Application package version: 1.0.1-commit1, source revision of repository 'repository1', branch 'master' with commit 'commit1', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\"}"); - // ... prod zone - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/", POST) - .data(createApplicationDeployData(Optional.empty(), false)) - .screwdriverIdentity(SCREWDRIVER_ID), - new File("deploy-result.json")); - controllerTester.jobCompletion(JobType.productionUsCentral1) - .application(id) - .projectId(screwdriverProjectId) - .unsuccessful() - .submit(); + app1.runJob(JobType.systemTest).runJob(JobType.stagingTest).runJob(JobType.productionUsCentral1); // POST an application deployment to a production zone - operator emergency deployment - fails since package is unknown entity = createApplicationDeployData(Optional.empty(), - Optional.of(ApplicationVersion.from(DeploymentContext.defaultSourceRevision, - BuildJob.defaultBuildNumber - 1)), + Optional.of(ApplicationVersion.from(DeploymentContext.defaultSourceRevision, 666)), true); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/", POST) .data(entity) .userIdentity(HOSTED_VESPA_OPERATOR), - "{\"error-code\":\"BAD_REQUEST\",\"message\":\"No application package found for tenant1.application1.instance1 with version 1.0.41-commit1\"}", + "{\"error-code\":\"BAD_REQUEST\",\"message\":\"No application package found for tenant1.application1 with version 1.0.666-commit1\"}", 400); // POST an application deployment to a production zone - operator emergency deployment - works with known package entity = createApplicationDeployData(Optional.empty(), - Optional.of(ApplicationVersion.from(DeploymentContext.defaultSourceRevision, - BuildJob.defaultBuildNumber)), + Optional.of(ApplicationVersion.from(DeploymentContext.defaultSourceRevision, 1)), true); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/", POST) .data(entity) .userIdentity(HOSTED_VESPA_OPERATOR), new File("deploy-result.json")); + // POST an application deployment to a production zone - operator emergency deployment - chooses latest package without arguments + entity = createApplicationDeployData(Optional.empty(), true); + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/", POST) + .data(entity) + .userIdentity(HOSTED_VESPA_OPERATOR), + new File("deploy-result.json")); + // POST (create) another application ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .instances("instance1") .globalServiceId("foo") .environment(Environment.prod) .region("us-west-1") + .region("us-east-3") .allow(ValidationId.globalEndpointChange) .build(); @@ -350,20 +320,22 @@ public class ApplicationApiTest extends ControllerContainerTest { .oktaAccessToken(OKTA_AT).oktaIdentityToken(OKTA_IT), new File("instance-reference-2.json")); - ApplicationId app2 = ApplicationId.from("tenant2", "application2", "default"); - long screwdriverProjectId2 = 456; + ApplicationId id2 = ApplicationId.from("tenant2", "application2", "instance1"); + var app2 = deploymentTester.newDeploymentContext(id2); addScrewdriverUserToDeployRole(SCREWDRIVER_ID, ATHENZ_TENANT_DOMAIN_2, - new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(app2.application().value())); + id2.application()); // Trigger upgrade and then application change - controllerTester.controller().applications().deploymentTrigger().triggerChange(TenantAndApplicationId.from(app2), Change.of(Version.fromString("7.0"))); + deploymentTester.applications().deploymentTrigger().triggerChange(TenantAndApplicationId.from(id2), Change.of(Version.fromString("7.0"))); + + // POST an application package and a test jar, submitting a new application for production deployment. + tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/submit", POST) + .screwdriverIdentity(SCREWDRIVER_ID) + .data(createApplicationSubmissionData(applicationPackage, 1000)), + "{\"message\":\"Application package version: 1.0.1-commit1, source revision of repository 'repository1', branch 'master' with commit 'commit1', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\"}"); - controllerTester.jobCompletion(JobType.component) - .application(app2) - .projectId(screwdriverProjectId2) - .uploadArtifact(applicationPackage) - .submit(); + deploymentTester.triggerJobs(); // GET application having both change and outstanding change tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", GET) @@ -375,7 +347,6 @@ public class ApplicationApiTest extends ControllerContainerTest { .screwdriverIdentity(SCREWDRIVER_ID), new File("application2.json")); - // PATCH in a major version override tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", PATCH) .userIdentity(USER_ID) @@ -415,16 +386,22 @@ public class ApplicationApiTest extends ControllerContainerTest { .userIdentity(USER_ID), new File("application2.json")); - // DELETE application + // DELETE instance 1 of 2 + tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/instance/default", DELETE) + .userIdentity(USER_ID) + .oktaAccessToken(OKTA_AT).oktaIdentityToken(OKTA_IT), + "{\"message\":\"Deleted instance tenant2.application2.default\"}"); + + // DELETE application with only one instance left tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", DELETE) .userIdentity(USER_ID) .oktaAccessToken(OKTA_AT).oktaIdentityToken(OKTA_IT), "{\"message\":\"Deleted application tenant2.application2\"}"); // Set version 6.1 to broken to change compile version for. - controllerTester.upgrader().overrideConfidence(Version.fromString("6.1"), VespaVersion.Confidence.broken); - tester.computeVersionStatus(); - setDeploymentMaintainedInfo(controllerTester); + deploymentTester.upgrader().overrideConfidence(Version.fromString("6.1"), VespaVersion.Confidence.broken); + deploymentTester.controllerTester().computeVersionStatus(); + setDeploymentMaintainedInfo(); setZoneInRotation("rotation-fqdn-1", ZoneId.from("prod", "us-central-1")); // GET tenant application deployments @@ -436,7 +413,7 @@ public class ApplicationApiTest extends ControllerContainerTest { .userIdentity(USER_ID), new File("deployment.json")); - addIssues(controllerTester, TenantAndApplicationId.from("tenant1", "application1")); + addIssues(deploymentTester, TenantAndApplicationId.from("tenant1", "application1")); // GET at root, with "&recursive=deployment", returns info about all tenants, their applications and their deployments tester.assertResponse(request("/application/v4/", GET) .userIdentity(USER_ID) @@ -471,7 +448,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // DELETE (cancel) ongoing change tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", DELETE) .userIdentity(HOSTED_VESPA_OPERATOR), - "{\"message\":\"Changed deployment from 'application change to 1.0.42-commit1' to 'no change' for application 'tenant1.application1'\"}"); + "{\"message\":\"Changed deployment from 'application change to 1.0.1-commit1' to 'no change' for application 'tenant1.application1'\"}"); // DELETE (cancel) again is a no-op tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", DELETE) @@ -557,7 +534,7 @@ public class ApplicationApiTest extends ControllerContainerTest { "{\"message\":\"Requested restart of tenant1.application1.instance1 in dev.us-central-1\"}"); // POST a 'restart application' command with a host filter (other filters not supported yet) - tester.serviceRegistry().configServerMock().nodeRepository().addFixedNodes(ZoneId.from("prod", "us-central-1")); + deploymentTester.configServer().nodeRepository().addFixedNodes(ZoneId.from("prod", "us-central-1")); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/restart?hostname=hostA", POST) .screwdriverIdentity(SCREWDRIVER_ID), "{\"message\":\"Requested restart of tenant1.application1.instance1 in prod.us-central-1\"}", 200); @@ -583,9 +560,9 @@ public class ApplicationApiTest extends ControllerContainerTest { new File("delete-with-active-deployments.json"), 400); // DELETE (deactivate) a deployment - dev - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/instance1", DELETE) + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-east-1/instance/instance1", DELETE) .userIdentity(USER_ID), - "{\"message\":\"Deactivated tenant1.application1.instance1 in dev.us-west-1\"}"); + "{\"message\":\"Deactivated tenant1.application1.instance1 in dev.us-east-1\"}"); // DELETE (deactivate) a deployment - prod tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1", DELETE) @@ -625,38 +602,6 @@ public class ApplicationApiTest extends ControllerContainerTest { ZoneId.from("dev", "us-east-1")); // teardown for test config tests - // POST an application package to start a deployment to dev - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploy/dev-us-east-1", POST) - .userIdentity(USER_ID) - .data(createApplicationDeployData(applicationPackage, false)), - new File("deployment-job-accepted.json")); - - // POST an application package is allowed under user instance - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/otheruser/deploy/dev-us-east-1", POST) - .userIdentity(OTHER_USER_ID) - .data(createApplicationDeployData(applicationPackage, false)), - new File("deployment-job-accepted-2.json")); - - // DELETE a dev deployment is allowed under user instance - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/otheruser/environment/dev/region/us-east-1", DELETE) - .userIdentity(OTHER_USER_ID), - "{\"message\":\"Deactivated tenant1.application1.otheruser in dev.us-east-1\"}"); - - // POST an application package and a test jar, submitting a new application for internal pipeline deployment. - // First attempt does not have an Athenz service definition in deployment spec, and is accepted. - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/submit", POST) - .screwdriverIdentity(SCREWDRIVER_ID) - .data(createApplicationSubmissionData(applicationPackage)), - "{\"message\":\"Application package version: 1.0.43-d00d, source revision of repository 'repo', branch 'master' with commit 'd00d', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\"}"); - - // GET application package - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/package", GET).userIdentity(HOSTED_VESPA_OPERATOR), - (response) -> { - assertEquals("attachment; filename=\"tenant1.application1-build43.zip\"", response.getHeaders().getFirst("Content-Disposition")); - assertArrayEquals(applicationPackage.zippedContent(), response.getBody()); - }, - 200); - // Second attempt has a service under a different domain than the tenant of the application, and fails. ApplicationPackage packageWithServiceForWrongDomain = new ApplicationPackageBuilder() .instances("instance1") @@ -664,39 +609,40 @@ public class ApplicationApiTest extends ControllerContainerTest { .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from(ATHENZ_TENANT_DOMAIN_2.getName()), AthenzService.from("service")) .region("us-west-1") .build(); - configureAthenzIdentity(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN_2, "service"), true); + allowLaunchOfService(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN_2, "service")); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/submit", POST) .screwdriverIdentity(SCREWDRIVER_ID) - .data(createApplicationSubmissionData(packageWithServiceForWrongDomain)), + .data(createApplicationSubmissionData(packageWithServiceForWrongDomain, 123)), "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Athenz domain in deployment.xml: [domain2] must match tenant domain: [domain1]\"}", 400); - // Third attempt finally has a service under the domain of the tenant, and succeeds. + // Third attempt has a service under the domain of the tenant, and also succeeds. ApplicationPackage packageWithService = new ApplicationPackageBuilder() .instances("instance1") .globalServiceId("foo") .environment(Environment.prod) .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from(ATHENZ_TENANT_DOMAIN.getName()), AthenzService.from("service")) - .region("us-west-1") + .region("us-central-1") + .parallel("us-west-1", "us-east-3") .build(); - configureAthenzIdentity(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service"), true); + allowLaunchOfService(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service")); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/submit", POST) .screwdriverIdentity(SCREWDRIVER_ID) - .data(createApplicationSubmissionData(packageWithService)), - "{\"message\":\"Application package version: 1.0.44-d00d, source revision of repository 'repo', branch 'master' with commit 'd00d', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\"}"); + .data(createApplicationSubmissionData(packageWithService, 123)), + "{\"message\":\"Application package version: 1.0.2-commit1, source revision of repository 'repository1', branch 'master' with commit 'commit1', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\"}"); // GET last submitted application package tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/package", GET).userIdentity(HOSTED_VESPA_OPERATOR), (response) -> { - assertEquals("attachment; filename=\"tenant1.application1-build44.zip\"", response.getHeaders().getFirst("Content-Disposition")); + assertEquals("attachment; filename=\"tenant1.application1-build2.zip\"", response.getHeaders().getFirst("Content-Disposition")); assertArrayEquals(packageWithService.zippedContent(), response.getBody()); }, 200); // GET application package for previous build - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/package?build=43", GET).userIdentity(HOSTED_VESPA_OPERATOR), + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/package?build=1", GET).userIdentity(HOSTED_VESPA_OPERATOR), (response) -> { - assertEquals("attachment; filename=\"tenant1.application1-build43.zip\"", response.getHeaders().getFirst("Content-Disposition")); - assertArrayEquals(applicationPackage.zippedContent(), response.getBody()); + assertEquals("attachment; filename=\"tenant1.application1-build1.zip\"", response.getHeaders().getFirst("Content-Disposition")); + assertArrayEquals(applicationPackageInstance1.zippedContent(), response.getBody()); }, 200); @@ -704,40 +650,30 @@ public class ApplicationApiTest extends ControllerContainerTest { tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/submit", POST) .screwdriverIdentity(SCREWDRIVER_ID) .header("X-Content-Hash", "not/the/right/hash") - .data(createApplicationSubmissionData(packageWithService)), + .data(createApplicationSubmissionData(packageWithService, 123)), "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Value of X-Content-Hash header does not match computed content hash\"}", 400); // Fifth attempt has the right content hash in a header, and succeeds. - MultiPartStreamer streamer = createApplicationSubmissionData(packageWithService); + MultiPartStreamer streamer = createApplicationSubmissionData(packageWithService, 123); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/submit", POST) .screwdriverIdentity(SCREWDRIVER_ID) .header("X-Content-Hash", Base64.getEncoder().encodeToString(Signatures.sha256Digest(streamer::data))) .data(streamer), - "{\"message\":\"Application package version: 1.0.45-d00d, source revision of repository 'repo', branch 'master' with commit 'd00d', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\"}"); + "{\"message\":\"Application package version: 1.0.3-commit1, source revision of repository 'repository1', branch 'master' with commit 'commit1', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\"}"); - // Sixth attempt has a multi-instance deployment spec, and fails. + // Sixth attempt has a multi-instance deployment spec, and is accepted. ApplicationPackage multiInstanceSpec = new ApplicationPackageBuilder() .instances("instance1,instance2") .environment(Environment.prod) - .region("us-west-1") + .region("us-central-1") + .parallel("us-west-1", "us-east-3") + .endpoint("default", "foo", "us-central-1", "us-west-1", "us-east-3") .build(); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/submit", POST) .screwdriverIdentity(SCREWDRIVER_ID) - .data(createApplicationSubmissionData(multiInstanceSpec)), - "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Only single-instance deployment specs are currently supported\"}", 400); + .data(createApplicationSubmissionData(multiInstanceSpec, 123)), + "{\"message\":\"Application package version: 1.0.4-commit1, source revision of repository 'repository1', branch 'master' with commit 'commit1', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\"}"); - ApplicationId app1 = ApplicationId.from("tenant1", "application1", "instance1"); - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/jobreport", POST) - .screwdriverIdentity(SCREWDRIVER_ID) - .data(asJson(DeploymentJobs.JobReport.ofComponent(app1, - 1234, - 123, - Optional.empty(), - DeploymentContext.defaultSourceRevision))), - "{\"error-code\":\"BAD_REQUEST\",\"message\":\"" + app1 + " is set up to be deployed from internally," + - " and no longer accepts submissions from Screwdriver v3 jobs. If you need to revert " + - "to the old pipeline, please file a ticket at yo/vespa-support and request this.\"}", - 400); // GET deployment job overview, after triggering system and staging test jobs. assertEquals(2, tester.controller().applications().deploymentTrigger().triggerReadyJobs()); @@ -758,21 +694,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // DELETE a running job to have it aborted. tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/job/staging-test", DELETE) .userIdentity(USER_ID), - "{\"message\":\"Aborting run 1 of staging-test for tenant1.application1.instance1\"}"); - - // DELETE submission to unsubscribe from continuous deployment. - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/submit", DELETE) - .userIdentity(HOSTED_VESPA_OPERATOR), - "{\"message\":\"Unregistered 'tenant1.application1' from internal deployment pipeline.\"}"); - - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/jobreport", POST) - .screwdriverIdentity(SCREWDRIVER_ID) - .data(asJson(DeploymentJobs.JobReport.ofComponent(app1, - 1234, - 123, - Optional.empty(), - DeploymentContext.defaultSourceRevision))), - "{\"message\":\"ok\"}"); + "{\"message\":\"Aborting run 2 of staging-test for tenant1.application1.instance1\"}"); // PUT (create) the authenticated user byte[] data = new byte[0]; @@ -804,10 +726,10 @@ public class ApplicationApiTest extends ControllerContainerTest { .userIdentity(USER_ID) .oktaAccessToken(OKTA_AT).oktaIdentityToken(OKTA_IT), "{\"message\":\"Deleted instance tenant1.application1.instance1\"}"); - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/otheruser", DELETE) + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance2", DELETE) .userIdentity(USER_ID) .oktaAccessToken(OKTA_AT).oktaIdentityToken(OKTA_IT), - "{\"message\":\"Deleted instance tenant1.application1.otheruser\"}"); + "{\"message\":\"Deleted instance tenant1.application1.instance2\"}"); // DELETE a tenant tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE).userIdentity(USER_ID) @@ -815,8 +737,8 @@ public class ApplicationApiTest extends ControllerContainerTest { new File("tenant-without-applications.json")); } - private void addIssues(ContainerControllerTester tester, TenantAndApplicationId id) { - tester.controller().applications().lockApplicationOrThrow(id, application -> + private void addIssues(DeploymentTester tester, TenantAndApplicationId id) { + tester.applications().lockApplicationOrThrow(id, application -> tester.controller().applications().store(application.withDeploymentIssueId(IssueId.from("123")) .withOwnershipIssueId(IssueId.from("321")) .withOwner(User.from("owner-username")))); @@ -825,7 +747,6 @@ public class ApplicationApiTest extends ControllerContainerTest { @Test public void testRotationOverride() { // Setup - tester.computeVersionStatus(); createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .instances("instance1") @@ -835,21 +756,8 @@ public class ApplicationApiTest extends ControllerContainerTest { .build(); // Create tenant and deploy - ApplicationId id = createTenantAndApplication(); - long projectId = 1; - MultiPartStreamer deployData = createApplicationDeployData(Optional.of(applicationPackage), false); - startAndTestChange(controllerTester, id, projectId, applicationPackage, deployData, 100); - - // us-west-1 - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/deploy", POST) - .data(deployData) - .screwdriverIdentity(SCREWDRIVER_ID), - new File("deploy-result.json")); - controllerTester.jobCompletion(JobType.productionUsWest1) - .application(id) - .projectId(projectId) - .submit(); - setZoneInRotation("rotation-fqdn-1", ZoneId.from("prod", "us-west-1")); + var app = deploymentTester.newDeploymentContext(createTenantAndApplication()); + app.submit(applicationPackage).runJob(JobType.systemTest).runJob(JobType.stagingTest).runJob(JobType.productionUsWest1); // Invalid application fails tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/environment/prod/region/us-west-1/instance/default/global-rotation", GET) @@ -897,7 +805,6 @@ public class ApplicationApiTest extends ControllerContainerTest { @Test public void multiple_endpoints() { // Setup - tester.computeVersionStatus(); createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .instances("instance1") @@ -909,20 +816,9 @@ public class ApplicationApiTest extends ControllerContainerTest { .build(); // Create tenant and deploy - ApplicationId id = createTenantAndApplication(); - long projectId = 1; - MultiPartStreamer deployData = createApplicationDeployData(Optional.empty(), false); - startAndTestChange(controllerTester, id, projectId, applicationPackage, deployData, 100); - for (var job : List.of(JobType.productionUsWest1, JobType.productionUsEast3, JobType.productionEuWest1)) { - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/" + job.zone(SystemName.main).region().value() + "/deploy", POST) - .data(deployData) - .screwdriverIdentity(SCREWDRIVER_ID), - new File("deploy-result.json")); - controllerTester.jobCompletion(job) - .application(id) - .projectId(projectId) - .submit(); - } + var app = deploymentTester.newDeploymentContext("tenant1", "application1", "instance1"); + app.submit(applicationPackage).deploy(); + setZoneInRotation("rotation-fqdn-2", ZoneId.from("prod", "us-west-1")); setZoneInRotation("rotation-fqdn-2", ZoneId.from("prod", "us-east-3")); setZoneInRotation("rotation-fqdn-1", ZoneId.from("prod", "eu-west-1")); @@ -955,7 +851,6 @@ public class ApplicationApiTest extends ControllerContainerTest { @Test public void testDeployDirectly() { // Setup - tester.computeVersionStatus(); createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR)); @@ -974,7 +869,7 @@ public class ApplicationApiTest extends ControllerContainerTest { // Grant deploy access addScrewdriverUserToDeployRole(SCREWDRIVER_ID, ATHENZ_TENANT_DOMAIN, - new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId("application1")); + ApplicationName.from("application1")); // POST (deploy) an application to a prod zone - allowed when project ID is not specified MultiPartStreamer entity = createApplicationDeployData(applicationPackageInstance1, true); @@ -990,7 +885,7 @@ public class ApplicationApiTest extends ControllerContainerTest { .userIdentity(HOSTED_VESPA_OPERATOR), "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Deployment of system applications during a system upgrade is not allowed\"}", 400); - tester.upgradeSystem(tester.controller().versionStatus().controllerVersion().get().versionNumber()); + deploymentTester.controllerTester().upgradeSystem(deploymentTester.controller().versionStatus().controllerVersion().get().versionNumber()); tester.assertResponse(request("/application/v4/tenant/hosted-vespa/application/routing/environment/prod/region/us-central-1/instance/default/deploy", POST) .data(noAppEntity) .userIdentity(HOSTED_VESPA_OPERATOR), @@ -1005,27 +900,14 @@ public class ApplicationApiTest extends ControllerContainerTest { @Test public void testSortsDeploymentsAndJobs() { - tester.computeVersionStatus(); - // Deploy ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .instances("instance1") .region("us-east-3") .build(); - ApplicationId id = createTenantAndApplication(); - long projectId = 1; - MultiPartStreamer deployData = createApplicationDeployData(Optional.empty(), false); - startAndTestChange(controllerTester, id, projectId, applicationPackage, deployData, 100); - - // us-east-3 - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-east-3/deploy", POST) - .data(deployData) - .screwdriverIdentity(SCREWDRIVER_ID), - new File("deploy-result.json")); - controllerTester.jobCompletion(JobType.productionUsEast3) - .application(id) - .projectId(projectId) - .submit(); + + var app = deploymentTester.newDeploymentContext("tenant1", "application1", "instance1"); + app.submit(applicationPackage).deploy(); // New zone is added before us-east-3 applicationPackage = new ApplicationPackageBuilder() @@ -1035,31 +917,13 @@ public class ApplicationApiTest extends ControllerContainerTest { .region("us-west-1") .region("us-east-3") .build(); - startAndTestChange(controllerTester, id, projectId, applicationPackage, deployData, 101); - - // us-west-1 - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/deploy", POST) - .data(deployData) - .screwdriverIdentity(SCREWDRIVER_ID), - new File("deploy-result.json")); - controllerTester.jobCompletion(JobType.productionUsWest1) - .application(id) - .projectId(projectId) - .submit(); + app.submit(applicationPackage).runJob(JobType.systemTest).runJob(JobType.stagingTest).runJob(JobType.productionUsWest1); setZoneInRotation("rotation-fqdn-1", ZoneId.from("prod", "us-west-1")); - // us-east-3 - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-east-3/deploy", POST) - .data(deployData) - .screwdriverIdentity(SCREWDRIVER_ID), - new File("deploy-result.json")); - controllerTester.jobCompletion(JobType.productionUsEast3) - .application(id) - .projectId(projectId) - .submit(); + app.runJob(JobType.stagingTest).runJob(JobType.productionUsEast3); - setDeploymentMaintainedInfo(controllerTester); + setDeploymentMaintainedInfo(); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", GET) .userIdentity(USER_ID), new File("instance-without-change-multiple-deployments.json")); @@ -1067,7 +931,7 @@ public class ApplicationApiTest extends ControllerContainerTest { @Test public void testMeteringResponses() { - MockMeteringClient mockMeteringClient = (MockMeteringClient) controllerTester.containerTester().serviceRegistry().meteringService(); + MockMeteringClient mockMeteringClient = tester.serviceRegistry().meteringService(); // Mock response for MeteringClient ResourceAllocation currentSnapshot = new ResourceAllocation(1, 2, 3); @@ -1090,7 +954,7 @@ public class ApplicationApiTest extends ControllerContainerTest { @Test public void testTenantCostResponse() { ApplicationId applicationId = createTenantAndApplication(); - MockTenantCost mockTenantCost = (MockTenantCost) controllerTester.containerTester().serviceRegistry().tenantCost(); + MockTenantCost mockTenantCost = deploymentTester.controllerTester().serviceRegistry().tenantCost(); mockTenantCost.setMonthsWithMetering( new TreeSet<>(Set.of( @@ -1127,7 +991,6 @@ public class ApplicationApiTest extends ControllerContainerTest { @Test public void testErrorResponses() throws Exception { - tester.computeVersionStatus(); createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); // PUT (update) non-existing tenant returns 403 as tenant access cannot be determined when the tenant does not exist @@ -1216,7 +1079,7 @@ public class ApplicationApiTest extends ControllerContainerTest { "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Could not create 'tenant1.application1.instance1': Instance already exists\"}", 400); - ConfigServerMock configServer = serviceRegistry().configServerMock(); + ConfigServerMock configServer = tester.serviceRegistry().configServerMock(); configServer.throwOnNextPrepare(new ConfigServerException(new URI("server-url"), "Failed to prepare application", ConfigServerException.ErrorCode.INVALID_APPLICATION_PACKAGE, null)); // GET non-existent application package @@ -1431,87 +1294,67 @@ public class ApplicationApiTest extends ControllerContainerTest { } @Test - public void deployment_fails_on_illegal_domain_in_deployment_spec() { + public void athenz_service_must_be_allowed_to_launch_and_be_under_tenant_domain() { ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .upgradePolicy("default") .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("another.domain"), com.yahoo.config.provision.AthenzService.from("service")) .environment(Environment.prod) .region("us-west-1") .build(); - long screwdriverProjectId = 123; createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); - configureAthenzIdentity(new com.yahoo.vespa.athenz.api.AthenzService(new AthenzDomain("another.domain"), "service"), true); - - Application application = controllerTester.createApplication(ATHENZ_TENANT_DOMAIN.getName(), "tenant1", "application1", "default"); - ScrewdriverId screwdriverId = new ScrewdriverId(Long.toString(screwdriverProjectId)); - controllerTester.authorize(ATHENZ_TENANT_DOMAIN, screwdriverId, ApplicationAction.deploy, application.id()); - - controllerTester.jobCompletion(JobType.component) - .application(application) - .projectId(screwdriverProjectId) - .uploadArtifact(applicationPackage) - .submit(); - - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default/", POST) - .data(createApplicationDeployData(applicationPackage, false)) + + deploymentTester.controllerTester().createTenant("tenant1", ATHENZ_TENANT_DOMAIN.getName(), 1234L); + var application = deploymentTester.newDeploymentContext("tenant1", "application1", "default"); + ScrewdriverId screwdriverId = new ScrewdriverId("123"); + addScrewdriverUserToDeployRole(screwdriverId, ATHENZ_TENANT_DOMAIN, application.instanceId().application()); + + allowLaunchOfService(new com.yahoo.vespa.athenz.api.AthenzService(new AthenzDomain("another.domain"), "service")); + // Submit a package with a service under a different Athenz domain from that of the tenant + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/submit/", POST) + .data(createApplicationSubmissionData(applicationPackage, 123)) .screwdriverIdentity(screwdriverId), "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Athenz domain in deployment.xml: [another.domain] must match tenant domain: [domain1]\"}", 400); - } - - @Test - public void deployment_succeeds_when_correct_domain_is_used() { - ApplicationPackage applicationPackage = new ApplicationPackageBuilder() + // Set the correct domain in the application package, but do not yet allow Vespa to launch the service. + applicationPackage = new ApplicationPackageBuilder() .upgradePolicy("default") .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain1"), com.yahoo.config.provision.AthenzService.from("service")) .environment(Environment.prod) .region("us-west-1") .build(); - long screwdriverProjectId = 123; - ScrewdriverId screwdriverId = new ScrewdriverId(Long.toString(screwdriverProjectId)); - createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); - configureAthenzIdentity(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service"), true); - - Application application = controllerTester.createApplication(ATHENZ_TENANT_DOMAIN.getName(), "tenant1", "application1", "default"); - controllerTester.authorize(ATHENZ_TENANT_DOMAIN, screwdriverId, ApplicationAction.deploy, application.id()); - - // Allow systemtest to succeed by notifying completion of component - controllerTester.jobCompletion(JobType.component) - .application(application) - .projectId(screwdriverProjectId) - .uploadArtifact(applicationPackage) - .submit(); - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default/", POST) - .data(createApplicationDeployData(applicationPackage, false)) + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/submit", POST) + .data(createApplicationSubmissionData(applicationPackage, 123)) .screwdriverIdentity(screwdriverId), - new File("deploy-result.json")); + "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Not allowed to launch Athenz service domain1.service\"}", + 400); + + // Allow Vespa to launch the Athenz service. + allowLaunchOfService(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service")); + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/submit/", POST) + .data(createApplicationSubmissionData(applicationPackage, 123)) + .screwdriverIdentity(screwdriverId), + "{\"message\":\"Application package version: 1.0.1-commit1, source revision of repository 'repository1', branch 'master' with commit 'commit1', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\"}"); } @Test - public void deployment_fails_for_personal_tenants_when_athenzdomain_specified_and_user_not_admin() { + public void personal_deployment_with_athenz_service_requires_user_is_admin() { // Setup - tester.computeVersionStatus(); UserId tenantAdmin = new UserId("tenant-admin"); UserId userId = new UserId("new-user"); createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, tenantAdmin); - configureAthenzIdentity(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service"), true); + allowLaunchOfService(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service")); // Create tenant // PUT (create) the authenticated user - byte[] data = new byte[0]; tester.assertResponse(request("/application/v4/user?user=new_user&domain=by", PUT) - .data(data) .userIdentity(userId), // Normalized to by-new-user by API new File("create-user-response.json")); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() - .upgradePolicy("default") .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain1"), com.yahoo.config.provision.AthenzService.from("service")) - .environment(Environment.dev) - .region("us-west-1") .build(); // POST (deploy) an application to a dev zone @@ -1523,252 +1366,88 @@ public class ApplicationApiTest extends ControllerContainerTest { expectedResult, 400); - } - - @Test - public void deployment_succeeds_for_personal_tenants_when_user_is_tenant_admin() { - - // Setup - tester.computeVersionStatus(); - UserId tenantAdmin = new UserId("new_user"); - createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, tenantAdmin); - configureAthenzIdentity(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service"), true); - - // Create tenant - // PUT (create) the authenticated user - byte[] data = new byte[0]; - tester.assertResponse(request("/application/v4/user?user=new_user&domain=by", PUT) - .data(data) - .userIdentity(tenantAdmin), // Normalized to by-new-user by API - new File("create-user-response.json")); + createTenantAndApplication(); + // POST (deploy) an application to dev through a deployment job + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/new-user/deploy/dev-us-east-1", POST) + .data(entity) + .userIdentity(userId), + expectedResult, + 400); - ApplicationPackage applicationPackage = new ApplicationPackageBuilder() - .upgradePolicy("default") - .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain1"), com.yahoo.config.provision.AthenzService.from("service")) - .environment(Environment.dev) - .region("us-west-1") - .build(); + // Add "new-user" to the admin role, to allow service launches. + tester.athenzClientFactory().getSetup() + .domains.get(ATHENZ_TENANT_DOMAIN) + .admin(HostedAthenzIdentities.from(userId)); // POST (deploy) an application to a dev zone - MultiPartStreamer entity = createApplicationDeployData(applicationPackage, true); tester.assertResponse(request("/application/v4/tenant/by-new-user/application/application1/environment/dev/region/us-west-1/instance/default", POST) .data(entity) - .userIdentity(tenantAdmin), + .userIdentity(userId), new File("deploy-result.json")); - } - @Test - public void deployment_fails_when_athenz_service_cannot_be_launched() { - ApplicationPackage applicationPackage = new ApplicationPackageBuilder() - .upgradePolicy("default") - .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain1"), com.yahoo.config.provision.AthenzService.from("service")) - .environment(Environment.prod) - .region("us-west-1") - .build(); - long screwdriverProjectId = 123; - ScrewdriverId screwdriverId = new ScrewdriverId(Long.toString(screwdriverProjectId)); - - createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); - configureAthenzIdentity(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service"), false); - - Application application = controllerTester.createApplication(ATHENZ_TENANT_DOMAIN.getName(), "tenant1", "application1", "default"); - controllerTester.authorize(ATHENZ_TENANT_DOMAIN, screwdriverId, ApplicationAction.deploy, application.id()); - - // Allow systemtest to succeed by notifying completion of system test - controllerTester.jobCompletion(JobType.component) - .application(application) - .projectId(screwdriverProjectId) - .uploadArtifact(applicationPackage) - .submit(); - - String expectedResult="{\"error-code\":\"BAD_REQUEST\",\"message\":\"Not allowed to launch Athenz service domain1.service\"}"; - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default/", POST) - .data(createApplicationDeployData(applicationPackage, false)) - .screwdriverIdentity(screwdriverId), - expectedResult, - 400); - - } - - @Test - public void redeployment_succeeds_when_not_specifying_versions_or_application_package() { - // Setup - addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR)); - tester.computeVersionStatus(); - - ApplicationPackage applicationPackage = new ApplicationPackageBuilder() - .upgradePolicy("default") - .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain1"), com.yahoo.config.provision.AthenzService.from("service")) - .environment(Environment.prod) - .region("us-west-1") - .build(); - long screwdriverProjectId = 123; - ScrewdriverId screwdriverId = new ScrewdriverId(Long.toString(screwdriverProjectId)); - - createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); - configureAthenzIdentity(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service"), true); - - Application application = controllerTester.createApplication(ATHENZ_TENANT_DOMAIN.getName(), "tenant1", "application1", "default"); - controllerTester.authorize(ATHENZ_TENANT_DOMAIN, screwdriverId, ApplicationAction.deploy, application.id()); - - // Allow systemtest to succeed by notifying completion of component - controllerTester.jobCompletion(JobType.component) - .application(application) - .projectId(screwdriverProjectId) - .uploadArtifact(applicationPackage) - .submit(); - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default/", POST) - .data(createApplicationDeployData(applicationPackage, false)) - .screwdriverIdentity(screwdriverId), - new File("deploy-result.json")); - - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default/", POST) - .data(createApplicationDeployData(Optional.empty(), true)) - .userIdentity(HOSTED_VESPA_OPERATOR), - new File("deploy-result.json")); + // POST (deploy) an application to dev through a deployment job + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/new-user/deploy/dev-us-east-1", POST) + .data(entity) + .userIdentity(userId), + "{\"message\":\"Deployment started in run 1 of dev-us-east-1 for tenant1.application1.new-user. This may take about 15 minutes the first time.\",\"run\":1}"); } - @Test public void testJobStatusReporting() { addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR)); - tester.computeVersionStatus(); - long projectId = 1; - Application app = controllerTester.createApplication(); - ApplicationPackage applicationPackage = new ApplicationPackageBuilder() - .environment(Environment.prod) - .region("us-central-1") - .build(); + var app = deploymentTester.newDeploymentContext(createTenantAndApplication()); - Version vespaVersion = new Version("6.1"); // system version from mock config server client + Version vespaVersion = tester.configServer().initialVersion(); // system version from mock config server client - BuildJob job = new BuildJob(report -> notifyCompletion(report, controllerTester), controllerTester.containerTester().serviceRegistry().artifactRepositoryMock()) - .application(app) - .projectId(projectId); - job.type(JobType.component).uploadArtifact(applicationPackage).submit(); - controllerTester.deploy(app.id().defaultInstance(), applicationPackage, TEST_ZONE); - ((ManualClock) controllerTester.controller().clock()).advance(Duration.ofSeconds(1)); - job.type(JobType.systemTest).submit(); + app.submit(applicationPackageInstance1); + String data = "{\"jobName\":\"system-test\",\"instance\":\"instance1\"}"; - // Notifying about job started not by the controller fails var request = request("/application/v4/tenant/tenant1/application/application1/jobreport", POST) - .data(asJson(job.type(JobType.systemTest).report())) + .data(data) .userIdentity(HOSTED_VESPA_OPERATOR); - tester.assertResponse(request, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Notified of completion " + - "of system-test for tenant1.application1, but that has not been triggered; last was " + - controllerTester.controller().applications().requireInstance(app.id().defaultInstance()).deploymentJobs().jobStatus().get(JobType.systemTest).lastTriggered().get().at() + "\"}", 400); - // Notifying about unknown job fails - request = request("/application/v4/tenant/tenant1/application/application1/jobreport", POST) - .data(asJson(job.type(JobType.productionUsEast3).report())) - .userIdentity(HOSTED_VESPA_OPERATOR); + // Notifying about non-running job fails tester.assertResponse(request, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Notified of completion " + - "of production-us-east-3 for tenant1.application1, but that has not been triggered; last was never\"}", + "of system-test for tenant1.application1.instance1, but that has not been triggered; last was never\"}", 400); - // ... and assert it was recorded - JobStatus recordedStatus = - tester.controller().applications().getInstance(app.id().defaultInstance()).get().deploymentJobs().jobStatus().get(JobType.systemTest); + deploymentTester.triggerJobs(); + // Notifying about running jobs stores success status in DeploymentTrigger + tester.assertResponse(request, "{\"message\":\"ok\"}"); + JobStatus recordedStatus = app.instance().deploymentJobs().jobStatus().get(JobType.systemTest); assertNotNull("Status was recorded", recordedStatus); assertTrue(recordedStatus.isSuccess()); assertEquals(vespaVersion, recordedStatus.lastCompleted().get().platform()); - - recordedStatus = - tester.controller().applications().getInstance(app.id().defaultInstance()).get().deploymentJobs().jobStatus().get(JobType.productionApNortheast2); - assertNull("Status of never-triggered jobs is empty", recordedStatus); - assertTrue("All jobs have been run", tester.controller().applications().deploymentTrigger().jobsToRun().isEmpty()); - } - - @Test - public void testJobStatusReportingOutOfCapacity() { - controllerTester.containerTester().computeVersionStatus(); - - long projectId = 1; - Application app = controllerTester.createApplication(); - ApplicationPackage applicationPackage = new ApplicationPackageBuilder() - .environment(Environment.prod) - .region("us-central-1") - .build(); - - // Report job failing with out of capacity - BuildJob job = new BuildJob(report -> notifyCompletion(report, controllerTester), controllerTester.containerTester().serviceRegistry().artifactRepositoryMock()) - .application(app) - .projectId(projectId); - job.type(JobType.component).uploadArtifact(applicationPackage).submit(); - - controllerTester.deploy(app.id().defaultInstance(), applicationPackage, TEST_ZONE); - job.type(JobType.systemTest).submit(); - controllerTester.deploy(app.id().defaultInstance(), applicationPackage, STAGING_ZONE); - job.type(JobType.stagingTest).error(DeploymentJobs.JobError.outOfCapacity).submit(); - - // Appropriate error is recorded - JobStatus jobStatus = tester.controller().applications().getInstance(app.id().defaultInstance()).get() - .deploymentJobs() - .jobStatus() - .get(JobType.stagingTest); - assertFalse(jobStatus.isSuccess()); - assertEquals(DeploymentJobs.JobError.outOfCapacity, jobStatus.jobError().get()); } @Test public void applicationWithRoutingPolicy() { - Application app = controllerTester.createApplication(); + var app = deploymentTester.newDeploymentContext(createTenantAndApplication()); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) + .instances("instance1") .region("us-west-1") .build(); - controllerTester.deployCompletely(app, applicationPackage, 1, false); - RoutingPolicy policy = new RoutingPolicy(app.id().defaultInstance(), + app.submit(applicationPackage).deploy(); + RoutingPolicy policy = new RoutingPolicy(app.instanceId(), ClusterSpec.Id.from("default"), ZoneId.from(Environment.prod, RegionName.from("us-west-1")), HostName.from("lb-0-canonical-name"), Optional.of("dns-zone-1"), Set.of(EndpointId.of("c0"))); - tester.controller().curator().writeRoutingPolicies(app.id().defaultInstance(), Set.of(policy)); + tester.controller().curator().writeRoutingPolicies(app.instanceId(), Set.of(policy)); // GET application - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/default", GET) + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", GET) .userIdentity(USER_ID), new File("instance-with-routing-policy.json")); // GET deployment - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default", GET) + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/instance1", GET) .userIdentity(USER_ID), new File("deployment-with-routing-policy.json")); } - private void notifyCompletion(DeploymentJobs.JobReport report, ContainerControllerTester tester) { - assertResponse(request("/application/v4/tenant/tenant1/application/application1/jobreport", POST) - .userIdentity(HOSTED_VESPA_OPERATOR) - .data(asJson(report)) - .get(), - 200, "{\"message\":\"ok\"}"); - tester.controller().applications().deploymentTrigger().triggerReadyJobs(); - } - - private static byte[] asJson(DeploymentJobs.JobReport report) { - Slime slime = new Slime(); - Cursor cursor = slime.setObject(); - cursor.setLong("projectId", report.projectId()); - cursor.setString("jobName", report.jobType().jobName()); - cursor.setLong("buildNumber", report.buildNumber()); - report.jobError().ifPresent(jobError -> cursor.setString("jobError", jobError.name())); - report.version().flatMap(ApplicationVersion::source).ifPresent(sr -> { - Cursor sourceRevision = cursor.setObject("sourceRevision"); - sourceRevision.setString("repository", sr.repository()); - sourceRevision.setString("branch", sr.branch()); - sourceRevision.setString("commit", sr.commit()); - }); - cursor.setString("tenant", report.applicationId().tenant().value()); - cursor.setString("application", report.applicationId().application().value()); - cursor.setString("instance", report.applicationId().instance().value()); - try { - return SlimeUtils.toJsonBytes(slime); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - private MultiPartStreamer createApplicationDeployData(ApplicationPackage applicationPackage, boolean deployDirectly) { return createApplicationDeployData(Optional.of(applicationPackage), deployDirectly); } @@ -1785,8 +1464,9 @@ public class ApplicationApiTest extends ControllerContainerTest { return streamer; } - private MultiPartStreamer createApplicationSubmissionData(ApplicationPackage applicationPackage) { - return new MultiPartStreamer().addJson(EnvironmentResource.SUBMIT_OPTIONS, "{\"repository\":\"repo\",\"branch\":\"master\",\"commit\":\"d00d\",\"authorEmail\":\"a@b\"}") + private MultiPartStreamer createApplicationSubmissionData(ApplicationPackage applicationPackage, long projectId) { + return new MultiPartStreamer().addJson(EnvironmentResource.SUBMIT_OPTIONS, "{\"repository\":\"repository1\",\"branch\":\"master\",\"commit\":\"commit1\"," + + "\"projectId\":" + projectId + ",\"authorEmail\":\"a@b\"}") .addBytes(EnvironmentResource.APPLICATION_ZIP, applicationPackage.zippedContent()) .addBytes(EnvironmentResource.APPLICATION_TEST_ZIP, "content".getBytes()); } @@ -1817,9 +1497,7 @@ public class ApplicationApiTest extends ControllerContainerTest { * mock setup to replicate the action. */ private void createAthenzDomainWithAdmin(AthenzDomain domain, UserId userId) { - AthenzClientFactoryMock mock = (AthenzClientFactoryMock) container.components() - .getComponent(AthenzClientFactoryMock.class.getName()); - AthenzDbMock.Domain domainMock = mock.getSetup().getOrCreateDomain(domain); + AthenzDbMock.Domain domainMock = tester.athenzClientFactory().getSetup().getOrCreateDomain(domain); domainMock.markAsVespaTenant(); domainMock.admin(AthenzUser.fromUserId(userId.id())); } @@ -1827,11 +1505,9 @@ public class ApplicationApiTest extends ControllerContainerTest { /** * Mock athenz service identity configuration. Simulates that configserver is allowed to launch a service */ - private void configureAthenzIdentity(com.yahoo.vespa.athenz.api.AthenzService service, boolean allowLaunch) { - AthenzClientFactoryMock mock = (AthenzClientFactoryMock) container.components() - .getComponent(AthenzClientFactoryMock.class.getName()); - AthenzDbMock.Domain domainMock = mock.getSetup().domains.computeIfAbsent(service.getDomain(), AthenzDbMock.Domain::new); - domainMock.services.put(service.getName(), new AthenzDbMock.Service(allowLaunch)); + private void allowLaunchOfService(com.yahoo.vespa.athenz.api.AthenzService service) { + AthenzDbMock.Domain domainMock = tester.athenzClientFactory().getSetup().getOrCreateDomain(service.getDomain()); + domainMock.services.put(service.getName(), new AthenzDbMock.Service(true)); } @@ -1841,12 +1517,8 @@ public class ApplicationApiTest extends ControllerContainerTest { */ private void addScrewdriverUserToDeployRole(ScrewdriverId screwdriverId, AthenzDomain domain, - com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId applicationId) { - AthenzClientFactoryMock mock = (AthenzClientFactoryMock) container.components() - .getComponent(AthenzClientFactoryMock.class.getName()); - AthenzIdentity screwdriverIdentity = HostedAthenzIdentities.from(screwdriverId); - AthenzDbMock.Application athenzApplication = mock.getSetup().domains.get(domain).applications.get(applicationId); - athenzApplication.addRoleMember(ApplicationAction.deploy, screwdriverIdentity); + ApplicationName application) { + tester.authorize(domain, HostedAthenzIdentities.from(screwdriverId), ApplicationAction.deploy, application); } private ApplicationId createTenantAndApplication() { @@ -1860,65 +1532,19 @@ public class ApplicationApiTest extends ControllerContainerTest { .userIdentity(USER_ID) .oktaAccessToken(OKTA_AT).oktaIdentityToken(OKTA_IT), new File("instance-reference.json")); - addScrewdriverUserToDeployRole(SCREWDRIVER_ID, ATHENZ_TENANT_DOMAIN, - new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId("application1")); + addScrewdriverUserToDeployRole(SCREWDRIVER_ID, ATHENZ_TENANT_DOMAIN, ApplicationName.from("application1")); return ApplicationId.from("tenant1", "application1", "instance1"); } - private void startAndTestChange(ContainerControllerTester controllerTester, ApplicationId application, - long projectId, ApplicationPackage applicationPackage, - MultiPartStreamer deployData, long buildNumber) { - ContainerTester tester = controllerTester.containerTester(); - - // Trigger application change - controllerTester.containerTester().serviceRegistry().artifactRepositoryMock() - .put(application, applicationPackage,"1.0." + buildNumber + "-commit1"); - controllerTester.jobCompletion(JobType.component) - .application(application) - .projectId(projectId) - .buildNumber(buildNumber) - .submit(); - - // system-test - String testPath = String.format("/application/v4/tenant/%s/application/%s/instance/%s/environment/test/region/us-east-1", - application.tenant().value(), application.application().value(), application.instance().value()); - tester.assertResponse(request(testPath, POST) - .data(deployData) - .screwdriverIdentity(SCREWDRIVER_ID), - new File("deploy-result.json")); - tester.assertResponse(request(testPath, DELETE) - .screwdriverIdentity(SCREWDRIVER_ID), - "{\"message\":\"Deactivated " + application + " in test.us-east-1\"}"); - controllerTester.jobCompletion(JobType.systemTest) - .application(application) - .projectId(projectId) - .submit(); - - // staging - String stagingPath = String.format("/application/v4/tenant/%s/application/%s/instance/%s/environment/staging/region/us-east-3", - application.tenant().value(), application.application().value(), application.instance().value()); - tester.assertResponse(request(stagingPath, POST) - .data(deployData) - .screwdriverIdentity(SCREWDRIVER_ID), - new File("deploy-result.json")); - tester.assertResponse(request(stagingPath, DELETE) - .screwdriverIdentity(SCREWDRIVER_ID), - "{\"message\":\"Deactivated " + application + " in staging.us-east-3\"}"); - controllerTester.jobCompletion(JobType.stagingTest) - .application(application) - .projectId(projectId) - .submit(); - } - /** * Cluster info, utilization and application and deployment metrics are maintained async by maintainers. * * This sets these values as if the maintainers has been ran. */ - private void setDeploymentMaintainedInfo(ContainerControllerTester controllerTester) { - for (Application application : controllerTester.controller().applications().asList()) { - controllerTester.controller().applications().lockApplicationOrThrow(application.id(), lockedApplication -> { + private void setDeploymentMaintainedInfo() { + for (Application application : deploymentTester.applications().asList()) { + deploymentTester.applications().lockApplicationOrThrow(application.id(), lockedApplication -> { lockedApplication = lockedApplication.with(new ApplicationMetrics(0.5, 0.7)); for (Instance instance : application.instances().values()) { @@ -1938,18 +1564,14 @@ public class ApplicationApiTest extends ControllerContainerTest { .with(deployment.zone(), metrics) .recordActivityAt(Instant.parse("2018-06-01T10:15:30.00Z"), deployment.zone())); } - controllerTester.controller().applications().store(lockedApplication); + deploymentTester.applications().store(lockedApplication); } }); } } - private ServiceRegistryMock serviceRegistry() { - return (ServiceRegistryMock) tester.container().components().getComponent(ServiceRegistryMock.class.getName()); - } - private void setZoneInRotation(String rotationName, ZoneId zone) { - serviceRegistry().globalRoutingServiceMock().setStatus(rotationName, zone, com.yahoo.vespa.hosted.controller.api.integration.routing.RotationStatus.IN); + tester.serviceRegistry().globalRoutingServiceMock().setStatus(rotationName, zone, com.yahoo.vespa.hosted.controller.api.integration.routing.RotationStatus.IN); new RotationStatusUpdater(tester.controller(), Duration.ofDays(1), new JobControl(tester.controller().curator())).run(); } @@ -1965,12 +1587,12 @@ public class ApplicationApiTest extends ControllerContainerTest { private void registerContact(long propertyId) { PropertyId p = new PropertyId(String.valueOf(propertyId)); - serviceRegistry().contactRetrieverMock().addContact(p, new Contact(URI.create("www.issues.tld/" + p.id()), - URI.create("www.contacts.tld/" + p.id()), - URI.create("www.properties.tld/" + p.id()), - List.of(Collections.singletonList("alice"), + tester.serviceRegistry().contactRetrieverMock().addContact(p, new Contact(URI.create("www.issues.tld/" + p.id()), + URI.create("www.contacts.tld/" + p.id()), + URI.create("www.properties.tld/" + p.id()), + List.of(Collections.singletonList("alice"), Collections.singletonList("bob")), - "queue", Optional.empty())); + "queue", Optional.empty())); } private static class RequestBuilder implements Supplier<Request> { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java index daf1a4c2ea7..8e3a3b8d727 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java @@ -33,9 +33,7 @@ import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobTy import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.stagingTest; import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.systemTest; import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Status.FAILURE; -import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTester.instanceId; import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.applicationPackage; -import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTester.testerId; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.deploymentFailed; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.installationFailed; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running; @@ -51,60 +49,59 @@ public class JobControllerApiHandlerHelperTest { @Test public void testResponses() { DeploymentTester tester = new DeploymentTester(); + var app = tester.newDeploymentContext(); tester.clock().setInstant(Instant.EPOCH); // Revision 1 gets deployed everywhere. - ApplicationVersion revision1 = tester.newSubmission(); - tester.deployNewSubmission(revision1); + app.submit().deploy(); + ApplicationVersion revision1 = app.lastSubmission().get(); assertEquals(1000, tester.application().projectId().getAsLong()); tester.clock().advance(Duration.ofMillis(1000)); // Revision 2 gets deployed everywhere except in us-east-3. - ApplicationVersion revision2 = tester.newSubmission(); - tester.runJob(systemTest); - tester.runJob(stagingTest); - tester.runJob(productionUsCentral1); + ApplicationVersion revision2 = app.submit().lastSubmission().get(); + app.runJob(systemTest); + app.runJob(stagingTest); + app.runJob(productionUsCentral1); tester.triggerJobs(); // us-east-3 eats the deployment failure and fails before deployment, while us-west-1 fails after. tester.configServer().throwOnNextPrepare(new ConfigServerException(URI.create("url"), "ERROR!", INVALID_APPLICATION_PACKAGE, null)); tester.runner().run(); - assertEquals(deploymentFailed, tester.jobs().last(instanceId, productionUsEast3).get().status()); + assertEquals(deploymentFailed, tester.jobs().last(app.instanceId(), productionUsEast3).get().status()); ZoneId usWest1 = productionUsWest1.zone(tester.controller().system()); - tester.configServer().convergeServices(instanceId, usWest1); - tester.configServer().convergeServices(testerId.id(), usWest1); - tester.setEndpoints(instanceId, usWest1); - tester.setEndpoints(testerId.id(), usWest1); + tester.configServer().convergeServices(app.instanceId(), usWest1); + tester.configServer().convergeServices(app.testerId().id(), usWest1); + tester.setEndpoints(app.instanceId(), usWest1); + tester.setEndpoints(app.testerId().id(), usWest1); tester.runner().run(); tester.cloud().set(FAILURE); tester.runner().run(); - assertEquals(testFailure, tester.jobs().last(instanceId, productionUsWest1).get().status()); - assertEquals(revision2, tester.instance().deployments().get(productionUsCentral1.zone(tester.controller().system())).applicationVersion()); - assertEquals(revision1, tester.instance().deployments().get(productionUsEast3.zone(tester.controller().system())).applicationVersion()); - assertEquals(revision2, tester.instance().deployments().get(productionUsWest1.zone(tester.controller().system())).applicationVersion()); + assertEquals(testFailure, tester.jobs().last(app.instanceId(), productionUsWest1).get().status()); + assertEquals(revision2, app.deployment(productionUsCentral1.zone(tester.controller().system())).applicationVersion()); + assertEquals(revision1, app.deployment(productionUsEast3.zone(tester.controller().system())).applicationVersion()); + assertEquals(revision2, app.deployment(productionUsWest1.zone(tester.controller().system())).applicationVersion()); tester.clock().advance(Duration.ofMillis(1000)); // Revision 3 starts. - tester.newSubmission(); - tester.runJob(systemTest); - tester.runJob(stagingTest); - tester.triggerJobs(); // Starts a run for us-central-1. - tester.triggerJobs(); // Starts a new staging test run. + app.submit() + .runJob(systemTest).runJob(stagingTest); + tester.triggerJobs(); // Starts runs for us-central-1 and a new staging test run. tester.runner().run(); - assertEquals(running, tester.jobs().last(instanceId, productionUsCentral1).get().status()); - assertEquals(running, tester.jobs().last(instanceId, stagingTest).get().status()); + assertEquals(running, tester.jobs().last(app.instanceId(), productionUsCentral1).get().status()); + assertEquals(running, tester.jobs().last(app.instanceId(), stagingTest).get().status()); - // Staging is expired, and the job fails and won't be retried immediately. - tester.controller().applications().deactivate(instanceId, stagingTest.zone(tester.controller().system())); + // Staging deployment expires, the job fails, and won't be retried immediately. + tester.controller().applications().deactivate(app.instanceId(), stagingTest.zone(tester.controller().system())); tester.runner().run(); - assertEquals(installationFailed, tester.jobs().last(instanceId, stagingTest).get().status()); + assertEquals(installationFailed, tester.jobs().last(app.instanceId(), stagingTest).get().status()); tester.clock().advance(Duration.ofMillis(100_000)); // More than the minute within which there are immediate retries. tester.triggerJobs(); - assertEquals(installationFailed, tester.jobs().last(instanceId, stagingTest).get().status()); + assertEquals(installationFailed, tester.jobs().last(app.instanceId(), stagingTest).get().status()); // System upgrades to a new version, which won't yet start. Version platform = new Version("7.1"); @@ -117,36 +114,38 @@ public class JobControllerApiHandlerHelperTest { // Only us-east-3 is verified, on revision1. // staging-test has 4 runs: one success without sources on revision1, one success from revision1 to revision2, // one success from revision2 to revision3 and one failure from revision1 to revision3. - assertResponse(JobControllerApiHandlerHelper.runResponse(tester.jobs().runs(instanceId, stagingTest), URI.create("https://some.url:43/root")), "staging-runs.json"); - assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(instanceId, productionUsEast3).get().id(), "0"), "us-east-3-log-without-first.json"); - assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), instanceId, URI.create("https://some.url:43/root/")), "overview.json"); + assertResponse(JobControllerApiHandlerHelper.runResponse(tester.jobs().runs(app.instanceId(), stagingTest), URI.create("https://some.url:43/root")), "staging-runs.json"); + assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(app.instanceId(), productionUsEast3).get().id(), "0"), "us-east-3-log-without-first.json"); + assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), app.instanceId(), URI.create("https://some.url:43/root/")), "overview.json"); - tester.runJob(instanceId, JobType.devAwsUsEast2a, applicationPackage); - assertResponse(JobControllerApiHandlerHelper.runResponse(tester.jobs().runs(instanceId, devAwsUsEast2a), URI.create("https://some.url:43/root")), "dev-aws-us-east-2a-runs.json"); + app.runJob(devAwsUsEast2a, applicationPackage); + assertResponse(JobControllerApiHandlerHelper.runResponse(tester.jobs().runs(app.instanceId(), devAwsUsEast2a), URI.create("https://some.url:43/root")), "dev-aws-us-east-2a-runs.json"); } @Test public void testDevResponses() { DeploymentTester tester = new DeploymentTester(); + var app = tester.newDeploymentContext(); tester.clock().setInstant(Instant.EPOCH); ZoneId zone = JobType.devUsEast1.zone(tester.controller().system()); - tester.jobs().deploy(instanceId, JobType.devUsEast1, Optional.empty(), applicationPackage); + tester.jobs().deploy(app.instanceId(), JobType.devUsEast1, Optional.empty(), applicationPackage); tester.configServer().setLogStream("1554970337.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"); - assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(instanceId, devUsEast1).get().id(), null), "dev-us-east-1-log-first-part.json"); + assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(app.instanceId(), devUsEast1).get().id(), null), "dev-us-east-1-log-first-part.json"); tester.configServer().setLogStream("Nope, this won't be logged"); - tester.configServer().convergeServices(instanceId, zone); - tester.setEndpoints(instanceId, zone); + tester.configServer().convergeServices(app.instanceId(), zone); + tester.setEndpoints(app.instanceId(), zone); tester.runner().run(); - assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), instanceId, URI.create("https://some.url:43/root")), "dev-overview.json"); - assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(instanceId, devUsEast1).get().id(), "9"), "dev-us-east-1-log-second-part.json"); + assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), app.instanceId(), URI.create("https://some.url:43/root")), "dev-overview.json"); + assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(app.instanceId(), devUsEast1).get().id(), "9"), "dev-us-east-1-log-second-part.json"); } @Test public void testResponsesWithDirectDeployment() { var tester = new DeploymentTester(); + var app = tester.newDeploymentContext(); tester.clock().setInstant(Instant.EPOCH); var region = "us-west-1"; var applicationPackage = new ApplicationPackageBuilder().region(region).build(); @@ -155,7 +154,7 @@ public class JobControllerApiHandlerHelperTest { Optional.of(applicationPackage), new DeployOptions(true, Optional.empty(), false, false)); - assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), instanceId, URI.create("https://some.url:43/root/")), + assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), app.instanceId(), URI.create("https://some.url:43/root/")), "jobs-direct-deployment.json"); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json index ad86fa34cde..195219c691e 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json @@ -3,22 +3,22 @@ "application": "application2", "deployments": "http://localhost:8080/application/v4/tenant/tenant2/application/application2/job/", "latestVersion": { - "buildNumber": 42, - "hash": "1.0.42-commit1", + "buildNumber": 1, + "hash": "1.0.1-commit1", "source": { "gitRepository": "repository1", "gitBranch": "master", "gitCommit": "commit1" } }, - "projectId": 456, + "projectId": 1000, "deploying": { - "version": "(ignore)" + "version": "7" }, "outstandingChange": { "revision": { - "buildNumber": 42, - "hash": "(ignore)", + "buildNumber": 1, + "hash": "1.0.1-commit1", "source": { "gitRepository": "repository1", "gitBranch": "master", @@ -31,6 +31,11 @@ "instances": [ { "instance": "default", + "globalRotations": [], + "deployments": [] + }, + { + "instance": "instance1", "deploymentJobs": [ { "type": "system-test", @@ -39,21 +44,43 @@ "id": -1, "version": "7.0.0", "revision": { - "buildNumber": 42, - "hash": "1.0.42-commit1", + "buildNumber": 1, + "hash": "1.0.1-commit1", "source": { "gitRepository": "repository1", "gitBranch": "master", "gitCommit": "commit1" } }, - "reason": "Testing last changes outside prod", + "reason": "Testing deployment for production-us-west-1 (platform 7, application 1.0.1-commit1)", + "at": "(ignore)" + } + }, + { + "type": "staging-test", + "success": false, + "lastTriggered": { + "id": -1, + "version": "7.0.0", + "revision": { + "buildNumber": 1, + "hash": "1.0.1-commit1", + "source": { + "gitRepository": "repository1", + "gitBranch": "master", + "gitCommit": "commit1" + } + }, + "reason": "Testing deployment for production-us-west-1 (platform 7, application 1.0.1-commit1)", "at": "(ignore)" } } ], "changeBlockers": [], - "globalRotations": [], + "globalRotations": [ + "https://instance1--application2--tenant2.global.vespa.oath.cloud:4443/" + ], + "rotationId": "rotation-id-2", "deployments": [] } ], @@ -66,3 +93,4 @@ }, "activity": {} } + diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2.json index 6d89fa4ee20..c29944924dc 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2.json @@ -3,22 +3,22 @@ "application": "application2", "deployments": "http://localhost:8080/application/v4/tenant/tenant2/application/application2/job/", "latestVersion": { - "buildNumber": 42, - "hash": "1.0.42-commit1", + "buildNumber": 1, + "hash": "1.0.1-commit1", "source": { "gitRepository": "repository1", "gitBranch": "master", "gitCommit": "commit1" } }, - "projectId": 456, + "projectId": 1000, "deploying": { - "version": "(ignore)" + "version": "7" }, "outstandingChange": { "revision": { - "buildNumber": 42, - "hash": "(ignore)", + "buildNumber": 1, + "hash": "1.0.1-commit1", "source": { "gitRepository": "repository1", "gitBranch": "master", @@ -30,6 +30,11 @@ "instances": [ { "instance": "default", + "globalRotations": [], + "deployments": [] + }, + { + "instance": "instance1", "deploymentJobs": [ { "type": "system-test", @@ -38,21 +43,43 @@ "id": -1, "version": "7.0.0", "revision": { - "buildNumber": 42, - "hash": "1.0.42-commit1", + "buildNumber": 1, + "hash": "1.0.1-commit1", "source": { "gitRepository": "repository1", "gitBranch": "master", "gitCommit": "commit1" } }, - "reason": "Testing last changes outside prod", + "reason": "Testing deployment for production-us-west-1 (platform 7, application 1.0.1-commit1)", + "at": "(ignore)" + } + }, + { + "type": "staging-test", + "success": false, + "lastTriggered": { + "id": -1, + "version": "7.0.0", + "revision": { + "buildNumber": 1, + "hash": "1.0.1-commit1", + "source": { + "gitRepository": "repository1", + "gitBranch": "master", + "gitCommit": "commit1" + } + }, + "reason": "Testing deployment for production-us-west-1 (platform 7, application 1.0.1-commit1)", "at": "(ignore)" } } ], "changeBlockers": [], - "globalRotations": [], + "globalRotations": [ + "https://instance1--application2--tenant2.global.vespa.oath.cloud:4443/" + ], + "rotationId": "rotation-id-2", "deployments": [] } ], @@ -63,3 +90,4 @@ }, "activity": {} } + diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/delete-with-active-deployments.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/delete-with-active-deployments.json index c9ed2ad3391..25fed881dec 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/delete-with-active-deployments.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/delete-with-active-deployments.json @@ -1,4 +1,4 @@ { "error-code": "BAD_REQUEST", - "message": "Could not delete 'application 'tenant1.application1'': It has active deployments in: dev.us-west-1, prod.us-central-1" + "message": "Could not delete 'application 'tenant1.application1'': It has active deployments in: dev.us-east-1, prod.us-central-1" } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-with-routing-policy.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-with-routing-policy.json index 519c9b1c842..0fcbbdd925d 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-with-routing-policy.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-with-routing-policy.json @@ -1,29 +1,25 @@ { "tenant": "tenant1", "application": "application1", - "instance": "default", + "instance": "instance1", "environment": "prod", "region": "us-west-1", "endpoints": [ { "cluster": "default", "tls": true, - "url": "https://application1.tenant1.us-west-1.vespa.oath.cloud/" + "url": "https://instance1.application1.tenant1.us-west-1.vespa.oath.cloud/" } ], "serviceUrls": [ - "http://old-endpoint.vespa.yahooapis.com:4080", - "http://qrs-endpoint.vespa.yahooapis.com:4080", - "http://feeding-endpoint.vespa.yahooapis.com:4080", - "http://global-endpoint.vespa.yahooapis.com:4080", - "http://alias-endpoint.vespa.yahooapis.com:4080" + "https://instance1--application1--tenant1.us-west-1.prod.vespa:43" ], - "nodes": "http://localhost:8080/zone/v2/prod/us-west-1/nodes/v2/node/%3F&recursive=true&application=tenant1.application1.default", - "yamasUrl": "http://monitoring-system.test/?environment=prod®ion=us-west-1&application=tenant1.application1", + "nodes": "http://localhost:8080/zone/v2/prod/us-west-1/nodes/v2/node/%3F&recursive=true&application=tenant1.application1.instance1", + "yamasUrl": "http://monitoring-system.test/?environment=prod®ion=us-west-1&application=tenant1.application1.instance1", "version": "(ignore)", - "revision": "1.0.42-commit1", + "revision": "1.0.1-commit1", "deployTimeEpochMs": "(ignore)", - "screwdriverId": "1", + "screwdriverId": "1000", "gitRepository": "repository1", "gitBranch": "master", "gitCommit": "commit1", diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json index d62e39e42e7..4b3e4829c53 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json @@ -6,11 +6,7 @@ "region": "us-central-1", "endpoints": [], "serviceUrls": [ - "http://old-endpoint.vespa.yahooapis.com:4080", - "http://qrs-endpoint.vespa.yahooapis.com:4080", - "http://feeding-endpoint.vespa.yahooapis.com:4080", - "http://global-endpoint.vespa.yahooapis.com:4080", - "http://alias-endpoint.vespa.yahooapis.com:4080" + "https://instance1--application1--tenant1.us-central-1.prod.vespa:43" ], "nodes": "http://localhost:8080/zone/v2/prod/us-central-1/nodes/v2/node/%3F&recursive=true&application=tenant1.application1.instance1", "yamasUrl": "http://monitoring-system.test/?environment=prod®ion=us-central-1&application=tenant1.application1.instance1", diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1.json index c56a269b9d4..204927f6954 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1.json @@ -3,17 +3,13 @@ "application": "application1", "instance": "instance1", "environment": "dev", - "region": "us-west-1", + "region": "us-east-1", "endpoints": [], "serviceUrls": [ - "http://old-endpoint.vespa.yahooapis.com:4080", - "http://qrs-endpoint.vespa.yahooapis.com:4080", - "http://feeding-endpoint.vespa.yahooapis.com:4080", - "http://global-endpoint.vespa.yahooapis.com:4080", - "http://alias-endpoint.vespa.yahooapis.com:4080" + "https://instance1--application1--tenant1.us-east-1.dev.vespa:43" ], - "nodes": "http://localhost:8080/zone/v2/dev/us-west-1/nodes/v2/node/%3F&recursive=true&application=tenant1.application1.instance1", - "yamasUrl": "http://monitoring-system.test/?environment=dev®ion=us-west-1&application=tenant1.application1.instance1", + "nodes": "http://localhost:8080/zone/v2/dev/us-east-1/nodes/v2/node/%3F&recursive=true&application=tenant1.application1.instance1", + "yamasUrl": "http://monitoring-system.test/?environment=dev®ion=us-east-1&application=tenant1.application1.instance1", "version": "(ignore)", "revision": "(ignore)", "deployTimeEpochMs": "(ignore)", diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation-get.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation-get.json index 52aabab1584..a21b1558aee 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation-get.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation-get.json @@ -1 +1 @@ -{"globalrotationoverride":["upstream1",{"status":"in","reason":"","agent":"","timestamp":1497618757},"upstream1",{"status":"in","reason":"","agent":"","timestamp":1497618757}]} +{"globalrotationoverride":["cluster1.application1.tenant1.us-west-1.prod",{"status":"in","reason":"","agent":"","timestamp":1497618757}]} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-with-routing-policy.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-with-routing-policy.json index 91356bc83bf..0f759eaed55 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-with-routing-policy.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-with-routing-policy.json @@ -1,15 +1,14 @@ { "tenant": "tenant1", "application": "application1", - "instance": "default", - "deployments": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/default/job/", + "instance": "instance1", + "deployments": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/", "source": { "gitRepository": "repository1", "gitBranch": "master", "gitCommit": "commit1" }, - "projectId": 1, - "deployedInternally": false, + "projectId": 1000, "deploymentJobs": [ { "type": "system-test", @@ -19,44 +18,44 @@ "version": "(ignore)", "revision": { "buildNumber": "(ignore)", - "hash": "1.0.42-commit1", + "hash": "1.0.1-commit1", "source": { "gitRepository": "repository1", "gitBranch": "master", "gitCommit": "commit1" } }, - "reason": "Testing last changes outside prod", + "reason": "Testing deployment for production-us-west-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" }, "lastCompleted": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", - "hash": "1.0.42-commit1", + "hash": "1.0.1-commit1", "source": { "gitRepository": "repository1", "gitBranch": "master", "gitCommit": "commit1" } }, - "reason": "Testing last changes outside prod", + "reason": "Testing deployment for production-us-west-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" }, "lastSuccess": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", - "hash": "1.0.42-commit1", + "hash": "1.0.1-commit1", "source": { "gitRepository": "repository1", "gitBranch": "master", "gitCommit": "commit1" } }, - "reason": "Testing last changes outside prod", + "reason": "Testing deployment for production-us-west-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" } }, @@ -68,44 +67,44 @@ "version": "(ignore)", "revision": { "buildNumber": "(ignore)", - "hash": "1.0.42-commit1", + "hash": "1.0.1-commit1", "source": { "gitRepository": "repository1", "gitBranch": "master", "gitCommit": "commit1" } }, - "reason": "Testing deployment for production-us-west-1 (platform (ignore), application 1.0.42-commit1)", + "reason": "Testing deployment for production-us-west-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" }, "lastCompleted": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", - "hash": "1.0.42-commit1", + "hash": "1.0.1-commit1", "source": { "gitRepository": "repository1", "gitBranch": "master", "gitCommit": "commit1" } }, - "reason": "Testing deployment for production-us-west-1 (platform (ignore), application 1.0.42-commit1)", + "reason": "Testing deployment for production-us-west-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" }, "lastSuccess": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", - "hash": "1.0.42-commit1", + "hash": "1.0.1-commit1", "source": { "gitRepository": "repository1", "gitBranch": "master", "gitCommit": "commit1" } }, - "reason": "Testing deployment for production-us-west-1 (platform (ignore), application 1.0.42-commit1)", + "reason": "Testing deployment for production-us-west-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" } }, @@ -117,7 +116,7 @@ "version": "(ignore)", "revision": { "buildNumber": "(ignore)", - "hash": "1.0.42-commit1", + "hash": "1.0.1-commit1", "source": { "gitRepository": "repository1", "gitBranch": "master", @@ -128,11 +127,11 @@ "at": "(ignore)" }, "lastCompleted": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", - "hash": "1.0.42-commit1", + "hash": "1.0.1-commit1", "source": { "gitRepository": "repository1", "gitBranch": "master", @@ -143,11 +142,11 @@ "at": "(ignore)" }, "lastSuccess": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", - "hash": "1.0.42-commit1", + "hash": "1.0.1-commit1", "source": { "gitRepository": "repository1", "gitBranch": "master", @@ -162,14 +161,14 @@ "changeBlockers": [], "compileVersion": "(ignore)", "globalRotations": [ - "https://c0.application1.tenant1.global.vespa.oath.cloud/" + "https://c0.instance1.application1.tenant1.global.vespa.oath.cloud/" ], "instances": [ { "environment": "prod", "region": "us-west-1", - "instance": "default", - "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/default/environment/prod/region/us-west-1" + "instance": "instance1", + "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1" } ], "pemDeployKeys": [], diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-without-change-multiple-deployments.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-without-change-multiple-deployments.json index 582753d04d6..e8adfd579d5 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-without-change-multiple-deployments.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-without-change-multiple-deployments.json @@ -8,8 +8,7 @@ "gitBranch": "master", "gitCommit": "commit1" }, - "projectId": 1, - "deployedInternally": false, + "projectId": 1000, "deploymentJobs": [ { "type": "system-test", @@ -26,11 +25,11 @@ "gitCommit": "commit1" } }, - "reason": "Testing deployment for production-us-east-3 (platform 6.1, application 1.0.100-commit1 -> 1.0.101-commit1)", + "reason": "Testing deployment for production-us-west-1 (platform 6.1, application 1.0.2-commit1)", "at": "(ignore)" }, "lastCompleted": { - "id": 42, + "id": 2, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -41,11 +40,11 @@ "gitCommit": "commit1" } }, - "reason": "Testing deployment for production-us-east-3 (platform 6.1, application 1.0.100-commit1 -> 1.0.101-commit1)", + "reason": "Testing deployment for production-us-west-1 (platform 6.1, application 1.0.2-commit1)", "at": "(ignore)" }, "lastSuccess": { - "id": 42, + "id": 2, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -56,7 +55,7 @@ "gitCommit": "commit1" } }, - "reason": "Testing deployment for production-us-east-3 (platform 6.1, application 1.0.100-commit1 -> 1.0.101-commit1)", + "reason": "Testing deployment for production-us-west-1 (platform 6.1, application 1.0.2-commit1)", "at": "(ignore)" } }, @@ -75,11 +74,11 @@ "gitCommit": "commit1" } }, - "reason": "Testing deployment for production-us-east-3 (platform 6.1, application 1.0.100-commit1 -> 1.0.101-commit1)", + "reason": "Testing deployment for production-us-east-3 (platform 6.1, application 1.0.1-commit1 -> 1.0.2-commit1)", "at": "(ignore)" }, "lastCompleted": { - "id": 42, + "id": 3, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -90,11 +89,11 @@ "gitCommit": "commit1" } }, - "reason": "Testing deployment for production-us-east-3 (platform 6.1, application 1.0.100-commit1 -> 1.0.101-commit1)", + "reason": "Testing deployment for production-us-east-3 (platform 6.1, application 1.0.1-commit1 -> 1.0.2-commit1)", "at": "(ignore)" }, "lastSuccess": { - "id": 42, + "id": 3, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -105,7 +104,7 @@ "gitCommit": "commit1" } }, - "reason": "Testing deployment for production-us-east-3 (platform 6.1, application 1.0.100-commit1 -> 1.0.101-commit1)", + "reason": "Testing deployment for production-us-east-3 (platform 6.1, application 1.0.1-commit1 -> 1.0.2-commit1)", "at": "(ignore)" } }, @@ -128,7 +127,7 @@ "at": "(ignore)" }, "lastCompleted": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -143,7 +142,7 @@ "at": "(ignore)" }, "lastSuccess": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -177,7 +176,7 @@ "at": "(ignore)" }, "lastCompleted": { - "id": 42, + "id": 2, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -192,7 +191,7 @@ "at": "(ignore)" }, "lastSuccess": { - "id": 42, + "id": 2, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -211,7 +210,7 @@ "changeBlockers": [], "compileVersion": "(ignore)", "globalRotations": [ - "https://application1--tenant1.global.vespa.oath.cloud:4443/" + "https://instance1--application1--tenant1.global.vespa.oath.cloud:4443/" ], "rotationId": "rotation-id-1", "instances": [ diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance.json index 951b42ff07a..3664af7116c 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance.json @@ -20,7 +20,6 @@ } } }, - "deployedInternally": false, "deploymentJobs": [ { "type": "system-test", @@ -37,11 +36,11 @@ "gitCommit": "commit1" } }, - "reason": "Testing last changes outside prod", + "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" }, "lastCompleted": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -52,11 +51,11 @@ "gitCommit": "commit1" } }, - "reason": "Testing last changes outside prod", + "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" }, "lastSuccess": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -67,7 +66,7 @@ "gitCommit": "commit1" } }, - "reason": "Testing last changes outside prod", + "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" } }, @@ -86,11 +85,11 @@ "gitCommit": "commit1" } }, - "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.42-commit1)", + "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" }, "lastCompleted": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -101,11 +100,11 @@ "gitCommit": "commit1" } }, - "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.42-commit1)", + "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" }, "lastSuccess": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -116,13 +115,13 @@ "gitCommit": "commit1" } }, - "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.42-commit1)", + "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" } }, { "type": "production-us-central-1", - "success": false, + "success": true, "lastTriggered": { "id": -1, "version": "(ignore)", @@ -139,7 +138,7 @@ "at": "(ignore)" }, "lastCompleted": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -153,8 +152,8 @@ "reason": "New change available", "at": "(ignore)" }, - "firstFailing": { - "id": 42, + "lastSuccess": { + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -168,6 +167,25 @@ "reason": "New change available", "at": "(ignore)" } + }, + { + "type": "production-us-east-3", + "success": false, + "lastTriggered": { + "id": -1, + "version": "(ignore)", + "revision": { + "buildNumber": "(ignore)", + "hash": "(ignore)", + "source": { + "gitRepository": "repository1", + "gitBranch": "master", + "gitCommit": "commit1" + } + }, + "reason": "Available change in production-us-central-1", + "at": "(ignore)" + } } ], "changeBlockers": [ @@ -197,15 +215,15 @@ ], "compileVersion": "6.0.0", "globalRotations": [ - "https://application1--tenant1.global.vespa.oath.cloud:4443/" + "https://instance1--application1--tenant1.global.vespa.oath.cloud:4443/" ], "rotationId": "rotation-id-1", "instances": [ { "environment": "dev", - "region": "us-west-1", + "region": "us-east-1", "instance": "instance1", - "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/dev/region/us-west-1" + "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/dev/region/us-east-1" }, { "bcpStatus": { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance1-recursive.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance1-recursive.json index afa506d1e48..ddce3921518 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance1-recursive.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance1-recursive.json @@ -20,7 +20,6 @@ } } }, - "deployedInternally": false, "deploymentJobs": [ { "type": "system-test", @@ -37,11 +36,11 @@ "gitCommit": "commit1" } }, - "reason": "Testing last changes outside prod", + "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" }, "lastCompleted": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -52,11 +51,11 @@ "gitCommit": "commit1" } }, - "reason": "Testing last changes outside prod", + "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" }, "lastSuccess": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -67,7 +66,7 @@ "gitCommit": "commit1" } }, - "reason": "Testing last changes outside prod", + "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" } }, @@ -86,11 +85,11 @@ "gitCommit": "commit1" } }, - "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.42-commit1)", + "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" }, "lastCompleted": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -101,11 +100,11 @@ "gitCommit": "commit1" } }, - "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.42-commit1)", + "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" }, "lastSuccess": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -116,13 +115,13 @@ "gitCommit": "commit1" } }, - "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.42-commit1)", + "reason": "Testing deployment for production-us-central-1 (platform 6.1, application 1.0.1-commit1)", "at": "(ignore)" } }, { "type": "production-us-central-1", - "success": false, + "success": true, "lastTriggered": { "id": -1, "version": "(ignore)", @@ -139,7 +138,7 @@ "at": "(ignore)" }, "lastCompleted": { - "id": 42, + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -153,8 +152,8 @@ "reason": "New change available", "at": "(ignore)" }, - "firstFailing": { - "id": 42, + "lastSuccess": { + "id": 1, "version": "(ignore)", "revision": { "buildNumber": "(ignore)", @@ -168,6 +167,25 @@ "reason": "New change available", "at": "(ignore)" } + }, + { + "type": "production-us-east-3", + "success": false, + "lastTriggered": { + "id": -1, + "version": "(ignore)", + "revision": { + "buildNumber": "(ignore)", + "hash": "(ignore)", + "source": { + "gitRepository": "repository1", + "gitBranch": "master", + "gitCommit": "commit1" + } + }, + "reason": "Available change in production-us-central-1", + "at": "(ignore)" + } } ], "changeBlockers": [ @@ -197,7 +215,7 @@ ], "compileVersion": "(ignore)", "globalRotations": [ - "https://application1--tenant1.global.vespa.oath.cloud:4443/" + "https://instance1--application1--tenant1.global.vespa.oath.cloud:4443/" ], "rotationId": "rotation-id-1", "instances": [ diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs-direct-deployment.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs-direct-deployment.json index 5535e286dcd..85a3245c308 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs-direct-deployment.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs-direct-deployment.json @@ -1,79 +1,7 @@ { "devJobs": {}, - "deployments": [ - { - "us-west-1": { - "at": 0, - "application": { - "hash": "unknown" - }, - "verified": false, - "platform": "6.1" - } - } - ], + "deployments": [], "lastVersions": {}, "deploying": {}, - "jobs": { - "staging-test": { - "runs": [ - { - "reason": "Testing for productionUsWest1", - "wantedPlatform": "6.1", - "currentPlatform": "6.1", - "wantedApplication": { - "hash": "unknown" - }, - "currentApplication": { - "hash": "unknown" - }, - "tasks": { - "capacity": "running" - }, - "status": "pending" - } - ], - "url": "https://some.url:43/root/staging-test" - }, - "system-test": { - "runs": [ - { - "reason": "Testing for productionUsWest1", - "wantedPlatform": "6.1", - "currentPlatform": "6.1", - "wantedApplication": { - "hash": "unknown" - }, - "currentApplication": { - "hash": "unknown" - }, - "tasks": { - "capacity": "running" - }, - "status": "pending" - } - ], - "url": "https://some.url:43/root/system-test" - }, - "us-west-1": { - "runs": [ - { - "wantedPlatform": "6.1", - "currentPlatform": "6.1", - "wantedApplication": { - "hash": "unknown" - }, - "currentApplication": { - "hash": "unknown" - }, - "tasks": { - "staging-test": "pending", - "system-test": "pending" - }, - "status": "pending" - } - ], - "url": "https://some.url:43/root/production-us-west-1" - } - } + "jobs": {} } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs.json index 1a312f3c99c..b73c36c804b 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs.json @@ -3,51 +3,52 @@ "platform": { "platform": "6.1", "at": "(ignore)", - "pending": "Waiting for application change to 1.0.45-d00d to complete" + "pending": "Waiting for application change to 1.0.4-commit1 to complete" }, "application": { "application": { - "hash": "1.0.45-d00d", - "build": 45, + "hash": "1.0.4-commit1", + "build": 4, "source": { - "gitRepository": "repo", + "gitRepository": "repository1", "gitBranch": "master", - "gitCommit": "d00d" + "gitCommit": "commit1" } }, - "at": (ignore), - "deploying": "0 of 1 complete" + "at": "(ignore)", + "deploying": "0 of 3 complete" } }, "deploying": { "application": { - "hash": "1.0.45-d00d", - "build": 45, + "hash": "1.0.4-commit1", + "build": 4, "source": { - "gitRepository": "repo", + "gitRepository": "repository1", "gitBranch": "master", - "gitCommit": "d00d" + "gitCommit": "commit1" } } }, "deployments": [ + {}, {} ], "jobs": { "system-test": { "runs": [ { - "id": 1, + "id": 2, "status": "running", - "start": (ignore), + "start": "(ignore)", "wantedPlatform": "6.1", "wantedApplication": { - "hash": "1.0.45-d00d", - "build": 45, + "hash": "1.0.4-commit1", + "build": 4, "source": { - "gitRepository": "repo", + "gitRepository": "repository1", "gitBranch": "master", - "gitCommit": "d00d" + "gitCommit": "commit1" } }, "steps": { @@ -63,6 +64,40 @@ "report": "unfinished" }, "tasks": {}, + "log": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/system-test/run/2" + }, + { + "id": 1, + "status": "success", + "start": "(ignore)", + "end": "(ignore)", + "wantedPlatform": "6.1", + "wantedApplication": { + "hash": "1.0.1-commit1", + "build": 1, + "source": { + "gitRepository": "repository1", + "gitBranch": "master", + "gitCommit": "commit1" + } + }, + "steps": { + "deployTester": "succeeded", + "deployReal": "succeeded", + "installTester": "succeeded", + "installReal": "succeeded", + "startTests": "succeeded", + "endTests": "succeeded", + "copyVespaLogs": "succeeded", + "deactivateReal": "succeeded", + "deactivateTester": "succeeded", + "report": "succeeded" + }, + "tasks": { + "deploy": "succeeded", + "install": "succeeded", + "test": "succeeded" + }, "log": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/system-test/run/1" } ], @@ -71,17 +106,17 @@ "staging-test": { "runs": [ { - "id": 1, + "id": 2, "status": "running", - "start": (ignore), + "start": "(ignore)", "wantedPlatform": "6.1", "wantedApplication": { - "hash": "1.0.45-d00d", - "build": 45, + "hash": "1.0.4-commit1", + "build": 4, "source": { - "gitRepository": "repo", + "gitRepository": "repository1", "gitBranch": "master", - "gitCommit": "d00d" + "gitCommit": "commit1" } }, "steps": { @@ -99,32 +134,164 @@ "report": "unfinished" }, "tasks": {}, + "log": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/staging-test/run/2" + }, + { + "id": 1, + "status": "success", + "start": "(ignore)", + "end": "(ignore)", + "wantedPlatform": "6.1", + "wantedApplication": { + "hash": "1.0.1-commit1", + "build": 1, + "source": { + "gitRepository": "repository1", + "gitBranch": "master", + "gitCommit": "commit1" + } + }, + "steps": { + "deployTester": "succeeded", + "deployInitialReal": "succeeded", + "installInitialReal": "succeeded", + "deployReal": "succeeded", + "installTester": "succeeded", + "installReal": "succeeded", + "startTests": "succeeded", + "endTests": "succeeded", + "copyVespaLogs": "succeeded", + "deactivateReal": "succeeded", + "deactivateTester": "succeeded", + "report": "succeeded" + }, + "tasks": { + "deploy": "succeeded", + "install": "succeeded", + "test": "succeeded" + }, "log": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/staging-test/run/1" } ], "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/staging-test" }, - "us-west-1": { + "us-central-1": { "runs": [ { "status": "pending", "wantedPlatform": "6.1", "wantedApplication": { - "hash": "1.0.45-d00d", - "build": 45, + "hash": "1.0.4-commit1", + "build": 4, "source": { - "gitRepository": "repo", + "gitRepository": "repository1", "gitBranch": "master", - "gitCommit": "d00d" + "gitCommit": "commit1" } }, "tasks": { "system-test": "running", "staging-test": "running" } + }, + { + "id": 1, + "status": "success", + "start": "(ignore)", + "end": "(ignore)", + "wantedPlatform": "6.1", + "wantedApplication": { + "hash": "1.0.1-commit1", + "build": 1, + "source": { + "gitRepository": "repository1", + "gitBranch": "master", + "gitCommit": "commit1" + } + }, + "steps": { + "deployTester": "succeeded", + "deployReal": "succeeded", + "installTester": "succeeded", + "installReal": "succeeded", + "startTests": "succeeded", + "endTests": "succeeded", + "deactivateTester": "succeeded", + "report": "succeeded" + }, + "tasks": { + "deploy": "succeeded", + "install": "succeeded", + "test": "succeeded" + }, + "log": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-central-1/run/1" + } + ], + "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-central-1" + }, + "us-west-1": { + "runs": [ + { + "id": 1, + "status": "aborted", + "start": "(ignore)", + "wantedPlatform": "6.1", + "wantedApplication": { + "hash": "1.0.1-commit1", + "build": 1, + "source": { + "gitRepository": "repository1", + "gitBranch": "master", + "gitCommit": "commit1" + } + }, + "steps": { + "deployTester": "unfinished", + "deployReal": "unfinished", + "installTester": "unfinished", + "installReal": "unfinished", + "startTests": "unfinished", + "endTests": "unfinished", + "deactivateTester": "unfinished", + "report": "unfinished" + }, + "tasks": {}, + "log": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-west-1/run/1" } ], "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-west-1" + }, + "us-east-3": { + "runs": [ + { + "id": 1, + "status": "aborted", + "start": "(ignore)", + "wantedPlatform": "6.1", + "wantedApplication": { + "hash": "1.0.1-commit1", + "build": 1, + "source": { + "gitRepository": "repository1", + "gitBranch": "master", + "gitCommit": "commit1" + } + }, + "steps": { + "deployTester": "unfinished", + "deployReal": "unfinished", + "installTester": "unfinished", + "installReal": "unfinished", + "startTests": "unfinished", + "endTests": "unfinished", + "deactivateTester": "unfinished", + "report": "unfinished" + }, + "tasks": {}, + "log": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-east-3/run/1" + } + ], + "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-east-3" } }, "devJobs": { @@ -132,19 +299,21 @@ "runs": [ { "id": 1, - "status": "running", - "start": (ignore), + "status": "success", + "start": "(ignore)", + "end": "(ignore)", "wantedPlatform": "6.1", "wantedApplication": { "hash": "unknown" }, "steps": { - "deployReal": "unfinished", - "installReal": "unfinished", - "copyVespaLogs": "unfinished" + "deployReal": "succeeded", + "installReal": "succeeded", + "copyVespaLogs": "succeeded" }, "tasks": { - "deploy": "running" + "deploy": "succeeded", + "install": "succeeded" }, "log": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/dev-us-east-1/run/1" } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json index 143042e3600..a9d85dda0a9 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json @@ -18,11 +18,7 @@ "region": "us-central-1", "endpoints": [], "serviceUrls": [ - "http://old-endpoint.vespa.yahooapis.com:4080", - "http://qrs-endpoint.vespa.yahooapis.com:4080", - "http://feeding-endpoint.vespa.yahooapis.com:4080", - "http://global-endpoint.vespa.yahooapis.com:4080", - "http://alias-endpoint.vespa.yahooapis.com:4080" + "https://instance1--application1--tenant1.us-central-1.prod.vespa:43" ], "nodes": "http://localhost:8080/zone/v2/prod/us-central-1/nodes/v2/node/%3F&recursive=true&application=tenant1.application1.instance1", "yamasUrl": "http://monitoring-system.test/?environment=prod®ion=us-central-1&application=tenant1.application1.instance1", diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-details.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-details.json index 10d7f3260c1..993c53b52d8 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-details.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-details.json @@ -1,5 +1,404 @@ { - "active": true, - "status": "running", - "log": {} + "active": false, + "status": "success", + "log": { + "deployTester": [ + { + "at": "(ignore)", + "type": "info", + "message": "Deploying the tester container on platform 6.1 ..." + }, + { + "at": "(ignore)", + "type": "info", + "message": "No services requiring restart." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Deployment successful." + }, + { + "at": "(ignore)", + "type": "info", + "message": "foo" + } + ], + "deployReal": [ + { + "at": "(ignore)", + "type": "info", + "message": "Deploying platform version 6.1 and application version 1.0.1-commit1 ..." + }, + { + "at": "(ignore)", + "type": "info", + "message": "No services requiring restart." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Deployment successful." + }, + { + "at": "(ignore)", + "type": "info", + "message": "foo" + } + ], + "installTester": [ + { + "at": "(ignore)", + "type": "info", + "message": "Checking installation of tester container ..." + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-t-test.us-east-1: unorchestrated 6.1 " + }, + { + "at": "(ignore)", + "type": "info", + "message": "Wanted config generation is 2" + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-t-test.us-east-1: container on port 43 has config generation 1" + }, + { + "at": "(ignore)", + "type": "info", + "message": "Installation of tester not yet complete." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Checking installation of tester container ..." + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-t-test.us-east-1: unorchestrated 6.1 " + }, + { + "at": "(ignore)", + "type": "info", + "message": "Wanted config generation is 2" + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-t-test.us-east-1: container on port 43 has config generation 1" + }, + { + "at": "(ignore)", + "type": "info", + "message": "Installation of tester not yet complete." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Checking installation of tester container ..." + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-t-test.us-east-1: unorchestrated 6.1 " + }, + { + "at": "(ignore)", + "type": "info", + "message": "Wanted config generation is 2" + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-t-test.us-east-1: container on port 43 has config generation 1" + }, + { + "at": "(ignore)", + "type": "info", + "message": "Installation of tester not yet complete." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Checking installation of tester container ..." + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-t-test.us-east-1: unorchestrated 6.1 " + }, + { + "at": "(ignore)", + "type": "info", + "message": "Wanted config generation is 2" + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-t-test.us-east-1: container on port 43 has config generation 1" + }, + { + "at": "(ignore)", + "type": "info", + "message": "Installation of tester not yet complete." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Checking installation of tester container ..." + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-t-test.us-east-1: unorchestrated 6.1 " + }, + { + "at": "(ignore)", + "type": "info", + "message": "Wanted config generation is 2" + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-t-test.us-east-1: container on port 43 has config generation 1" + }, + { + "at": "(ignore)", + "type": "info", + "message": "Installation of tester not yet complete." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Checking installation of tester container ..." + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-t-test.us-east-1: unorchestrated 6.1 " + }, + { + "at": "(ignore)", + "type": "info", + "message": "Wanted config generation is 2" + }, + { + "at": "(ignore)", + "type": "info", + "message": "All services on wanted config generation." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Attempting to find deployment endpoints ..." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Endpoints not yet ready." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Installation of tester not yet complete." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Checking installation of tester container ..." + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-t-test.us-east-1: unorchestrated 6.1 " + }, + { + "at": "(ignore)", + "type": "info", + "message": "Wanted config generation is 2" + }, + { + "at": "(ignore)", + "type": "info", + "message": "All services on wanted config generation." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Attempting to find deployment endpoints ..." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Found endpoints:" + }, + { + "at": "(ignore)", + "type": "info", + "message": "- test.us-east-1" + }, + { + "at": "(ignore)", + "type": "info", + "message": " |-- https://instance1-t--application1--tenant1.us-east-1.test.vespa:43 (cluster 'default')" + }, + { + "at": "(ignore)", + "type": "info", + "message": "Tester container successfully installed!" + } + ], + "installReal": [ + { + "at": "(ignore)", + "type": "info", + "message": "Checking installation of 6.1 and 1.0.1-commit1 ..." + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-test.us-east-1: unorchestrated 6.1 " + }, + { + "at": "(ignore)", + "type": "info", + "message": "Wanted config generation is 2" + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-test.us-east-1: container on port 43 has config generation 1" + }, + { + "at": "(ignore)", + "type": "info", + "message": "Installation not yet complete." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Checking installation of 6.1 and 1.0.1-commit1 ..." + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-test.us-east-1: unorchestrated 6.1 " + }, + { + "at": "(ignore)", + "type": "info", + "message": "Wanted config generation is 2" + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-test.us-east-1: container on port 43 has config generation 1" + }, + { + "at": "(ignore)", + "type": "info", + "message": "Installation not yet complete." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Checking installation of 6.1 and 1.0.1-commit1 ..." + }, + { + "at": "(ignore)", + "type": "info", + "message": " host-tenant1:application1:instance1-test.us-east-1: unorchestrated 6.1 " + }, + { + "at": "(ignore)", + "type": "info", + "message": "Wanted config generation is 2" + }, + { + "at": "(ignore)", + "type": "info", + "message": "All services on wanted config generation." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Attempting to find deployment endpoints ..." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Found endpoints:" + }, + { + "at": "(ignore)", + "type": "info", + "message": "- test.us-east-1" + }, + { + "at": "(ignore)", + "type": "info", + "message": " |-- https://instance1--application1--tenant1.us-east-1.test.vespa:43 (cluster 'default')" + }, + { + "at": "(ignore)", + "type": "info", + "message": "Installation succeeded!" + } + ], + "startTests": [ + { + "at": "(ignore)", + "type": "info", + "message": "Attempting to find endpoints ..." + }, + { + "at": "(ignore)", + "type": "info", + "message": "Found endpoints:" + }, + { + "at": "(ignore)", + "type": "info", + "message": "- test.us-east-1" + }, + { + "at": "(ignore)", + "type": "info", + "message": " |-- https://instance1--application1--tenant1.us-east-1.test.vespa:43 (cluster 'default')" + }, + { + "at": "(ignore)", + "type": "info", + "message": "Starting tests ..." + } + ], + "endTests": [ + { + "at": "(ignore)", + "type": "info", + "message": "Tests completed successfully." + } + ], + "deactivateReal": [ + { + "at": "(ignore)", + "type": "info", + "message": "Deactivating deployment of tenant1.application1.instance1 in test.us-east-1 ..." + } + ], + "deactivateTester": [ + { + "at": "(ignore)", + "type": "info", + "message": "Deactivating tester of tenant1.application1.instance1 in test.us-east-1 ..." + } + ] + }, + "lastId": 75 } + diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-job.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-job.json index 228bdb17ecb..a572fa04781 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-job.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-job.json @@ -1,16 +1,50 @@ { "1": { "id": 1, + "status": "success", + "start": "(ignore)", + "end": "(ignore)", + "wantedPlatform": "6.1", + "wantedApplication": { + "hash": "1.0.1-commit1", + "build": 1, + "source": { + "gitRepository": "repository1", + "gitBranch": "master", + "gitCommit": "commit1" + } + }, + "steps": { + "deployTester": "succeeded", + "deployReal": "succeeded", + "installTester": "succeeded", + "installReal": "succeeded", + "startTests": "succeeded", + "endTests": "succeeded", + "copyVespaLogs": "succeeded", + "deactivateReal": "succeeded", + "deactivateTester": "succeeded", + "report": "succeeded" + }, + "tasks": { + "deploy": "succeeded", + "install": "succeeded", + "test": "succeeded" + }, + "log": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/system-test/run/1" + }, + "2": { + "id": 2, "status": "running", - "start": (ignore), + "start": "(ignore)", "wantedPlatform": "6.1", "wantedApplication": { - "hash": "1.0.45-d00d", - "build": 45, + "hash": "1.0.4-commit1", + "build": 4, "source": { - "gitRepository": "repo", + "gitRepository": "repository1", "gitBranch": "master", - "gitCommit": "d00d" + "gitCommit": "commit1" } }, "steps": { @@ -25,7 +59,8 @@ "deactivateTester": "unfinished", "report": "unfinished" }, - "tasks": { }, - "log": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/system-test/run/1" + "tasks": {}, + "log": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/system-test/run/2" } } + diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java index e0fc403da8e..e3216a0038b 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java @@ -3,7 +3,6 @@ package com.yahoo.vespa.hosted.controller.restapi.athenz; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock; -import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; import org.junit.Test; @@ -19,7 +18,7 @@ public class AthenzApiTest extends ControllerContainerTest { @Test public void testAthenzApi() { - ContainerTester tester = new ContainerControllerTester(container, responseFiles).containerTester(); + ContainerTester tester = new ContainerTester(container, responseFiles); ((AthenzClientFactoryMock) tester.container().components().getComponent(AthenzClientFactoryMock.class.getName())) .getSetup().addDomain(new AthenzDbMock.Domain(new AthenzDomain("domain1"))); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandlerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandlerTest.java new file mode 100644 index 00000000000..8aea26f21e3 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandlerTest.java @@ -0,0 +1,143 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.restapi.configserver; + +import com.yahoo.application.container.handler.Request; +import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.zone.ZoneApi; +import com.yahoo.vespa.hosted.controller.integration.ConfigServerProxyMock; +import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock; +import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock; +import com.yahoo.vespa.hosted.controller.proxy.ProxyRequest; +import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; +import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.net.URI; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +/** + * @author freva + */ +public class ConfigServerApiHandlerTest extends ControllerContainerTest { + + private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/responses/"; + private static final List<ZoneApi> zones = List.of( + ZoneApiMock.fromId("prod.us-north-1"), + ZoneApiMock.fromId("dev.aws-us-north-2"), + ZoneApiMock.fromId("test.us-north-3"), + ZoneApiMock.fromId("staging.us-north-4")); + + private ContainerTester tester; + private ConfigServerProxyMock proxy; + + @Before + public void before() { + tester = new ContainerTester(container, responseFiles); + tester.serviceRegistry().zoneRegistry() + .setDefaultRegionForEnvironment(Environment.dev, RegionName.from("us-north-2")) + .setZones(zones); + proxy = (ConfigServerProxyMock) container.components().getComponent(ConfigServerProxyMock.class.getName()); + } + + @Test + public void test_requests() { + // GET /configserver/v1 + tester.assertResponse(operatorRequest("http://localhost:8080/configserver/v1"), + new File("root.json")); + + // GET /configserver/v1/nodes/v2/node/?recursive=true + tester.assertResponse(operatorRequest("http://localhost:8080/configserver/v1/prod/us-north-1/nodes/v2/node/?recursive=true"), + "ok"); + assertLastRequest("https://cfg.prod.us-north-1.test.vip:4443/", "GET"); + + // POST /configserver/v1/dev/us-north-2/nodes/v2/command/restart?hostname=node1 + tester.assertResponse(operatorRequest("http://localhost:8080/configserver/v1/dev/aws-us-north-2/nodes/v2/command/restart?hostname=node1", + "", Request.Method.POST), + "ok"); + + // PUT /configserver/v1/prod/us-north-1/nodes/v2/state/dirty/node1 + tester.assertResponse(operatorRequest("http://localhost:8080/configserver/v1/prod/us-north-1/nodes/v2/state/dirty/node1", + "", Request.Method.PUT), "ok"); + assertLastRequest("https://cfg.prod.us-north-1.test.vip:4443/", "PUT"); + + // DELETE /configserver/v1/prod/us-north-1/nodes/v2/node/node1 + tester.assertResponse(operatorRequest("http://localhost:8080/api/configserver/v1/prod/controller/nodes/v2/node/node1", + "", Request.Method.DELETE), "ok"); + assertLastRequest("https://localhost:4443/", "DELETE"); + + // PATCH /configserver/v1/prod/us-north-1/nodes/v2/node/node1 + tester.assertResponse(operatorRequest("http://localhost:8080/configserver/v1/dev/aws-us-north-2/nodes/v2/node/node1", + "{\"currentRestartGeneration\": 1}", + Request.Method.PATCH), "ok"); + assertLastRequest("https://cfg.dev.aws-us-north-2.test.vip:4443/", "PATCH"); + assertEquals("{\"currentRestartGeneration\": 1}", proxy.lastRequestBody().get()); + + assertFalse("Actions are logged to audit log", tester.controller().auditLogger().readLog().entries().isEmpty()); + } + + @Test + public void test_allowed_apis() { + // GET /configserver/v1/prod/us-north-1 + tester.assertResponse(() -> operatorRequest("http://localhost:8080/configserver/v1/prod/us-north-1"), + "{\"error-code\":\"FORBIDDEN\",\"message\":\"Cannot access '/' through /configserver/v1, following APIs are permitted: /flags/v1/, /nodes/v2/, /orchestrator/v1/\"}", + 403); + + tester.assertResponse(() -> operatorRequest("http://localhost:8080/configserver/v1/prod/us-north-1/application/v2/tenant/vespa"), + "{\"error-code\":\"FORBIDDEN\",\"message\":\"Cannot access '/application/v2/tenant/vespa' through /configserver/v1, following APIs are permitted: /flags/v1/, /nodes/v2/, /orchestrator/v1/\"}", + 403); + } + + @Test + public void test_invalid_requests() { + // POST /configserver/v1/prod/us-north-34/nodes/v2 + tester.assertResponse(() -> operatorRequest("http://localhost:8080/configserver/v1/prod/us-north-42/nodes/v2", + "", Request.Method.POST), + "{\"error-code\":\"BAD_REQUEST\",\"message\":\"No such zone: prod.us-north-42\"}", 400); + assertFalse(proxy.lastReceived().isPresent()); + } + + @Test + public void non_operators_are_forbidden() { + // Read request + tester.assertResponse(() -> authenticatedRequest("http://localhost:8080/configserver/v1/prod/us-north-1/nodes/v2/node"), + "{\n" + + " \"code\" : 403,\n" + + " \"message\" : \"Access denied\"\n" + + "}", 403); + + // Write request + tester.assertResponse(() -> authenticatedRequest("http://localhost:8080/configserver/v1/prod/us-north-1/nodes/v2/node", "", Request.Method.POST), + "{\n" + + " \"code\" : 403,\n" + + " \"message\" : \"Access denied\"\n" + + "}", 403); + } + + @Test + public void unauthenticated_request_are_unauthorized() { + { + // Read request + Request request = new Request("http://localhost:8080/configserver/v1/prod/us-north-1/nodes/v2/node", "", Request.Method.GET); + tester.assertResponse(() -> request, "{\n \"message\" : \"Not authenticated\"\n}", 401); + } + + { + // Write request + Request request = new Request("http://localhost:8080/configserver/v1/prod/us-north-1/nodes/v2/node", "", Request.Method.POST); + tester.assertResponse(() -> request, "{\n \"message\" : \"Not authenticated\"\n}", 401); + } + } + + + private void assertLastRequest(String target, String method) { + ProxyRequest last = proxy.lastReceived().orElseThrow(); + assertEquals(List.of(URI.create(target)), last.getTargets()); + assertEquals(com.yahoo.jdisc.http.HttpRequest.Method.valueOf(method), last.getMethod()); + } +}
\ No newline at end of file diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/responses/root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/responses/root.json new file mode 100644 index 00000000000..5ccf75d2448 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/responses/root.json @@ -0,0 +1,29 @@ +{ + "zones": [ + { + "environment": "prod", + "region": "controller", + "uri": "http://localhost:8080/configserver/v1/prod/controller" + }, + { + "environment": "prod", + "region": "us-north-1", + "uri": "http://localhost:8080/configserver/v1/prod/us-north-1" + }, + { + "environment": "dev", + "region": "aws-us-north-2", + "uri": "http://localhost:8080/configserver/v1/dev/aws-us-north-2" + }, + { + "environment": "test", + "region": "us-north-3", + "uri": "http://localhost:8080/configserver/v1/test/us-north-3" + }, + { + "environment": "staging", + "region": "us-north-4", + "uri": "http://localhost:8080/configserver/v1/staging/us-north-4" + } + ] +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java index 74d637499bd..b21c588235e 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java @@ -4,10 +4,8 @@ package com.yahoo.vespa.hosted.controller.restapi.controller; import com.yahoo.application.container.handler.Request; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.test.ManualClock; -import com.yahoo.vespa.athenz.api.AthenzIdentity; -import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.hosted.controller.auditlog.AuditLogger; -import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; +import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; import org.junit.Before; import org.junit.Test; @@ -27,48 +25,46 @@ import static org.junit.Assert.assertFalse; public class ControllerApiTest extends ControllerContainerTest { private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/"; - private static final AthenzIdentity HOSTED_VESPA_OPERATOR = AthenzUser.fromUserId("johnoperator"); - private ContainerControllerTester tester; + private ContainerTester tester; @Before public void before() { - addUserToHostedOperatorRole(HOSTED_VESPA_OPERATOR); - tester = new ContainerControllerTester(container, responseFiles); + tester = new ContainerTester(container, responseFiles); } @Test public void testControllerApi() { - tester.assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/", new byte[0], Request.Method.GET), new File("root.json")); + tester.assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/", "", Request.Method.GET), new File("root.json")); // POST deactivates a maintenance job - tester.assertResponse(hostedOperatorRequest("http://localhost:8080/controller/v1/maintenance/inactive/DeploymentExpirer", + tester.assertResponse(operatorRequest("http://localhost:8080/controller/v1/maintenance/inactive/DeploymentExpirer", "", Request.Method.POST), "{\"message\":\"Deactivated job 'DeploymentExpirer'\"}", 200); // GET a list of all maintenance jobs - tester.assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/maintenance/", new byte[0], Request.Method.GET), + tester.assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/maintenance/", "", Request.Method.GET), new File("maintenance.json")); // DELETE activates maintenance job - tester.assertResponse(hostedOperatorRequest("http://localhost:8080/controller/v1/maintenance/inactive/DeploymentExpirer", + tester.assertResponse(operatorRequest("http://localhost:8080/controller/v1/maintenance/inactive/DeploymentExpirer", "", Request.Method.DELETE), "{\"message\":\"Re-activated job 'DeploymentExpirer'\"}", 200); // DELETE fails to activate unknown maintenance job - tester.assertResponse(hostedOperatorRequest("http://localhost:8080/controller/v1/maintenance/inactive/foo", + tester.assertResponse(operatorRequest("http://localhost:8080/controller/v1/maintenance/inactive/foo", "", Request.Method.DELETE), "{\"error-code\":\"NOT_FOUND\",\"message\":\"No job named 'foo'\"}", 404); // DELETE clears inactive flag for maintenance job that has been removed from the code base tester.controller().curator().writeInactiveJobs(Set.of("bar")); - tester.assertResponse(hostedOperatorRequest("http://localhost:8080/controller/v1/maintenance/inactive/bar", + tester.assertResponse(operatorRequest("http://localhost:8080/controller/v1/maintenance/inactive/bar", "", Request.Method.DELETE), "{\"message\":\"Re-activated job 'bar'\"}", 200); - tester.assertResponse(hostedOperatorRequest("http://localhost:8080/controller/v1/maintenance/inactive/bar", + tester.assertResponse(operatorRequest("http://localhost:8080/controller/v1/maintenance/inactive/bar", "", Request.Method.DELETE), "{\"error-code\":\"NOT_FOUND\",\"message\":\"No job named 'bar'\"}", 404); @@ -79,55 +75,55 @@ public class ControllerApiTest extends ControllerContainerTest { @Test public void testUpgraderApi() { // Get current configuration - tester.assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/jobs/upgrader", new byte[0], Request.Method.GET), - "{\"upgradesPerMinute\":100.0,\"confidenceOverrides\":[]}", + tester.assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/jobs/upgrader", "", Request.Method.GET), + "{\"upgradesPerMinute\":0.125,\"confidenceOverrides\":[]}", 200); // Set invalid configuration tester.assertResponse( - hostedOperatorRequest("http://localhost:8080/controller/v1/jobs/upgrader", "{\"upgradesPerMinute\":-1}", Request.Method.PATCH), + operatorRequest("http://localhost:8080/controller/v1/jobs/upgrader", "{\"upgradesPerMinute\":-1}", Request.Method.PATCH), "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Upgrades per minute must be >= 0, got -1.0\"}", 400); // Ignores unrecognized field tester.assertResponse( - hostedOperatorRequest("http://localhost:8080/controller/v1/jobs/upgrader","{\"foo\":\"bar\"}", Request.Method.PATCH), + operatorRequest("http://localhost:8080/controller/v1/jobs/upgrader", "{\"foo\":\"bar\"}", Request.Method.PATCH), "{\"error-code\":\"BAD_REQUEST\",\"message\":\"No such modifiable field(s)\"}", 400); // Set upgrades per minute tester.assertResponse( - hostedOperatorRequest("http://localhost:8080/controller/v1/jobs/upgrader", "{\"upgradesPerMinute\":42.0}", Request.Method.PATCH), + operatorRequest("http://localhost:8080/controller/v1/jobs/upgrader", "{\"upgradesPerMinute\":42.0}", Request.Method.PATCH), "{\"upgradesPerMinute\":42.0,\"confidenceOverrides\":[]}", 200); // Set target major version tester.assertResponse( - hostedOperatorRequest("http://localhost:8080/controller/v1/jobs/upgrader", "{\"targetMajorVersion\":6}", Request.Method.PATCH), + operatorRequest("http://localhost:8080/controller/v1/jobs/upgrader", "{\"targetMajorVersion\":6}", Request.Method.PATCH), "{\"upgradesPerMinute\":42.0,\"targetMajorVersion\":6,\"confidenceOverrides\":[]}", 200); // Clear target major version tester.assertResponse( - hostedOperatorRequest("http://localhost:8080/controller/v1/jobs/upgrader", "{\"targetMajorVersion\":null}", Request.Method.PATCH), + operatorRequest("http://localhost:8080/controller/v1/jobs/upgrader", "{\"targetMajorVersion\":null}", Request.Method.PATCH), "{\"upgradesPerMinute\":42.0,\"confidenceOverrides\":[]}", 200); // Override confidence tester.assertResponse( - hostedOperatorRequest("http://localhost:8080/controller/v1/jobs/upgrader/confidence/6.42", "broken", Request.Method.POST), + operatorRequest("http://localhost:8080/controller/v1/jobs/upgrader/confidence/6.42", "broken", Request.Method.POST), "{\"upgradesPerMinute\":42.0,\"confidenceOverrides\":[{\"6.42\":\"broken\"}]}", 200); // Override confidence for another version tester.assertResponse( - hostedOperatorRequest("http://localhost:8080/controller/v1/jobs/upgrader/confidence/6.43", "broken", Request.Method.POST), + operatorRequest("http://localhost:8080/controller/v1/jobs/upgrader/confidence/6.43", "broken", Request.Method.POST), "{\"upgradesPerMinute\":42.0,\"confidenceOverrides\":[{\"6.42\":\"broken\"},{\"6.43\":\"broken\"}]}", 200); // Remove first override tester.assertResponse( - hostedOperatorRequest("http://localhost:8080/controller/v1/jobs/upgrader/confidence/6.42", "", Request.Method.DELETE), + operatorRequest("http://localhost:8080/controller/v1/jobs/upgrader/confidence/6.42", "", Request.Method.DELETE), "{\"upgradesPerMinute\":42.0,\"confidenceOverrides\":[{\"6.43\":\"broken\"}]}", 200); @@ -160,8 +156,4 @@ public class ControllerApiTest extends ControllerContainerTest { tester.assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/auditlog/"), new File("auditlog.json")); } - private static Request hostedOperatorRequest(String uri, String body, Request.Method method) { - return addIdentityToRequest(new Request(uri, body, method), HOSTED_VESPA_OPERATOR); - } - } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostApiTest.java index b085a87672e..f992a54a114 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/cost/CostApiTest.java @@ -8,8 +8,7 @@ import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock; -import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock; -import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; +import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; import org.junit.Before; import org.junit.Test; @@ -27,13 +26,13 @@ public class CostApiTest extends ControllerContainerTest { private static final ZoneApi zone2 = ZoneApiMock.newBuilder().withId("prod.us-west-1").with(cloud1).build(); private static final ZoneApi zone3 = ZoneApiMock.newBuilder().withId("prod.eu-west-1").with(cloud2).build(); - private ContainerControllerTester tester; + private ContainerTester tester; @Before public void before() { - tester = new ContainerControllerTester(container, responses); - zoneRegistryMock().setSystemName(SystemName.cd) - .setZones(zone1, zone2, zone3); + tester = new ContainerTester(container, responses); + tester.serviceRegistry().zoneRegistry().setSystemName(SystemName.cd) + .setZones(zone1, zone2, zone3); } @Test @@ -42,13 +41,9 @@ public class CostApiTest extends ControllerContainerTest { "Date,Property,Reserved Cpu Cores,Reserved Memory GB,Reserved Disk Space GB,Usage Fraction\n", 200); } - private ZoneRegistryMock zoneRegistryMock() { - return (ZoneRegistryMock) tester.containerTester().container().components() - .getComponent(ZoneRegistryMock.class.getName()); - } - private void assertResponse(Request request, String body, int statusCode) { addIdentityToRequest(request, operator); tester.assertResponse(request, body, statusCode); } + } 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 35ec5b0e37e..37cf5511c0b 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 @@ -1,12 +1,8 @@ package com.yahoo.vespa.hosted.controller.restapi.deployment; -import com.yahoo.config.provision.AthenzService; -import com.yahoo.config.provision.Environment; -import com.yahoo.vespa.hosted.controller.Application; -import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision; -import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; -import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder; -import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; +import com.yahoo.vespa.hosted.controller.ControllerTester; +import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester; +import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; import org.junit.Test; @@ -19,19 +15,9 @@ public class BadgeApiTest extends ControllerContainerTest { @Test public void testBadgeApi() { - ContainerControllerTester tester = new ContainerControllerTester(container, responseFiles); - Application application = tester.createApplication("domain", "tenant", "application", "default"); - ApplicationPackage packageWithService = new ApplicationPackageBuilder() - .environment(Environment.prod) - .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain"), AthenzService.from("service")) - .region("us-west-1") - .build(); - tester.controller().jobController().submit(application.id(), - new SourceRevision("repository", "branch", "commit"), - "foo@bar", - 123, - packageWithService, - new byte[0]); + ContainerTester tester = new ContainerTester(container, responseFiles); + var application = new DeploymentTester(new ControllerTester(tester)).newDeploymentContext("tenant", "application", "default"); + application.submit(); tester.assertResponse(authenticatedRequest("http://localhost:8080/badge/v1/tenant/application/default"), "", 302); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java index bb1e6b6256a..3c23053eac0 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java @@ -5,11 +5,12 @@ import com.yahoo.component.Version; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.zone.ZoneId; -import com.yahoo.vespa.hosted.controller.Application; -import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.ControllerTester; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder; -import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; +import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester; +import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; import com.yahoo.vespa.hosted.controller.versions.NodeVersion; import com.yahoo.vespa.hosted.controller.versions.NodeVersions; @@ -31,42 +32,42 @@ public class DeploymentApiTest extends ControllerContainerTest { @Test public void testDeploymentApi() { - ContainerControllerTester tester = new ContainerControllerTester(container, responseFiles); + ContainerTester tester = new ContainerTester(container, responseFiles); + DeploymentTester deploymentTester = new DeploymentTester(new ControllerTester(tester)); Version version = Version.fromString("5.0"); - tester.containerTester().upgradeSystem(version); + deploymentTester.controllerTester().upgradeSystem(version); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .environment(Environment.prod) .region("us-west-1") .build(); // 3 applications deploy on current system version - Application failingInstance = tester.createApplication("domain1", "tenant1", "application1", "default"); - Application productionInstance = tester.createApplication("domain2", "tenant2", "application2", "default"); - Application instanceWithoutDeployment = tester.createApplication("domain3", "tenant3", "application3", "default"); - tester.deployCompletely(failingInstance, applicationPackage, 1L, false); - tester.deployCompletely(productionInstance, applicationPackage, 2L, false); + var failingApp = deploymentTester.newDeploymentContext("tenant1", "application1", "default"); + var productionApp = deploymentTester.newDeploymentContext("tenant2", "application2", "default"); + var appWithoutDeployments = deploymentTester.newDeploymentContext("tenant3", "application3", "default"); + failingApp.submit(applicationPackage).deploy(); + productionApp.submit(applicationPackage).deploy(); // Deploy once so that job information is stored, then remove the deployment - tester.deployCompletely(instanceWithoutDeployment, applicationPackage, 3L, false); - tester.controller().applications().deactivate(instanceWithoutDeployment.id().defaultInstance(), ZoneId.from("prod", "us-west-1")); + appWithoutDeployments.submit(applicationPackage).deploy(); + deploymentTester.applications().deactivate(appWithoutDeployments.instanceId(), ZoneId.from("prod", "us-west-1")); // New version released version = Version.fromString("5.1"); - tester.containerTester().upgradeSystem(version); + deploymentTester.controllerTester().upgradeSystem(version); // Applications upgrade, 1/2 succeed - tester.upgrader().maintain(); - tester.controller().applications().deploymentTrigger().triggerReadyJobs(); - tester.controller().applications().deploymentTrigger().triggerReadyJobs(); - tester.deployCompletely(failingInstance, applicationPackage, 1L, true); - tester.deployCompletely(productionInstance, applicationPackage, 2L, false); + deploymentTester.upgrader().maintain(); + deploymentTester.triggerJobs(); + productionApp.deployPlatform(version); + failingApp.runJob(JobType.systemTest).failDeployment(JobType.stagingTest); + deploymentTester.triggerJobs(); - tester.controller().updateVersionStatus(censorConfigServers(VersionStatus.compute(tester.controller()), - tester.controller())); + tester.controller().updateVersionStatus(censorConfigServers(VersionStatus.compute(tester.controller()))); tester.assertResponse(authenticatedRequest("http://localhost:8080/deployment/v1/"), new File("root.json")); } - private VersionStatus censorConfigServers(VersionStatus versionStatus, Controller controller) { + private VersionStatus censorConfigServers(VersionStatus versionStatus) { List<VespaVersion> censored = new ArrayList<>(); for (VespaVersion version : versionStatus.versions()) { if (version.nodeVersions().size() > 0) { @@ -78,7 +79,7 @@ public class DeploymentApiTest extends ControllerContainerTest { version.isReleased(), NodeVersions.EMPTY.with(List.of(new NodeVersion(HostName.from("config1.test"), ZoneId.defaultId(), version.versionNumber(), version.versionNumber(), Instant.EPOCH), new NodeVersion(HostName.from("config2.test"), ZoneId.defaultId(), version.versionNumber(), version.versionNumber(), Instant.EPOCH))), - VespaVersion.confidenceFrom(version.statistics(), controller) + version.confidence() ); } censored.add(version); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json index 845fa4631b6..a3a763e9634 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json @@ -1,14 +1,14 @@ { - "versions":[ + "versions": [ { "version": "5", "confidence": "high", - "commit": "(ignore)", - "date": "(ignore)", + "commit": "badc0ffee", + "date": 0, "controllerVersion": false, "systemVersion": false, - "configServers": [ ], - "failingApplications": [ ], + "configServers": [], + "failingApplications": [], "productionApplications": [ { "tenant": "tenant1", @@ -20,16 +20,16 @@ "productionSuccesses": 1 } ], - "deployingApplications": [ ] + "deployingApplications": [] }, { - "version":"5.1", - "confidence":"normal", - "commit":"(ignore)", - "date": "(ignore)", - "controllerVersion":false, - "systemVersion":true, - "configServers":[ + "version": "5.1", + "confidence": "normal", + "commit": "badc0ffee", + "date": 0, + "controllerVersion": true, + "systemVersion": true, + "configServers": [ { "hostname":"config1.test" }, @@ -37,7 +37,7 @@ "hostname":"config2.test" } ], - "failingApplications":[ + "failingApplications": [ { "tenant": "tenant1", "application": "application1", @@ -47,7 +47,7 @@ "failing": "staging-test" } ], - "productionApplications":[ + "productionApplications": [ { "tenant": "tenant2", "application": "application2", @@ -68,18 +68,7 @@ "running": "staging-test" } ] - }, - { - "version": "(ignore)", - "confidence": "normal", - "commit": "(ignore)", - "date": "(ignore)", - "controllerVersion": true, - "systemVersion": false, - "configServers": [ ], - "failingApplications": [ ], - "productionApplications": [ ], - "deployingApplications": [ ] } ] } + diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsApiTest.java index b4ef98cc7f6..8fdfbb14c1b 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsApiTest.java @@ -5,7 +5,7 @@ import com.yahoo.application.container.handler.Request; import com.yahoo.vespa.athenz.api.AthenzIdentity; import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.hosted.controller.auditlog.AuditLog; -import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; +import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; import org.junit.Before; import org.junit.Test; @@ -20,12 +20,12 @@ public class AuditedFlagsApiTest extends ControllerContainerTest { private static final String responses = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/flags/responses/"; private static final AthenzIdentity operator = AthenzUser.fromUserId("operatorUser"); - private ContainerControllerTester tester; + private ContainerTester tester; @Before public void before() { addUserToHostedOperatorRole(operator); - tester = new ContainerControllerTester(container, responses); + tester = new ContainerTester(container, responses); } @Test 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 6bc221184c1..89f11eae5ae 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 @@ -17,7 +17,7 @@ import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock; import com.yahoo.vespa.hosted.controller.maintenance.JobControl; import com.yahoo.vespa.hosted.controller.maintenance.Maintainer; import com.yahoo.vespa.hosted.controller.maintenance.OsUpgrader; -import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; +import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus; import org.intellij.lang.annotations.Language; @@ -43,12 +43,12 @@ public class OsApiTest extends ControllerContainerTest { private static final ZoneApi zone2 = ZoneApiMock.newBuilder().withId("prod.us-west-1").with(cloud1).build(); private static final ZoneApi zone3 = ZoneApiMock.newBuilder().withId("prod.eu-west-1").with(cloud2).build(); - private ContainerControllerTester tester; + private ContainerTester tester; private List<OsUpgrader> osUpgraders; @Before public void before() { - tester = new ContainerControllerTester(container, responses); + tester = new ContainerTester(container, responses); addUserToHostedOperatorRole(operator); zoneRegistryMock().setSystemName(SystemName.cd) .setZones(zone1, zone2, zone3) @@ -70,8 +70,6 @@ public class OsApiTest extends ControllerContainerTest { // All nodes are initially on empty version upgradeAndUpdateStatus(); - assertFile(new Request("http://localhost:8080/os/v1/"), "versions-initial.json"); - // Upgrade OS to a different version in each cloud assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"7.5.2\", \"cloud\": \"cloud1\"}", Request.Method.PATCH), "{\"message\":\"Set target OS version for cloud 'cloud1' to 7.5.2\"}", 200); @@ -160,12 +158,11 @@ public class OsApiTest extends ControllerContainerTest { } private ZoneRegistryMock zoneRegistryMock() { - return (ZoneRegistryMock) tester.containerTester().container().components() - .getComponent(ZoneRegistryMock.class.getName()); + return tester.serviceRegistry().zoneRegistry(); } private NodeRepositoryMock nodeRepository() { - return tester.containerTester().serviceRegistry().configServerMock().nodeRepository(); + return tester.serviceRegistry().configServerMock().nodeRepository(); } private void assertResponse(Request request, @Language("JSON") String body, int statusCode) { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java index 89d90fe6221..eaeba420bc9 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java @@ -7,7 +7,7 @@ import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.vespa.hosted.controller.api.role.Role; import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock; import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock; -import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; +import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerCloudTest; import org.junit.Before; import org.junit.Test; @@ -30,31 +30,30 @@ public class ZoneApiTest extends ControllerContainerCloudTest { private static final Set<Role> everyone = Set.of(Role.everyone()); - private ContainerControllerTester tester; + private ContainerTester tester; @Before public void before() { - ZoneRegistryMock zoneRegistry = (ZoneRegistryMock) container.components() - .getComponent(ZoneRegistryMock.class.getName()); - zoneRegistry.setDefaultRegionForEnvironment(Environment.dev, RegionName.from("us-north-2")) - .setZones(zones); - this.tester = new ContainerControllerTester(container, responseFiles); + tester = new ContainerTester(container, responseFiles); + tester.serviceRegistry().zoneRegistry() + .setDefaultRegionForEnvironment(Environment.dev, RegionName.from("us-north-2")) + .setZones(zones); } @Test public void test_requests() { // GET /zone/v1 - tester.containerTester().assertResponse(request("/zone/v1") + tester.assertResponse(request("/zone/v1") .roles(everyone), new File("root.json")); // GET /zone/v1/environment/prod - tester.containerTester().assertResponse(request("/zone/v1/environment/prod") + tester.assertResponse(request("/zone/v1/environment/prod") .roles(everyone), new File("prod.json")); // GET /zone/v1/environment/dev/default - tester.containerTester().assertResponse(request("/api/zone/v1/environment/dev/default") + tester.assertResponse(request("/api/zone/v1/environment/dev/default") .roles(everyone), new File("default-for-region.json")); } @@ -62,7 +61,7 @@ public class ZoneApiTest extends ControllerContainerCloudTest { @Test public void test_invalid_requests() { // GET /zone/v1/environment/prod/default: No default region - tester.containerTester().assertResponse(request("/zone/v1/environment/prod/default") + tester.assertResponse(request("/zone/v1/environment/prod/default") .roles(everyone), new File("no-default-region.json"), 400); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java index 33ea538e9b6..f00363989e6 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java @@ -1,20 +1,16 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.restapi.zone.v2; -import com.yahoo.application.container.handler.Request; import com.yahoo.application.container.handler.Request.Method; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.config.provision.zone.ZoneId; -import com.yahoo.text.Utf8; -import com.yahoo.vespa.athenz.api.AthenzIdentity; -import com.yahoo.vespa.athenz.api.AthenzUser; import com.yahoo.vespa.hosted.controller.integration.ConfigServerProxyMock; import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock; import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock; import com.yahoo.vespa.hosted.controller.proxy.ProxyRequest; -import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; +import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; import org.junit.Before; import org.junit.Test; @@ -24,70 +20,69 @@ import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; /** * @author mpolden */ public class ZoneApiTest extends ControllerContainerTest { - private static final AthenzIdentity HOSTED_VESPA_OPERATOR = AthenzUser.fromUserId("johnoperator"); private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/"; private static final List<ZoneApi> zones = List.of( ZoneApiMock.fromId("prod.us-north-1"), - ZoneApiMock.fromId("dev.us-north-2"), + ZoneApiMock.fromId("dev.aws-us-north-2"), ZoneApiMock.fromId("test.us-north-3"), ZoneApiMock.fromId("staging.us-north-4")); - private ContainerControllerTester tester; + private ContainerTester tester; private ConfigServerProxyMock proxy; @Before public void before() { - ZoneRegistryMock zoneRegistry = (ZoneRegistryMock) container.components() - .getComponent(ZoneRegistryMock.class.getName()); - zoneRegistry.setDefaultRegionForEnvironment(Environment.dev, RegionName.from("us-north-2")) - .setZones(zones); - this.tester = new ContainerControllerTester(container, responseFiles); - this.proxy = (ConfigServerProxyMock) container.components().getComponent(ConfigServerProxyMock.class.getName()); - addUserToHostedOperatorRole(HOSTED_VESPA_OPERATOR); + tester = new ContainerTester(container, responseFiles); + tester.serviceRegistry().zoneRegistry() + .setDefaultRegionForEnvironment(Environment.dev, RegionName.from("us-north-2")) + .setZones(zones); + proxy = (ConfigServerProxyMock) container.components().getComponent(ConfigServerProxyMock.class.getName()); } @Test public void test_requests() { // GET /zone/v2 - tester.containerTester().assertResponse(authenticatedRequest("http://localhost:8080/zone/v2"), + tester.assertResponse(authenticatedRequest("http://localhost:8080/zone/v2"), new File("root.json")); // GET /zone/v2/prod/us-north-1 - tester.containerTester().assertResponse(authenticatedRequest("http://localhost:8080/zone/v2/prod/us-north-1"), + tester.assertResponse(authenticatedRequest("http://localhost:8080/zone/v2/prod/us-north-1"), "ok"); - assertLastRequest(ZoneId.from("prod", "us-north-1"), "GET"); + + assertLastRequest(ZoneId.from("prod", "us-north-1"), 2, "GET"); // GET /zone/v2/nodes/v2/node/?recursive=true - tester.containerTester().assertResponse(authenticatedRequest("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/?recursive=true"), + tester.assertResponse(authenticatedRequest("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/?recursive=true"), "ok"); - assertLastRequest(ZoneId.from("prod", "us-north-1"), "GET"); + assertLastRequest(ZoneId.from("prod", "us-north-1"), 2, "GET"); // POST /zone/v2/dev/us-north-2/nodes/v2/command/restart?hostname=node1 - tester.containerTester().assertResponse(hostedOperatorRequest("http://localhost:8080/zone/v2/dev/us-north-2/nodes/v2/command/restart?hostname=node1", - new byte[0], Method.POST), + tester.assertResponse(operatorRequest("http://localhost:8080/zone/v2/dev/aws-us-north-2/nodes/v2/command/restart?hostname=node1", + "", Method.POST), "ok"); // PUT /zone/v2/prod/us-north-1/nodes/v2/state/dirty/node1 - tester.containerTester().assertResponse(hostedOperatorRequest("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/state/dirty/node1", - new byte[0], Method.PUT), "ok"); - assertLastRequest(ZoneId.from("prod", "us-north-1"), "PUT"); + tester.assertResponse(operatorRequest("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/state/dirty/node1", + "", Method.PUT), "ok"); + assertLastRequest(ZoneId.from("prod", "us-north-1"), 2, "PUT"); // DELETE /zone/v2/prod/us-north-1/nodes/v2/node/node1 - tester.containerTester().assertResponse(hostedOperatorRequest("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/node1", - new byte[0], Method.DELETE), "ok"); - assertLastRequest(ZoneId.from("prod", "us-north-1"), "DELETE"); + tester.assertResponse(operatorRequest("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/node1", + "", Method.DELETE), "ok"); + assertLastRequest(ZoneId.from("prod", "us-north-1"), 2, "DELETE"); // PATCH /zone/v2/prod/us-north-1/nodes/v2/node/node1 - tester.containerTester().assertResponse(hostedOperatorRequest("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/node1", - Utf8.toBytes("{\"currentRestartGeneration\": 1}"), + tester.assertResponse(operatorRequest("http://localhost:8080/zone/v2/dev/aws-us-north-2/nodes/v2/node/node1", + "{\"currentRestartGeneration\": 1}", Method.PATCH), "ok"); - assertLastRequest(ZoneId.from("prod", "us-north-1"), "PATCH"); + assertLastRequest(ZoneId.from("dev", "aws-us-north-2"), 1, "PATCH"); assertEquals("{\"currentRestartGeneration\": 1}", proxy.lastRequestBody().get()); assertFalse("Actions are logged to audit log", tester.controller().auditLogger().readLog().entries().isEmpty()); @@ -96,20 +91,17 @@ public class ZoneApiTest extends ControllerContainerTest { @Test public void test_invalid_requests() { // POST /zone/v2/prod/us-north-34/nodes/v2 - tester.containerTester().assertResponse(hostedOperatorRequest("http://localhost:8080/zone/v2/prod/us-north-42/nodes/v2", - new byte[0], Method.POST), + tester.assertResponse(operatorRequest("http://localhost:8080/zone/v2/prod/us-north-42/nodes/v2", + "", Method.POST), new File("unknown-zone.json"), 400); assertFalse(proxy.lastReceived().isPresent()); } - private void assertLastRequest(ZoneId zoneId, String method) { + private void assertLastRequest(ZoneId zoneId, int targets, String method) { ProxyRequest last = proxy.lastReceived().orElseThrow(); - assertEquals(zoneId, last.getZoneId()); + assertEquals(targets, last.getTargets().size()); + assertTrue(last.getTargets().get(0).toString().contains(zoneId.value())); assertEquals(com.yahoo.jdisc.http.HttpRequest.Method.valueOf(method), last.getMethod()); } - private static Request hostedOperatorRequest(String uri, byte[] body, Request.Method method) { - return addIdentityToRequest(new Request(uri, body, method), HOSTED_VESPA_OPERATOR); - } - } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/root.json index ab168854267..bd1bc40ba81 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/root.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/root.json @@ -1,7 +1,7 @@ { "uris": [ "http://localhost:8080/zone/v2/prod/us-north-1", - "http://localhost:8080/zone/v2/dev/us-north-2", + "http://localhost:8080/zone/v2/dev/aws-us-north-2", "http://localhost:8080/zone/v2/test/us-north-3", "http://localhost:8080/zone/v2/staging/us-north-4" ], @@ -12,7 +12,7 @@ }, { "environment": "dev", - "region": "us-north-2" + "region": "aws-us-north-2" }, { "environment": "test", diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepositoryTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepositoryTest.java index c76046b3f67..674f084a8b7 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepositoryTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/rotation/RotationRepositoryTest.java @@ -146,6 +146,44 @@ public class RotationRepositoryTest { application2.instance().endpointsIn(SystemName.cd).main().get().url().toString()); } + @Test + public void multiple_instances_with_similar_global_service_id() { + ApplicationPackage applicationPackage = new ApplicationPackageBuilder() + .instances("instance1,instance2") + .region("us-central-1") + .parallel("us-west-1", "us-east-3") + .globalServiceId("global") + .build(); + var instance1 = tester.newDeploymentContext("tenant1", "application1", "instance1").submit(applicationPackage); + var instance2 = tester.newDeploymentContext("tenant1", "application1", "instance2"); + assertEquals(List.of(new RotationId("foo-1")), rotationIds(instance1.instance().rotations())); + assertEquals(List.of(new RotationId("foo-2")), rotationIds(instance2.instance().rotations())); + assertEquals(URI.create("https://instance1--application1--tenant1.global.vespa.oath.cloud:4443/"), + instance1.instance().endpointsIn(SystemName.main).main().get().url()); + assertEquals(URI.create("https://instance2--application1--tenant1.global.vespa.oath.cloud:4443/"), + instance2.instance().endpointsIn(SystemName.main).main().get().url()); + } + + @Test + public void multiple_instances_with_similar_endpoints() { + ApplicationPackage applicationPackage = new ApplicationPackageBuilder() + .instances("instance1,instance2") + .region("us-central-1") + .parallel("us-west-1", "us-east-3") + .endpoint("default", "foo", "us-central-1", "us-west-1") + .build(); + var instance1 = tester.newDeploymentContext("tenant1", "application1", "instance1").submit(applicationPackage); + var instance2 = tester.newDeploymentContext("tenant1", "application1", "instance2"); + + assertEquals(List.of(new RotationId("foo-1")), rotationIds(instance1.instance().rotations())); + assertEquals(List.of(new RotationId("foo-2")), rotationIds(instance2.instance().rotations())); + + assertEquals(URI.create("https://instance1--application1--tenant1.global.vespa.oath.cloud:4443/"), + instance1.instance().endpointsIn(SystemName.main).main().get().url()); + assertEquals(URI.create("https://instance2--application1--tenant1.global.vespa.oath.cloud:4443/"), + instance2.instance().endpointsIn(SystemName.main).main().get().url()); + } + private void assertSingleRotation(Rotation expected, List<AssignedRotation> assignedRotations, RotationRepository repository) { assertEquals(1, assignedRotations.size()); var rotationId = assignedRotations.get(0).rotationId(); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java index 5c68fd2e370..06260da833f 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java @@ -401,7 +401,7 @@ public class VersionStatusTest { assertEquals(commitDate0, tester.controller().versionStatus().systemVersion().get().committedAt()); // Deploy app on version0 to keep computing statistics for that version - tester.deploymentContext().submit().deploy(); + tester.newDeploymentContext().submit().deploy(); // Commit details are updated for new version var version1 = tester.controllerTester().nextVersion(); |