diff options
author | Øyvind Grønnesby <oyving@yahooinc.com> | 2022-05-13 11:09:01 +0200 |
---|---|---|
committer | Øyvind Grønnesby <oyving@yahooinc.com> | 2022-05-13 11:09:01 +0200 |
commit | 2195313649d2b733a443fcfa4fafee9f9e6a5e56 (patch) | |
tree | 96e17ca0f477f1aec29981b82710da8f13fbd24a /controller-server | |
parent | 631c539a74a1500b7956071e1ccf375e45ceca7b (diff) |
Split tenant info into separate rest resources
Diffstat (limited to 'controller-server')
2 files changed, 210 insertions, 19 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index 91b76ac8d05..d781ea96b47 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 @@ -150,6 +150,7 @@ import java.util.Optional; import java.util.OptionalInt; import java.util.Scanner; import java.util.StringJoiner; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.logging.Level; import java.util.stream.Collectors; @@ -245,6 +246,9 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}")) return tenant(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/access/request/operator")) return accessRequests(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/info")) return tenantInfo(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/info/profile")) return tenantInfoProfile(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/info/billing")) return tenantInfoBilling(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/info/contacts")) return tenantInfoContacts(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/notifications")) return notifications(request, Optional.of(path.get("tenant")), false); if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}/validate")) return validateSecretStore(path.get("tenant"), path.get("name"), request); if (path.matches("/application/v4/tenant/{tenant}/application")) return applications(path.get("tenant"), Optional.empty(), request); @@ -298,6 +302,9 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}/access/approve/operator")) return approveAccessRequest(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/access/managed/operator")) return addManagedAccess(path.get("tenant")); if (path.matches("/application/v4/tenant/{tenant}/info")) return updateTenantInfo(path.get("tenant"), request); + if (path.matches("/application/v4/tenant/{tenant}/info/profile")) return putTenantInfo(path.get("tenant"), request, this::putTenantInfoProfile); + if (path.matches("/application/v4/tenant/{tenant}/info/billing")) return putTenantInfo(path.get("tenant"), request, this::putTenantInfoBilling); + if (path.matches("/application/v4/tenant/{tenant}/info/contacts")) return putTenantInfo(path.get("tenant"), request, this::putTenantInfoContacts); if (path.matches("/application/v4/tenant/{tenant}/archive-access")) return allowArchiveAccess(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}")) return addSecretStore(path.get("tenant"), path.get("name"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/global-rotation/override")) return setGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), false, request); @@ -500,6 +507,27 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist or does not support this")); } + private HttpResponse tenantInfoProfile(String tenantName, HttpRequest request) { + return controller.tenants().get(TenantName.from(tenantName)) + .filter(tenant -> tenant.type() == Tenant.Type.cloud) + .map(tenant -> tenantInfoProfile((CloudTenant)tenant)) + .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist or does not support this")); + } + + private HttpResponse tenantInfoBilling(String tenantName, HttpRequest request) { + return controller.tenants().get(TenantName.from(tenantName)) + .filter(tenant -> tenant.type() == Tenant.Type.cloud) + .map(tenant -> tenantInfoBilling((CloudTenant)tenant)) + .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist or does not support this")); + } + + private HttpResponse tenantInfoContacts(String tenantName, HttpRequest request) { + return controller.tenants().get(TenantName.from(tenantName)) + .filter(tenant -> tenant.type() == Tenant.Type.cloud) + .map(tenant -> tenantInfoContacts((CloudTenant) tenant)) + .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist or does not support this")); + } + private SlimeJsonResponse tenantInfo(TenantInfo info, HttpRequest request) { Slime slime = new Slime(); Cursor infoCursor = slime.setObject(); @@ -517,6 +545,141 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { return new SlimeJsonResponse(slime); } + private SlimeJsonResponse tenantInfoProfile(CloudTenant cloudTenant) { + var slime = new Slime(); + var root = slime.setObject(); + var info = cloudTenant.info(); + + if (!info.isEmpty()) { + var contact = root.setObject("contact"); + contact.setString("name", info.contact().name()); + contact.setString("email", info.contact().email()); + + var tenant = root.setObject("tenant"); + tenant.setString("company", info.name()); + tenant.setString("website", info.website()); + + toSlime(info.address(), root); // will create "address" on the parent + } + + return new SlimeJsonResponse(slime); + } + + private SlimeJsonResponse putTenantInfo(String tenantName, HttpRequest request, BiFunction<CloudTenant, Inspector, SlimeJsonResponse> handler) { + return controller.tenants().get(tenantName) + .map(tenant -> handler.apply((CloudTenant) tenant, toSlime(request.getData()).get())) + .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist or does not support this")); + } + + private SlimeJsonResponse putTenantInfoProfile(CloudTenant cloudTenant, Inspector inspector) { + var info = cloudTenant.info(); + + var mergedContact = TenantContact.empty() + .withName(getString(inspector.field("contact").field("name"), info.contact().name())) + .withEmail(getString(inspector.field("contact").field("email"), info.contact().email())); + + var mergedAddress = updateTenantInfoAddress(inspector.field("address"), info.address()); + + var mergedInfo = info + .withName(getString(inspector.field("tenant").field("name"), info.name())) + .withWebsite(getString(inspector.field("tenant").field("website"), info.website())) + .withContact(mergedContact) + .withAddress(mergedAddress); + + validateMergedTenantInfo(mergedInfo); + + controller.tenants().lockOrThrow(cloudTenant.name(), LockedTenant.Cloud.class, lockedTenant -> { + lockedTenant = lockedTenant.withInfo(mergedInfo); + controller.tenants().store(lockedTenant); + }); + + return new MessageResponse("Tenant info updated"); + } + + private SlimeJsonResponse tenantInfoBilling(CloudTenant cloudTenant) { + var slime = new Slime(); + var root = slime.setObject(); + var info = cloudTenant.info(); + + if (!info.isEmpty()) { + var billingContact = info.billingContact(); + + var contact = root.setObject("contact"); + contact.setString("name", billingContact.contact().name()); + contact.setString("email", billingContact.contact().email()); + contact.setString("phone", billingContact.contact().phone()); + + toSlime(billingContact.address(), root); // will create "address" on the parent + } + + return new SlimeJsonResponse(slime); + } + + private SlimeJsonResponse putTenantInfoBilling(CloudTenant cloudTenant, Inspector inspector) { + var info = cloudTenant.info(); + var contact = info.billingContact().contact(); + var address = info.billingContact().address(); + + var mergedContact = updateTenantInfoContact(inspector.field("contact"), contact); + var mergedAddress = updateTenantInfoAddress(inspector.field("address"), info.billingContact().address()); + + var mergedBilling = info.billingContact() + .withContact(mergedContact) + .withAddress(mergedAddress); + + var mergedInfo = info.withBilling(mergedBilling); + + // Store changes + controller.tenants().lockOrThrow(cloudTenant.name(), LockedTenant.Cloud.class, lockedTenant -> { + lockedTenant = lockedTenant.withInfo(mergedInfo); + controller.tenants().store(lockedTenant); + }); + + return new MessageResponse("Tenant info updated"); + } + + private SlimeJsonResponse tenantInfoContacts(CloudTenant cloudTenant) { + var slime = new Slime(); + var root = slime.setObject(); + toSlime(cloudTenant.info().contacts(), root); + return new SlimeJsonResponse(slime); + } + + private SlimeJsonResponse putTenantInfoContacts(CloudTenant cloudTenant, Inspector inspector) { + var mergedInfo = cloudTenant.info() + .withContacts(updateTenantInfoContacts(inspector.field("contacts"), cloudTenant.info().contacts())); + + // Store changes + controller.tenants().lockOrThrow(cloudTenant.name(), LockedTenant.Cloud.class, lockedTenant -> { + lockedTenant = lockedTenant.withInfo(mergedInfo); + controller.tenants().store(lockedTenant); + }); + + return new MessageResponse("Tenant info updated"); + } + + private void validateMergedTenantInfo(TenantInfo mergedInfo) { + // Assert that we have a valid tenant info + if (mergedInfo.contact().name().isBlank()) { + throw new IllegalArgumentException("'contactName' cannot be empty"); + } + if (mergedInfo.contact().email().isBlank()) { + throw new IllegalArgumentException("'contactEmail' cannot be empty"); + } + if (! mergedInfo.contact().email().contains("@")) { + // email address validation is notoriously hard - we should probably just try to send a + // verification email to this address. checking for @ is a simple best-effort. + throw new IllegalArgumentException("'contactEmail' needs to be an email address"); + } + if (! mergedInfo.website().isBlank()) { + try { + new URL(mergedInfo.website()); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("'website' needs to be a valid address"); + } + } + } + private void toSlime(TenantAddress address, Cursor parentCursor) { if (address.isEmpty()) return; @@ -602,25 +765,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { .withBilling(updateTenantInfoBillingContact(insp.field("billingContact"), oldInfo.billingContact())) .withContacts(updateTenantInfoContacts(insp.field("contacts"), oldInfo.contacts())); - // Assert that we have a valid tenant info - if (mergedInfo.contact().name().isBlank()) { - throw new IllegalArgumentException("'contactName' cannot be empty"); - } - if (mergedInfo.contact().email().isBlank()) { - throw new IllegalArgumentException("'contactEmail' cannot be empty"); - } - if (! mergedInfo.contact().email().contains("@")) { - // email address validation is notoriously hard - we should probably just try to send a - // verification email to this address. checking for @ is a simple best-effort. - throw new IllegalArgumentException("'contactEmail' needs to be an email address"); - } - if (! mergedInfo.website().isBlank()) { - try { - new URL(mergedInfo.website()); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("'website' needs to be a valid address"); - } - } + validateMergedTenantInfo(mergedInfo); // Store changes controller.tenants().lockOrThrow(tenant.name(), LockedTenant.Cloud.class, lockedTenant -> { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java index 5368cc73480..065492e47ec 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java @@ -80,6 +80,52 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest { } @Test + public void tenant_info_profile() { + var request = request("/application/v4/tenant/scoober/info/profile", GET) + .roles(Set.of(Role.reader(tenantName))); + tester.assertResponse(request, "{}", 200); + + var updateRequest = request("/application/v4/tenant/scoober/info/profile", PUT) + .data("{\"contact\":{\"name\":\"Some Name\",\"email\":\"foo@example.com\"},\"tenant\":{\"company\":\"Scoober, Inc.\",\"website\":\"https://example.com/\"}}") + .roles(Set.of(Role.administrator(tenantName))); + tester.assertResponse(updateRequest, "{\"message\":\"Tenant info updated\"}", 200); + + tester.assertResponse(request, "{\"contact\":{\"name\":\"Some Name\",\"email\":\"foo@example.com\"},\"tenant\":{\"company\":\"\",\"website\":\"https://example.com/\"}}", 200); + } + + @Test + public void tenant_info_billing() { + var request = request("/application/v4/tenant/scoober/info/billing", GET) + .roles(Set.of(Role.reader(tenantName))); + tester.assertResponse(request, "{}", 200); + + var fullAddress = "{\"addressLines\":\"addressLines\",\"postalCodeOrZip\":\"postalCodeOrZip\",\"city\":\"city\",\"stateRegionProvince\":\"stateRegionProvince\",\"country\":\"country\"}"; + var fullBillingContact = "{\"contact\":{\"name\":\"name\",\"email\":\"foo@example\",\"phone\":\"phone\"},\"address\":" + fullAddress + "}"; + + var updateRequest = request("/application/v4/tenant/scoober/info/billing", PUT) + .data(fullBillingContact) + .roles(Set.of(Role.administrator(tenantName))); + tester.assertResponse(updateRequest, "{\"message\":\"Tenant info updated\"}", 200); + + tester.assertResponse(request, "{\"contact\":{\"name\":\"name\",\"email\":\"foo@example\",\"phone\":\"phone\"},\"address\":{\"addressLines\":\"addressLines\",\"postalCodeOrZip\":\"postalCodeOrZip\",\"city\":\"city\",\"stateRegionProvince\":\"stateRegionProvince\",\"country\":\"country\"}}", 200); + } + + @Test + public void tenant_info_contacts() { + var request = request("/application/v4/tenant/scoober/info/contacts", GET) + .roles(Set.of(Role.reader(tenantName))); + tester.assertResponse(request, "{\"contacts\":[]}", 200); + + + var fullContacts = "{\"contacts\":[{\"audiences\":[\"tenant\"],\"email\":\"contact1@example.com\"},{\"audiences\":[\"notifications\"],\"email\":\"contact2@example.com\"},{\"audiences\":[\"tenant\",\"notifications\"],\"email\":\"contact3@example.com\"}]}"; + var updateRequest = request("/application/v4/tenant/scoober/info/contacts", PUT) + .data(fullContacts) + .roles(Set.of(Role.administrator(tenantName))); + tester.assertResponse(updateRequest, "{\"message\":\"Tenant info updated\"}", 200); + tester.assertResponse(request, fullContacts, 200); + } + + @Test public void tenant_info_workflow() { var infoRequest = request("/application/v4/tenant/scoober/info", GET) |