diff options
3 files changed, 29 insertions, 9 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 681c1b4283a..c75f3102772 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 @@ -115,7 +115,7 @@ public class Application { /** Returns the instance with the given name, or throws. */ public Instance require(InstanceName instance) { - return get(instance).orElseThrow(() -> new IllegalArgumentException("Unknown instance '" + instance + "'")); + return get(instance).orElseThrow(() -> new IllegalArgumentException("Unknown instance '" + instance + "' in '" + id + "'")); } /** 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 4725af6fd28..07be894eaef 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 @@ -146,8 +146,16 @@ public class ApplicationController { Once.after(Duration.ofMinutes(1), () -> { Instant start = clock.instant(); int count = 0; - for (Application application : curator.readApplications()) { - lockApplicationIfPresent(application.id(), this::store); + for (TenantAndApplicationId id: curator.readApplicationIds()) { + lockApplicationIfPresent(id, application -> { + if (id.tenant().value().startsWith("by-")) + application = application.with(DeploymentSpec.empty); + else + for (InstanceName instance : application.get().deploymentSpec().instanceNames()) + if ( ! application.get().instances().keySet().contains(instance)) + application = application.withNewInstance(instance); + store(application); + }); count++; } log.log(Level.INFO, String.format("Wrote %d applications in %s", count, @@ -710,6 +718,7 @@ public class ApplicationController { if (instances.size() > 1) throw new IllegalArgumentException("Could not delete application; more than one instance present: " + instances); + lockApplicationOrThrow(id, application -> store(application.with(DeploymentSpec.empty))); for (ApplicationId instance : instances) deleteInstance(instance); @@ -736,6 +745,9 @@ public class ApplicationController { throw new IllegalArgumentException("Could not delete '" + application + "': It has active deployments in: " + application.get().require(instanceId.instance()).deployments().keySet().stream().map(ZoneId::toString) .sorted().collect(Collectors.joining(", "))); + if ( ! application.get().deploymentSpec().equals(DeploymentSpec.empty) + && application.get().deploymentSpec().instanceNames().contains(instanceId.instance())) + throw new IllegalArgumentException("Can not delete '" + instanceId + "', which is specified in 'deployment.xml'; remove it there instead"); Instance instance = application.get().require(instanceId.instance()); instance.rotations().forEach(assignedRotation -> { 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 9f869b0904b..6e778fd08c1 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 @@ -6,6 +6,7 @@ import ai.vespa.hosted.api.Signatures; import com.yahoo.application.container.handler.Request; import com.yahoo.component.Version; import com.yahoo.config.application.api.ValidationId; +import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.AthenzService; @@ -718,11 +719,14 @@ public class ApplicationApiTest extends ControllerContainerTest { .userIdentity(USER_ID), ""); + // POST an application package with an empty deployment spec, to allow removal of production instances. + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/submit", POST) + .screwdriverIdentity(SCREWDRIVER_ID) + .data(createApplicationSubmissionData(new ApplicationPackageBuilder() + .allow(ValidationId.deploymentRemoval) + .build(), 1000)), + "{\"message\":\"Application package version: 1.0.5-commit1, source revision of repository 'repository1', branch 'master' with commit 'commit1', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\"}"); // DELETE all instances under an application to delete the application - tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/default", DELETE) - .userIdentity(USER_ID) - .oktaAccessToken(OKTA_AT).oktaIdentityToken(OKTA_IT), - "{\"message\":\"Deleted instance tenant1.application1.default\"}"); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/my-user", DELETE) .userIdentity(USER_ID) .oktaAccessToken(OKTA_AT).oktaIdentityToken(OKTA_IT), @@ -735,8 +739,12 @@ public class ApplicationApiTest extends ControllerContainerTest { .userIdentity(USER_ID) .oktaAccessToken(OKTA_AT).oktaIdentityToken(OKTA_IT), "{\"message\":\"Deleted instance tenant1.application1.instance2\"}"); - - // DELETE a tenant + // DELETE the application which now only has one instance + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE) + .userIdentity(USER_ID) + .oktaAccessToken(OKTA_AT).oktaIdentityToken(OKTA_IT), + "{\"message\":\"Deleted application tenant1.application1\"}"); + // DELETE an empty tenant tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE).userIdentity(USER_ID) .oktaAccessToken(OKTA_AT).oktaIdentityToken(OKTA_IT), new File("tenant-without-applications.json")); |