package io.pivotal.cfapp.task; import java.time.Duration; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import io.pivotal.cfapp.config.PasSettings; import io.pivotal.cfapp.domain.AppDetail; import io.pivotal.cfapp.domain.EmailAttachment; import io.pivotal.cfapp.domain.EmailValidator; import io.pivotal.cfapp.domain.HygienePolicy; import io.pivotal.cfapp.domain.ServiceInstanceDetail; import io.pivotal.cfapp.domain.Space; import io.pivotal.cfapp.domain.UserSpaces; import io.pivotal.cfapp.domain.Workloads; import io.pivotal.cfapp.domain.Workloads.WorkloadsBuilder; import io.pivotal.cfapp.event.EmailNotificationEvent; import io.pivotal.cfapp.service.DormantWorkloadsService; import io.pivotal.cfapp.service.PoliciesService; import io.pivotal.cfapp.service.SpaceUsersService; import io.pivotal.cfapp.service.UserSpacesService; import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; @Slf4j @Component public class HygienePolicyExecutorTask implements PolicyExecutorTask { private final PasSettings settings; private final PoliciesService policiesService; private final SpaceUsersService spaceUsersService; private final UserSpacesService userSpacesService; private final DormantWorkloadsService dormantWorkloadsService; private final ApplicationEventPublisher publisher; @Autowired public HygienePolicyExecutorTask( PasSettings settings, PoliciesService policiesService, SpaceUsersService spaceUsersService, UserSpacesService userSpacesService, DormantWorkloadsService dormantWorkloadsService, ApplicationEventPublisher publisher ) { this.settings = settings; this.policiesService = policiesService; this.spaceUsersService = spaceUsersService; this.userSpacesService = userSpacesService; this.dormantWorkloadsService = dormantWorkloadsService; this.publisher = publisher; } @Override public void execute() { log.info("HygienePolicyExecutorTask started"); fetchHygienePolicies() .concatMap(hp -> executeHygienePolicy(hp).map(result -> Tuples.of(hp, result))) .collectList() .subscribe( results -> { results.forEach(tuple -> { notifyOperator(tuple); notifyUsers(tuple); }); log.info("HygienePolicyExecutorTask completed"); log.info("-- {} hygiene policies executed.", results.size()); }, error -> { log.error("HygienePolicyExecutorTask terminated with error", error); } ); } private void notifyOperator(Tuple2<HygienePolicy, Workloads> tuple) { log.trace("User: admin, " + tuple.getT2().toString()); publisher.publishEvent( new EmailNotificationEvent(this) .domain(settings.getAppsDomain()) .from(tuple.getT1().getOperatorTemplate().getFrom()) .recipients(tuple.getT1().getOperatorTemplate().getTo()) .subject(tuple.getT1().getOperatorTemplate().getSubject()) .body(tuple.getT1().getOperatorTemplate().getBody()) .attachments(buildAttachments(tuple)) ); } private void notifyUsers(Tuple2<HygienePolicy, Workloads> tuple) { if (tuple.getT1().getNotifyeeTemplate() != null) { // Pull distinct Set<Space> from applications and service instances Flux .fromIterable(getSpaces(tuple.getT2())) // For each Space in Set<Space>, obtain SpaceUsers#getUsers() .concatMap(space -> spaceUsersService.findByOrganizationAndSpace(space.getOrganizationName(), space.getSpaceName())) // then pair with matching space(s) that contain applications and service instances .concatMap(spaceUser -> Flux.fromIterable(spaceUser.getUsers())) .distinct() // filter out account names that are not email addresses .filter(userName -> EmailValidator.isValid(userName)) .concatMap(userName -> userSpacesService.getUserSpaces(userName)) // Create a list where each item is a tuple of user account and filtered workloads .concatMap(userSpace -> filterWorkloads(userSpace, tuple.getT2())) .delayElements(Duration.ofMillis(250)) .doOnNext( userWorkloads -> { publisher.publishEvent( new EmailNotificationEvent(this) .domain(settings.getAppsDomain()) .from(tuple.getT1().getNotifyeeTemplate().getFrom()) .recipient(userWorkloads.getT1().getAccountName()) .subject(tuple.getT1().getNotifyeeTemplate().getSubject()) .body(tuple.getT1().getNotifyeeTemplate().getBody()) .attachments(buildAttachments(Tuples.of(tuple.getT1(), userWorkloads.getT2()))) ); } ) .subscribe(); } } @Scheduled(cron = "${cron.execution}") protected void runTask() { execute(); } protected Flux<HygienePolicy> fetchHygienePolicies() { return policiesService .findAllHygienePolicies() .flatMapMany(policy -> Flux.fromIterable(policy.getHygienePolicies())); } protected Mono<Workloads> executeHygienePolicy(HygienePolicy policy) { final WorkloadsBuilder builder = Workloads.builder(); return dormantWorkloadsService .getDormantApplications(policy) .map(list -> builder.applications(list)) .then(dormantWorkloadsService.getDormantServiceInstances(policy)) .map(list -> builder.serviceInstances(list).build()); } private static Mono<Tuple2<UserSpaces, Workloads>> filterWorkloads(UserSpaces userSpaces, Workloads input){ Workloads workloads = input.matchBySpace(userSpaces.getSpaces()); log.trace(userSpaces.toString() + ", " + workloads.toString()); return Mono.just(Tuples.of(userSpaces, workloads)); } private static List<EmailAttachment> buildAttachments(Tuple2<HygienePolicy, Workloads> tuple) { String cr = System.getProperty("line.separator"); List<EmailAttachment> result = new ArrayList<>(); StringBuilder applications = new StringBuilder(); StringBuilder serviceInstances = new StringBuilder(); tuple.getT2() .getApplications() .forEach(app -> applications.append(app.toCsv()).append(cr)); tuple.getT2() .getServiceInstances() .forEach(sid -> serviceInstances.append(sid.toCsv()).append(cr)); result.add( EmailAttachment .builder() .filename(getFileNamePrefix(tuple.getT1()) + "applications") .extension(".csv") .mimeType("text/plain;charset=UTF-8") .content(applications.toString()) .headers(AppDetail.headers()) .build() ); result.add( EmailAttachment .builder() .filename(getFileNamePrefix(tuple.getT1()) + "service-instances") .extension(".csv") .mimeType("text/plain;charset=UTF-8") .content(serviceInstances.toString()) .headers(ServiceInstanceDetail.headers()) .build() ); return result; } private static String getFileNamePrefix(HygienePolicy policy) { String prefix = ""; if (policy.getDaysSinceLastUpdate() != -1) { prefix = "dormant-"; } return prefix; } private static Space buildSpace(String organization, String space) { return Space .builder() .organizationName(organization) .spaceName(space) .build(); } private static Set<Space> getSpaces(Workloads workloads) { Set<Space> applicationSpaces = workloads .getApplications() .stream() .map(app -> buildSpace(app.getOrganization(), app.getSpace())) .collect(Collectors.toSet()); Set<Space> serviceInstanceSpaces = workloads .getServiceInstances() .stream() .map(app -> buildSpace(app.getOrganization(), app.getSpace())) .collect(Collectors.toSet()); Set<Space> result = new HashSet<>(); result.addAll(applicationSpaces); result.addAll(serviceInstanceSpaces); return result; } }