blob: fdbb789b03a5dd0fcd5b62f6566710c3e7c34cce [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 com.epam.dlab.backendapi.service.impl;
import com.epam.dlab.auth.UserInfo;
import com.epam.dlab.backendapi.conf.SelfServiceApplicationConfiguration;
import com.epam.dlab.backendapi.dao.BillingDAO;
import com.epam.dlab.backendapi.dao.ImageExploratoryDao;
import com.epam.dlab.backendapi.domain.BillingReport;
import com.epam.dlab.backendapi.domain.BillingReportLine;
import com.epam.dlab.backendapi.domain.EndpointDTO;
import com.epam.dlab.backendapi.domain.ProjectDTO;
import com.epam.dlab.backendapi.domain.ProjectEndpointDTO;
import com.epam.dlab.backendapi.resources.dto.BillingFilter;
import com.epam.dlab.backendapi.roles.RoleType;
import com.epam.dlab.backendapi.roles.UserRoles;
import com.epam.dlab.backendapi.service.BillingService;
import com.epam.dlab.backendapi.service.EndpointService;
import com.epam.dlab.backendapi.service.ExploratoryService;
import com.epam.dlab.backendapi.service.ProjectService;
import com.epam.dlab.backendapi.util.BillingUtils;
import com.epam.dlab.cloud.CloudProvider;
import com.epam.dlab.constants.ServiceConsts;
import com.epam.dlab.dto.billing.BillingData;
import com.epam.dlab.exceptions.DlabException;
import com.epam.dlab.rest.client.RESTService;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.http.client.utils.URIBuilder;
import javax.ws.rs.core.GenericType;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.LocalDate;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
public class BillingServiceImpl implements BillingService {
private static final String BILLING_PATH = "/api/billing";
private static final String USAGE_DATE_FORMAT = "yyyy-MM";
private final ProjectService projectService;
private final EndpointService endpointService;
private final ExploratoryService exploratoryService;
private final SelfServiceApplicationConfiguration configuration;
private final RESTService provisioningService;
private final ImageExploratoryDao imageExploratoryDao;
private final BillingDAO billingDAO;
private final String sbn;
@Inject
public BillingServiceImpl(ProjectService projectService, EndpointService endpointService,
ExploratoryService exploratoryService, SelfServiceApplicationConfiguration configuration,
@Named(ServiceConsts.PROVISIONING_SERVICE_NAME) RESTService provisioningService, ImageExploratoryDao imageExploratoryDao,
BillingDAO billingDAO) {
this.projectService = projectService;
this.endpointService = endpointService;
this.exploratoryService = exploratoryService;
this.configuration = configuration;
this.provisioningService = provisioningService;
this.imageExploratoryDao = imageExploratoryDao;
this.billingDAO = billingDAO;
sbn = configuration.getServiceBaseName();
}
@Override
public BillingReport getBillingReport(UserInfo user, BillingFilter filter) {
setUserFilter(user, filter);
List<BillingReportLine> billingReportLines = billingDAO.aggregateBillingData(filter);
LocalDate min = billingReportLines.stream().min(Comparator.comparing(BillingReportLine::getUsageDateFrom)).map(BillingReportLine::getUsageDateFrom).orElse(null);
LocalDate max = billingReportLines.stream().max(Comparator.comparing(BillingReportLine::getUsageDateTo)).map(BillingReportLine::getUsageDateTo).orElse(null);
double sum = billingReportLines.stream().mapToDouble(BillingReportLine::getCost).sum();
String currency = billingReportLines.stream().map(BillingReportLine::getCurrency).distinct().count() == 1 ? billingReportLines.get(0).getCurrency() : null;
return BillingReport.builder()
.sbn(sbn)
.reportLines(billingReportLines)
.usageDateFrom(min)
.usageDateTo(max)
.totalCost(new BigDecimal(sum).setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue())
.currency(currency)
.isFull(isFullReport(user))
.build();
}
@Override
public String downloadReport(UserInfo user, BillingFilter filter) {
BillingReport report = getBillingReport(user, filter);
StringBuilder builder = new StringBuilder(BillingUtils.getFirstLine(report.getSbn(), report.getUsageDateFrom(), report.getUsageDateTo()));
builder.append(BillingUtils.getHeader());
try {
report.getReportLines().forEach(r -> builder.append(BillingUtils.printLine(r)));
builder.append(BillingUtils.getTotal(report.getTotalCost(), report.getCurrency()));
return builder.toString();
} catch (Exception e) {
log.error("Cannot write billing data ", e);
throw new DlabException("Cannot write billing file ", e);
}
}
public List<BillingReportLine> getExploratoryBillingData(String exploratoryId, List<String> resources) {
List<String> dlabIds = null;
try {
dlabIds = Stream.concat(
BillingUtils.getExploratoryIds(exploratoryId).stream(),
resources
.stream()
.map(BillingUtils::getComputationalIds)
.flatMap(Collection::stream)
)
.collect(Collectors.toList());
return billingDAO.findBillingData(dlabIds);
} catch (Exception e) {
log.error("Cannot retrieve billing information for {} {}", dlabIds, e.getMessage());
return Collections.emptyList();
}
}
public void updateRemoteBillingData(UserInfo userInfo) {
try {
List<EndpointDTO> endpoints = endpointService.getEndpoints();
if (CollectionUtils.isEmpty(endpoints)) {
log.error("Cannot update billing info. There are no endpoints");
throw new DlabException("Cannot update billing info. There are no endpoints");
}
Map<EndpointDTO, List<BillingData>> billingDataMap = endpoints
.stream()
.collect(Collectors.toMap(e -> e, e -> provisioningService.get(getBillingUrl(e.getUrl(), BILLING_PATH), userInfo.getAccessToken(),
new GenericType<List<BillingData>>() {
})));
billingDataMap.forEach((endpointDTO, billingData) -> {
log.info("Updating billing information for endpoint {}", endpointDTO.getName());
updateBillingData(userInfo, endpointDTO, billingData);
});
} catch (DlabException e) {
log.error("Cannot retrieve billing information for {}", e.getMessage());
}
}
private Map<String, BillingReportLine> getBillableResources(UserInfo userInfo) {
Set<ProjectDTO> projects;
if (isFullReport(userInfo)) {
projects = new HashSet<>(projectService.getProjects());
} else {
projects = new HashSet<>(projectService.getProjects(userInfo));
projects.addAll(projectService.getUserProjects(userInfo, false));
}
final Stream<BillingReportLine> ssnBillingDataStream = BillingUtils.ssnBillingDataStream(sbn);
final Stream<BillingReportLine> billableEdges = projects
.stream()
.collect(Collectors.toMap(ProjectDTO::getName, ProjectDTO::getEndpoints))
.entrySet()
.stream()
.flatMap(e -> projectEdges(sbn, e.getKey(), e.getValue()));
final Stream<BillingReportLine> billableSharedEndpoints = endpointService.getEndpoints()
.stream()
.flatMap(endpoint -> BillingUtils.sharedEndpointBillingDataStream(endpoint.getName(), sbn));
final Stream<BillingReportLine> billableUserInstances = exploratoryService.findAll(projects)
.stream()
.filter(userInstance -> Objects.nonNull(userInstance.getExploratoryId()))
.flatMap(ui -> BillingUtils.exploratoryBillingDataStream(ui, configuration.getMaxSparkInstanceCount(), sbn));
final Stream<BillingReportLine> customImages = projects
.stream()
.map(p -> imageExploratoryDao.getImagesForProject(p.getName()))
.flatMap(Collection::stream)
.flatMap(i -> BillingUtils.customImageBillingDataStream(i, sbn));
final Map<String, BillingReportLine> billableResources = Stream.of(ssnBillingDataStream, billableEdges, billableSharedEndpoints, billableUserInstances, customImages)
.flatMap(s -> s)
.collect(Collectors.toMap(BillingReportLine::getDlabId, b -> b));
log.debug("Billable resources are: {}", billableResources);
return billableResources;
}
private Stream<BillingReportLine> projectEdges(String serviceBaseName, String projectName, List<ProjectEndpointDTO> endpoints) {
return endpoints
.stream()
.flatMap(endpoint -> BillingUtils.edgeBillingDataStream(projectName, serviceBaseName, endpoint.getName(),
endpoint.getStatus().toString()));
}
private void updateBillingData(UserInfo userInfo, EndpointDTO endpointDTO, List<BillingData> billingData) {
final String endpointName = endpointDTO.getName();
final Map<String, BillingReportLine> billableResources = getBillableResources(userInfo);
if (endpointDTO.getCloudProvider() == CloudProvider.GCP) {
final Map<String, List<BillingReportLine>> gcpBillingData = billingData
.stream()
.filter(bd -> billableResources.containsKey(bd.getTag()))
.peek(bd -> bd.setApplication(endpointName))
.map(bd -> toBillingData(bd, billableResources.get(bd.getTag())))
.collect(Collectors.groupingBy(bd -> bd.getUsageDate().substring(0, USAGE_DATE_FORMAT.length())));
updateGcpBillingData(endpointName, gcpBillingData);
}
}
private void updateGcpBillingData(String endpointName, Map<String, List<BillingReportLine>> billingData) {
billingData.forEach((usageDate, billingReportLines) -> {
billingDAO.deleteByUsageDate(endpointName, usageDate);
billingDAO.save(billingReportLines);
});
}
private String getBillingUrl(String endpointUrl, String path) {
URI uri;
try {
uri = new URI(endpointUrl);
} catch (URISyntaxException e) {
log.error("Wrong URI syntax {}", e.getMessage(), e);
throw new DlabException("Wrong URI syntax");
}
return new URIBuilder()
.setScheme(uri.getScheme())
.setHost(uri.getHost())
.setPort(8088)
.setPath(path)
.toString();
}
private boolean isFullReport(UserInfo userInfo) {
return UserRoles.checkAccess(userInfo, RoleType.PAGE, "/api/infrastructure_provision/billing",
userInfo.getRoles());
}
private void setUserFilter(UserInfo userInfo, BillingFilter filter) {
if (!isFullReport(userInfo)) {
filter.setUsers(Lists.newArrayList(userInfo.getName()));
}
}
private BillingReportLine toBillingData(BillingData billingData, BillingReportLine billingReportLine) {
return BillingReportLine.builder()
.application(billingData.getApplication())
.cost(BigDecimal.valueOf(billingData.getCost()).setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue())
.currency(billingData.getCurrency())
.product(billingData.getProduct())
.project(billingReportLine.getProject())
.usageDateFrom(billingData.getUsageDateFrom())
.usageDateTo(billingData.getUsageDateTo())
.usageDate(billingData.getUsageDate())
.usageType(billingData.getUsageType())
.user(billingReportLine.getUser())
.dlabId(billingData.getTag())
.resourceType(billingReportLine.getResourceType())
.resourceName(billingReportLine.getResourceName())
.shape(billingReportLine.getShape())
.build();
}
}