/* * Copyright 2013-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.cloudfoundry.operations.routes; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.Resource; import org.cloudfoundry.client.v2.applications.ApplicationResource; import org.cloudfoundry.client.v2.applications.AssociateApplicationRouteRequest; import org.cloudfoundry.client.v2.applications.AssociateApplicationRouteResponse; import org.cloudfoundry.client.v2.applications.RemoveApplicationRouteRequest; import org.cloudfoundry.client.v2.organizations.ListOrganizationPrivateDomainsRequest; import org.cloudfoundry.client.v2.organizations.ListOrganizationSpacesRequest; import org.cloudfoundry.client.v2.privatedomains.PrivateDomainResource; import org.cloudfoundry.client.v2.routes.AbstractRouteResource; import org.cloudfoundry.client.v2.routes.CreateRouteResponse; import org.cloudfoundry.client.v2.routes.DeleteRouteResponse; import org.cloudfoundry.client.v2.routes.ListRouteApplicationsRequest; import org.cloudfoundry.client.v2.routes.RouteEntity; import org.cloudfoundry.client.v2.routes.RouteExistsRequest; import org.cloudfoundry.client.v2.routes.RouteResource; import org.cloudfoundry.client.v2.shareddomains.ListSharedDomainsRequest; import org.cloudfoundry.client.v2.shareddomains.SharedDomainResource; import org.cloudfoundry.client.v2.spaces.ListSpaceApplicationsRequest; import org.cloudfoundry.client.v2.spaces.ListSpaceRoutesRequest; import org.cloudfoundry.client.v2.spaces.SpaceResource; import org.cloudfoundry.operations.util.OperationsLogging; import org.cloudfoundry.util.ExceptionUtils; import org.cloudfoundry.util.JobUtils; import org.cloudfoundry.util.OperationUtils; import org.cloudfoundry.util.PaginationUtils; import org.cloudfoundry.util.ResourceUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuples; import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; import java.util.function.UnaryOperator; import static org.cloudfoundry.util.tuple.TupleUtils.function; import static org.cloudfoundry.util.tuple.TupleUtils.predicate; public final class DefaultRoutes implements Routes { private final Mono<CloudFoundryClient> cloudFoundryClient; private final Mono<String> organizationId; private final Mono<String> spaceId; public DefaultRoutes(Mono<CloudFoundryClient> cloudFoundryClient, Mono<String> organizationId, Mono<String> spaceId) { this.cloudFoundryClient = cloudFoundryClient; this.organizationId = organizationId; this.spaceId = spaceId; } @Override public Mono<Boolean> check(CheckRouteRequest request) { return Mono .when(this.cloudFoundryClient, this.organizationId) .then(function((cloudFoundryClient, organizationId) -> Mono .when( Mono.just(cloudFoundryClient), getOptionalDomainId(cloudFoundryClient, organizationId, request.getDomain()) ))) .then(function((cloudFoundryClient, domainId) -> requestRouteExists(cloudFoundryClient, domainId, request.getHost(), request.getPath()))) .defaultIfEmpty(false) .transform(OperationsLogging.log("Check Route Exists")) .checkpoint(); } @Override public Mono<Integer> create(CreateRouteRequest request) { return Mono .when(this.cloudFoundryClient, this.organizationId) .then(function((cloudFoundryClient, organizationId) -> Mono .when( Mono.just(cloudFoundryClient), getSpaceId(cloudFoundryClient, organizationId, request.getSpace()), getDomainId(cloudFoundryClient, organizationId, request.getDomain()) ))) .then(function((cloudFoundryClient, spaceId, domainId) -> requestCreateRoute(cloudFoundryClient, domainId, request.getHost(), request.getPath(), request.getPort(), request.getRandomPort(), spaceId) .map(ResourceUtils::getEntity) .then(routeEntity -> Mono.justOrEmpty(routeEntity.getPort())) )) .transform(OperationsLogging.log("Create Route")) .checkpoint(); } @Override public Mono<Void> delete(DeleteRouteRequest request) { return Mono .when(this.cloudFoundryClient, this.organizationId) .then(function((cloudFoundryClient, organizationId) -> Mono .when( Mono.just(cloudFoundryClient), getDomainId(cloudFoundryClient, organizationId, request.getDomain()) ))) .then(function((cloudFoundryClient, domainId) -> Mono .when( Mono.just(cloudFoundryClient), Mono.just(request.getCompletionTimeout()), getRouteId(cloudFoundryClient, request.getHost(), request.getDomain(), domainId, request.getPath(), request.getPort()) ))) .then(function(DefaultRoutes::deleteRoute)) .transform(OperationsLogging.log("Delete Route")) .checkpoint(); } @Override public Mono<Void> deleteOrphanedRoutes(DeleteOrphanedRoutesRequest request) { return Mono .when(this.cloudFoundryClient, this.spaceId) .flatMapMany(function((cloudFoundryClient, spaceId) -> requestSpaceRoutes(cloudFoundryClient, spaceId) .filter(route -> isRouteOrphan(ResourceUtils.getEntity(route))) .map(ResourceUtils::getId) .map(routeId -> Tuples.of(cloudFoundryClient, routeId)))) .flatMap(function((cloudFoundryClient, routeId) -> getApplications(cloudFoundryClient, routeId) .map(applicationResources -> Tuples.of(cloudFoundryClient, applicationResources, routeId)))) .filter(predicate((cloudFoundryClient, applicationResources, routeId) -> isApplicationOrphan(applicationResources))) .flatMap(function((cloudFoundryClient, applicationResources, routeId) -> deleteRoute(cloudFoundryClient, request.getCompletionTimeout(), routeId))) .then() .transform(OperationsLogging.log("Delete Orphaned Routes")) .checkpoint(); } @Override public Flux<Route> list(ListRoutesRequest request) { return Mono .when(this.cloudFoundryClient, this.organizationId) .then(function((cloudFoundryClient, organizationId) -> Mono .when( Mono.just(cloudFoundryClient), getAllDomains(cloudFoundryClient, organizationId), getAllSpaces(cloudFoundryClient, organizationId) ))) .flatMapMany(function((cloudFoundryClient, domains, spaces) -> getRoutes(cloudFoundryClient, request, this.organizationId, this.spaceId) .map(resource -> Tuples.of(cloudFoundryClient, domains, resource, spaces)))) .flatMap(function((cloudFoundryClient, domains, resource, spaces) -> Mono .when( getApplicationNames(cloudFoundryClient, ResourceUtils.getId(resource)), getDomainName(domains, ResourceUtils.getEntity(resource).getDomainId()), Mono.just(resource), getSpaceName(spaces, ResourceUtils.getEntity(resource).getSpaceId()) ))) .map(function(DefaultRoutes::toRoute)) .transform(OperationsLogging.log("List Routes")) .checkpoint(); } @Override public Mono<Integer> map(MapRouteRequest request) { return Mono .when(this.cloudFoundryClient, this.organizationId, this.spaceId) .then(function((cloudFoundryClient, organizationId, spaceId) -> Mono .when( Mono.just(cloudFoundryClient), getOrCreateRoute(cloudFoundryClient, organizationId, spaceId, request.getDomain(), request.getHost(), request.getPath(), request.getPort(), request.getRandomPort()), getApplicationId(cloudFoundryClient, request.getApplicationName(), spaceId) ))) .then(function((cloudFoundryClient, routeResource, applicationId) -> requestAssociateRoute(cloudFoundryClient, applicationId, ResourceUtils.getId(routeResource)))) .then(Mono.justOrEmpty(request.getPort())) .transform(OperationsLogging.log("Map Route")) .checkpoint(); } @Override public Mono<Void> unmap(UnmapRouteRequest request) { return Mono .when(this.cloudFoundryClient, this.organizationId, this.spaceId) .then(function((cloudFoundryClient, organizationId, spaceId) -> Mono .when( Mono.just(cloudFoundryClient), getApplicationId(cloudFoundryClient, request.getApplicationName(), spaceId), getDomainId(cloudFoundryClient, organizationId, request.getDomain()) .then(domainId -> getRouteId(cloudFoundryClient, request.getHost(), request.getDomain(), domainId, request.getPath(), request.getPort())) ))) .then(function(DefaultRoutes::requestRemoveRouteFromApplication)) .transform(OperationsLogging.log("Unmap Route")) .checkpoint(); } private static Mono<Void> deleteRoute(CloudFoundryClient cloudFoundryClient, Duration completionTimeout, String routeId) { return requestDeleteRoute(cloudFoundryClient, routeId) .then(job -> JobUtils.waitForCompletion(cloudFoundryClient, completionTimeout, job)); } private static Mono<Map<String, String>> getAllDomains(CloudFoundryClient cloudFoundryClient, String organizationId) { return requestAllPrivateDomains(cloudFoundryClient, organizationId) .map(resource -> Tuples.of(ResourceUtils.getId(resource), ResourceUtils.getEntity(resource).getName())) .mergeWith(requestAllSharedDomains(cloudFoundryClient) .map(resource -> Tuples.of(ResourceUtils.getId(resource), ResourceUtils.getEntity(resource).getName()))) .collectMap(function((id, name) -> id), function((id, name) -> name)); } private static Mono<Map<String, String>> getAllSpaces(CloudFoundryClient cloudFoundryClient, String organizationId) { return requestAllSpaces(cloudFoundryClient, organizationId) .map(resource -> Tuples.of(ResourceUtils.getId(resource), ResourceUtils.getEntity(resource).getName())) .collectMap(function((id, name) -> id), function((id, name) -> name)); } private static Mono<ApplicationResource> getApplication(CloudFoundryClient cloudFoundryClient, String application, String spaceId) { return requestApplications(cloudFoundryClient, application, spaceId) .single() .onErrorResume(NoSuchElementException.class, t -> ExceptionUtils.illegalArgument("Application %s does not exist", application)); } private static Mono<String> getApplicationId(CloudFoundryClient cloudFoundryClient, String application, String spaceId) { return getApplication(cloudFoundryClient, application, spaceId) .map(ResourceUtils::getId); } private static Mono<List<String>> getApplicationNames(CloudFoundryClient cloudFoundryClient, String routeId) { return requestApplications(cloudFoundryClient, routeId) .map(resource -> ResourceUtils.getEntity(resource).getName()) .collectList(); } private static Mono<List<ApplicationResource>> getApplications(CloudFoundryClient cloudFoundryClient, String routeId) { return requestApplications(cloudFoundryClient, routeId) .collectList(); } private static Mono<Resource<?>> getDomain(CloudFoundryClient cloudFoundryClient, String organizationId, String domain) { return getDomains(cloudFoundryClient, organizationId, domain) .single() .onErrorResume(NoSuchElementException.class, t -> ExceptionUtils.illegalArgument("Domain %s does not exist", domain)); } private static Mono<String> getDomainId(CloudFoundryClient cloudFoundryClient, String organizationId, String domain) { return getDomain(cloudFoundryClient, organizationId, domain) .map(ResourceUtils::getId); } private static Mono<String> getDomainName(Map<String, String> domains, String domainId) { return Mono.just(domains.get(domainId)); } private static Flux<Resource<?>> getDomains(CloudFoundryClient cloudFoundryClient, String organizationId, String domain) { return requestPrivateDomains(cloudFoundryClient, organizationId, domain) .map(OperationUtils.<PrivateDomainResource, Resource<?>>cast()) .switchIfEmpty(requestSharedDomains(cloudFoundryClient, domain)); } private static Mono<String> getOptionalDomainId(CloudFoundryClient cloudFoundryClient, String organizationId, String domain) { return getDomains(cloudFoundryClient, organizationId, domain) .singleOrEmpty() .map(ResourceUtils::getId); } private static Mono<AbstractRouteResource> getOrCreateRoute(CloudFoundryClient cloudFoundryClient, String organizationId, String spaceId, String domain, String host, String path, Integer port, Boolean randomPort) { return getDomainId(cloudFoundryClient, organizationId, domain) .then(domainId -> getRoute(cloudFoundryClient, domainId, host, path, port) .cast(AbstractRouteResource.class) .switchIfEmpty(requestCreateRoute(cloudFoundryClient, domainId, host, path, port, randomPort, spaceId))); } private static Mono<RouteResource> getRoute(CloudFoundryClient cloudFoundryClient, String domainId, String domain, String host, String path, Integer port) { return getRoute(cloudFoundryClient, domainId, host, path, port) .switchIfEmpty(ExceptionUtils.illegalArgument("Route for %s does not exist", domain)); } private static Mono<RouteResource> getRoute(CloudFoundryClient cloudFoundryClient, String domainId, String host, String path, Integer port) { if (port != null) { return requestRoutes(cloudFoundryClient, domainId, null, null, port) .singleOrEmpty(); } else { return requestRoutes(cloudFoundryClient, domainId, host, path, port) .filter(resource -> isIdentical(nullSafe(host), ResourceUtils.getEntity(resource).getHost())) .filter(resource -> isIdentical(Optional.ofNullable(path).orElse(""), ResourceUtils.getEntity(resource).getPath())) .singleOrEmpty(); } } private static Mono<String> getRouteId(CloudFoundryClient cloudFoundryClient, String host, String domain, String domainId, String path, Integer port) { return getRoute(cloudFoundryClient, domainId, domain, host, path, port) .map(ResourceUtils::getId); } private static Flux<RouteResource> getRoutes(CloudFoundryClient cloudFoundryClient, ListRoutesRequest request, Mono<String> organizationId, Mono<String> spaceId) { if (Level.ORGANIZATION == request.getLevel()) { return organizationId .flatMapMany(organizationId1 -> requestRoutes(cloudFoundryClient, builder -> builder.organizationId(organizationId1))); } else { return spaceId .flatMapMany(spaceId1 -> requestSpaceRoutes(cloudFoundryClient, spaceId1)); } } private static Mono<SpaceResource> getSpace(CloudFoundryClient cloudFoundryClient, String organizationId, String space) { return requestSpaces(cloudFoundryClient, organizationId, space) .single() .onErrorResume(NoSuchElementException.class, t -> ExceptionUtils.illegalArgument("Space %s does not exist", space)); } private static Mono<String> getSpaceId(CloudFoundryClient cloudFoundryClient, String organizationId, String space) { return getSpace(cloudFoundryClient, organizationId, space) .map(ResourceUtils::getId); } private static Mono<String> getSpaceName(Map<String, String> spaces, String spaceId) { return Mono.just(spaces.get(spaceId)); } private static boolean isApplicationOrphan(List<ApplicationResource> applications) { return applications.isEmpty(); } private static boolean isIdentical(String s, String t) { return s == null ? t == null : s.equals(t); } private static String nullSafe(String host) { return host == null ? "" : host; } private static Flux<PrivateDomainResource> requestAllPrivateDomains(CloudFoundryClient cloudFoundryClient, String organizationId) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.organizations() .listPrivateDomains(ListOrganizationPrivateDomainsRequest.builder() .organizationId(organizationId) .page(page) .build())); } private static Flux<SharedDomainResource> requestAllSharedDomains(CloudFoundryClient cloudFoundryClient) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.sharedDomains() .list(ListSharedDomainsRequest.builder() .page(page) .build())); } private static Flux<SpaceResource> requestAllSpaces(CloudFoundryClient cloudFoundryClient, String organizationId) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.organizations() .listSpaces(ListOrganizationSpacesRequest.builder() .organizationId(organizationId) .page(page) .build())); } private static Flux<ApplicationResource> requestApplications(CloudFoundryClient cloudFoundryClient, String routeId) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.routes() .listApplications(ListRouteApplicationsRequest.builder() .routeId(routeId) .page(page) .build())); } private static Flux<ApplicationResource> requestApplications(CloudFoundryClient cloudFoundryClient, String application, String spaceId) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.spaces() .listApplications(ListSpaceApplicationsRequest.builder() .name(application) .page(page) .spaceId(spaceId) .build())); } private static Mono<AssociateApplicationRouteResponse> requestAssociateRoute(CloudFoundryClient cloudFoundryClient, String applicationId, String routeId) { return cloudFoundryClient.applicationsV2() .associateRoute(AssociateApplicationRouteRequest.builder() .applicationId(applicationId) .routeId(routeId) .build()); } private static Mono<CreateRouteResponse> requestCreateRoute(CloudFoundryClient cloudFoundryClient, String domainId, String host, String path, Integer port, Boolean randomPort, String spaceId) { org.cloudfoundry.client.v2.routes.CreateRouteRequest.Builder builder = org.cloudfoundry.client.v2.routes.CreateRouteRequest.builder(); if (randomPort != null && randomPort) { builder.generatePort(true); } else if (port != null) { builder.port(port); } else { builder.host(host); builder.path(path); } return cloudFoundryClient.routes() .create(builder .domainId(domainId) .spaceId(spaceId) .build()); } private static Mono<DeleteRouteResponse> requestDeleteRoute(CloudFoundryClient cloudFoundryClient, String routeId) { return cloudFoundryClient.routes() .delete(org.cloudfoundry.client.v2.routes.DeleteRouteRequest.builder() .async(true) .routeId(routeId) .build()); } private static Flux<PrivateDomainResource> requestPrivateDomains(CloudFoundryClient cloudFoundryClient, String organizationId, String domain) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.organizations() .listPrivateDomains(ListOrganizationPrivateDomainsRequest.builder() .organizationId(organizationId) .name(domain) .page(page) .build())); } private static Mono<Void> requestRemoveRouteFromApplication(CloudFoundryClient cloudFoundryClient, String applicationId, String routeId) { return cloudFoundryClient.applicationsV2() .removeRoute(RemoveApplicationRouteRequest.builder() .applicationId(applicationId) .routeId(routeId) .build()); } private static Mono<Boolean> requestRouteExists(CloudFoundryClient cloudFoundryClient, String domainId, String host, String path) { return cloudFoundryClient.routes() .exists(RouteExistsRequest.builder() .domainId(domainId) .host(host) .path(path) .build()); } private static Flux<RouteResource> requestRoutes(CloudFoundryClient cloudFoundryClient, UnaryOperator<org.cloudfoundry.client.v2.routes.ListRoutesRequest.Builder> modifier) { org.cloudfoundry.client.v2.routes.ListRoutesRequest.Builder listBuilder = modifier.apply(org.cloudfoundry.client.v2.routes.ListRoutesRequest.builder()); return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.routes() .list(listBuilder .page(page) .build())); } private static Flux<RouteResource> requestRoutes(CloudFoundryClient cloudFoundryClient, String domainId, String host, String path, Integer port) { return requestRoutes(cloudFoundryClient, builder -> builder .domainId(domainId) .hosts(Optional.ofNullable(host).map(Collections::singletonList).orElse(null)) .paths(Optional.ofNullable(path).map(Collections::singletonList).orElse(null)) .port(Optional.ofNullable(port).orElse(null)) ); } private static Flux<SharedDomainResource> requestSharedDomains(CloudFoundryClient cloudFoundryClient, String domain) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.sharedDomains() .list(ListSharedDomainsRequest.builder() .name(domain) .page(page) .build())); } private static Flux<RouteResource> requestSpaceRoutes(CloudFoundryClient cloudFoundryClient, String spaceId) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.spaces() .listRoutes(ListSpaceRoutesRequest.builder() .spaceId(spaceId) .page(page) .build())); } private static Flux<SpaceResource> requestSpaces(CloudFoundryClient cloudFoundryClient, String organizationId, String space) { return PaginationUtils .requestClientV2Resources(page -> cloudFoundryClient.organizations() .listSpaces(ListOrganizationSpacesRequest.builder() .organizationId(organizationId) .name(space) .page(page) .build())); } private static Route toRoute(List<String> applications, String domain, RouteResource resource, String space) { RouteEntity entity = ResourceUtils.getEntity(resource); return Route.builder() .applications(applications) .domain(domain) .host(entity.getHost()) .id(ResourceUtils.getId(resource)) .path(entity.getPath()) .space(space) .build(); } private boolean isRouteOrphan(RouteEntity entity) { return entity.getServiceInstanceId() == null || entity.getServiceInstanceId().isEmpty(); } }