package com.hubspot.baragon.service.managers; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Optional; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.inject.Inject; import com.google.inject.Singleton; import com.hubspot.baragon.data.BaragonAgentResponseDatastore; import com.hubspot.baragon.data.BaragonLoadBalancerDatastore; import com.hubspot.baragon.data.BaragonRequestDatastore; import com.hubspot.baragon.data.BaragonResponseHistoryDatastore; import com.hubspot.baragon.data.BaragonStateDatastore; import com.hubspot.baragon.exceptions.InvalidRequestActionException; import com.hubspot.baragon.exceptions.InvalidUpstreamsException; import com.hubspot.baragon.exceptions.RequestAlreadyEnqueuedException; import com.hubspot.baragon.models.BaragonGroup; import com.hubspot.baragon.models.BaragonRequest; import com.hubspot.baragon.models.BaragonResponse; import com.hubspot.baragon.models.BaragonService; import com.hubspot.baragon.models.InternalRequestStates; import com.hubspot.baragon.models.InternalStatesMap; import com.hubspot.baragon.models.QueuedRequestId; import com.hubspot.baragon.models.RequestAction; import com.hubspot.baragon.service.config.BaragonConfiguration; @Singleton public class RequestManager { private static final Logger LOG = LoggerFactory.getLogger(RequestManager.class); private final BaragonRequestDatastore requestDatastore; private final BaragonLoadBalancerDatastore loadBalancerDatastore; private final BaragonStateDatastore stateDatastore; private final BaragonAgentResponseDatastore agentResponseDatastore; private final BaragonResponseHistoryDatastore responseHistoryDatastore; private final BaragonConfiguration configuration; @Inject public RequestManager(BaragonRequestDatastore requestDatastore, BaragonLoadBalancerDatastore loadBalancerDatastore, BaragonStateDatastore stateDatastore, BaragonAgentResponseDatastore agentResponseDatastore, BaragonResponseHistoryDatastore responseHistoryDatastore, BaragonConfiguration configuration) { this.requestDatastore = requestDatastore; this.loadBalancerDatastore = loadBalancerDatastore; this.stateDatastore = stateDatastore; this.agentResponseDatastore = agentResponseDatastore; this.responseHistoryDatastore = responseHistoryDatastore; this.configuration = configuration; } public Optional<BaragonRequest> getRequest(String requestId) { return requestDatastore.getRequest(requestId); } public Optional<InternalRequestStates> getRequestState(String requestId) { return requestDatastore.getRequestState(requestId); } public void setRequestState(String requestId, InternalRequestStates state) { requestDatastore.setRequestState(requestId, state); } public void setRequestMessage(String requestId, String message) { requestDatastore.setRequestMessage(requestId, message); } public List<QueuedRequestId> getQueuedRequestIds() { return requestDatastore.getQueuedRequestIds(); } public void removeQueuedRequest(QueuedRequestId queuedRequestId) { requestDatastore.removeQueuedRequest(queuedRequestId); } public List<BaragonResponse> getResponsesForService(String serviceId) { List<BaragonResponse> responses = new ArrayList<>(); for (String requestId : requestDatastore.getAllRequestIds()) { Optional<BaragonRequest> maybeRequest = requestDatastore.getRequest(requestId); if (maybeRequest.isPresent() && maybeRequest.get().getLoadBalancerService().getServiceId().equals(serviceId)) { Optional<InternalRequestStates> maybeStatus = requestDatastore.getRequestState(requestId); if (maybeStatus.isPresent()) { responses.add(new BaragonResponse(requestId, InternalStatesMap.getRequestState(maybeStatus.get()), requestDatastore.getRequestMessage(requestId), Optional.of(agentResponseDatastore.getLastResponses(requestId)), maybeRequest)); } } } responses.addAll(responseHistoryDatastore.getResponsesForService(serviceId, configuration.getHistoryConfiguration().getMaxResponsesToFetch())); return responses; } public Optional<BaragonResponse> getResponse(String requestId) { Optional<BaragonResponse> maybeActiveRequestResponse = getResponseFromActiveRequests(requestId); if (maybeActiveRequestResponse.isPresent()) { return maybeActiveRequestResponse; } else { Optional<String> maybeServiceId = responseHistoryDatastore.getServiceIdForRequestId(requestId); if (maybeServiceId.isPresent()) { return responseHistoryDatastore.getResponse(maybeServiceId.get(), requestId); } else { return Optional.absent(); } } } public Optional<BaragonResponse> getResponse(String serviceId, String requestId) { Optional<BaragonResponse> maybeActiveRequestResponse = getResponseFromActiveRequests(requestId); if (maybeActiveRequestResponse.isPresent()) { return maybeActiveRequestResponse; } else { return responseHistoryDatastore.getResponse(serviceId, requestId); } } private Optional<BaragonResponse> getResponseFromActiveRequests(String requestId) { if (requestDatastore.activeRequestExists(requestId)) { final Optional<InternalRequestStates> maybeStatus = requestDatastore.getRequestState(requestId); if (!maybeStatus.isPresent()) { return Optional.absent(); } final Optional<BaragonRequest> maybeRequest = requestDatastore.getRequest(requestId); if (!maybeRequest.isPresent()) { return Optional.absent(); } return Optional.of(new BaragonResponse(requestId, InternalStatesMap.getRequestState(maybeStatus.get()), requestDatastore.getRequestMessage(requestId), Optional.of(agentResponseDatastore.getLastResponses(requestId)), maybeRequest)); } else { return Optional.absent(); } } public Map<String, String> getBasePathConflicts(BaragonRequest request) { final BaragonService service = request.getLoadBalancerService(); final Map<String, String> loadBalancerServiceIds = Maps.newHashMap(); for (String loadBalancerGroup : service.getLoadBalancerGroups()) { Optional<BaragonGroup> maybeGroup = loadBalancerDatastore.getLoadBalancerGroup(loadBalancerGroup); for (String path : service.getAllPaths()) { final Optional<String> maybeServiceIdForPath = loadBalancerDatastore.getBasePathServiceId(loadBalancerGroup, path); if (maybeServiceIdForPath.isPresent() && !maybeServiceIdForPath.get().equals(service.getServiceId())) { if (!request.getReplaceServiceId().isPresent() || (request.getReplaceServiceId().isPresent() && !request.getReplaceServiceId().get().equals(maybeServiceIdForPath.get()))) { loadBalancerServiceIds.put(loadBalancerGroup, maybeServiceIdForPath.get()); continue; } } if (!path.startsWith("/")) { if (maybeGroup.isPresent() && maybeGroup.get().getDefaultDomain().isPresent() && path.startsWith(maybeGroup.get().getDefaultDomain().get())) { Optional<String> maybeServiceForDefaultDomainPath = loadBalancerDatastore.getBasePathServiceId(loadBalancerGroup, path.replace(maybeGroup.get().getDefaultDomain().get(), "")); if (maybeServiceForDefaultDomainPath.isPresent() && !maybeServiceForDefaultDomainPath.get().equals(service.getServiceId())) { if (!request.getReplaceServiceId().isPresent() || (request.getReplaceServiceId().isPresent() && !request.getReplaceServiceId().get().equals(maybeServiceForDefaultDomainPath.get()))) { loadBalancerServiceIds.put(loadBalancerGroup, maybeServiceForDefaultDomainPath.get()); } } } } } } return loadBalancerServiceIds; } public void revertBasePath(BaragonRequest request) { Optional<BaragonService> maybeOriginalService = Optional.absent(); if (request.getReplaceServiceId().isPresent()) { maybeOriginalService = stateDatastore.getService(request.getReplaceServiceId().get()); } if (!maybeOriginalService.isPresent()) { maybeOriginalService = stateDatastore.getService(request.getLoadBalancerService().getServiceId()); } // if the request is not in the state datastore (ie. no previous request) clear the base path lock if (!maybeOriginalService.isPresent()) { for (String loadBalancerGroup : request.getLoadBalancerService().getLoadBalancerGroups()) { for (String path : request.getLoadBalancerService().getAllPaths()) { loadBalancerDatastore.clearBasePath(loadBalancerGroup, path); } } } // if we changed the base path, revert it to the old one if (maybeOriginalService.isPresent() && request.getReplaceServiceId().isPresent() && maybeOriginalService.get().getServiceId().equals(request.getReplaceServiceId().get())) { lockBasePaths(request.getLoadBalancerService().getLoadBalancerGroups(), request.getLoadBalancerService().getAllPaths(), maybeOriginalService.get().getServiceId()); } } public Set<String> getMissingLoadBalancerGroups(BaragonRequest request) { final Set<String> groups = new HashSet<>(request.getLoadBalancerService().getLoadBalancerGroups()); return Sets.difference(groups, loadBalancerDatastore.getLoadBalancerGroupNames()); } public void lockBasePaths(BaragonRequest request) { for (String loadBalancerGroup : request.getLoadBalancerService().getLoadBalancerGroups()) { for (String path : request.getLoadBalancerService().getAllPaths()) { loadBalancerDatastore.setBasePathServiceId(loadBalancerGroup, path, request.getLoadBalancerService().getServiceId()); } } } public void lockBasePaths(Set<String> loadBalancerGroups, List<String> paths, String serviceId) { for (String loadBalancerGroup : loadBalancerGroups) { for (String path : paths) { loadBalancerDatastore.setBasePathServiceId(loadBalancerGroup, path, serviceId); } } } public BaragonResponse enqueueRequest(BaragonRequest request) throws RequestAlreadyEnqueuedException, InvalidRequestActionException, InvalidUpstreamsException { final Optional<BaragonResponse> maybePreexistingResponse = getResponse(request.getLoadBalancerService().getServiceId(), request.getLoadBalancerRequestId()); if (maybePreexistingResponse.isPresent()) { Optional<BaragonRequest> maybePreexistingRequest = requestDatastore.getRequest(request.getLoadBalancerRequestId()); if (maybePreexistingRequest.isPresent() && !maybePreexistingRequest.get().equals(request)) { throw new RequestAlreadyEnqueuedException(request.getLoadBalancerRequestId(), maybePreexistingResponse.get(), String.format("Request %s is already enqueued with different parameters", request.getLoadBalancerRequestId())); } else { return maybePreexistingResponse.get(); } } if (request.isNoReload() && request.getAction().isPresent() && request.getAction().get().equals(RequestAction.RELOAD)) { throw new InvalidRequestActionException("You can not specify 'noReload' on a request with action 'RELOAD'"); } if (!request.getReplaceUpstreams().isEmpty() && (!request.getAddUpstreams().isEmpty() || !request.getRemoveUpstreams().isEmpty())) { throw new InvalidUpstreamsException("If overrideUpstreams is specified, addUpstreams and removeUpstreams mustbe empty"); } if (request.getAction().isPresent() && request.getAction().equals(Optional.of(RequestAction.REVERT))) { throw new InvalidRequestActionException("The REVERT action may only be used internally by Baragon, you may specify UPDATE, DELETE, RELOAD, or leave the action blank(UPDATE)"); } final QueuedRequestId queuedRequestId = requestDatastore.enqueueRequest(request, InternalRequestStates.PENDING); requestDatastore.setRequestMessage(request.getLoadBalancerRequestId(), String.format("Queued as %s", queuedRequestId)); return getResponse(request.getLoadBalancerService().getServiceId(), request.getLoadBalancerRequestId()).get(); } public Optional<InternalRequestStates> cancelRequest(String requestId) { final Optional<InternalRequestStates> maybeState = getRequestState(requestId); if (!maybeState.isPresent() || !InternalStatesMap.isCancelable(maybeState.get())) { return maybeState; } requestDatastore.setRequestState(requestId, InternalRequestStates.CANCELLED_SEND_REVERT_REQUESTS); return Optional.of(InternalRequestStates.CANCELLED_SEND_REVERT_REQUESTS); } public void saveResponseToHistory(BaragonRequest request, InternalRequestStates state) { BaragonResponse response = new BaragonResponse(request.getLoadBalancerRequestId(), InternalStatesMap.getRequestState(state), requestDatastore.getRequestMessage(request.getLoadBalancerRequestId()), Optional.of(agentResponseDatastore.getLastResponses(request.getLoadBalancerRequestId())), Optional.of(request)); responseHistoryDatastore.addResponse(request.getLoadBalancerService().getServiceId(), request.getLoadBalancerRequestId(), response); } public void deleteRequest(String requestId) { requestDatastore.deleteRequest(requestId); } public synchronized void commitRequest(BaragonRequest request) throws Exception { RequestAction action = request.getAction().or(RequestAction.UPDATE); Optional<BaragonService> maybeOriginalService = getOriginalService(request); switch(action) { case UPDATE: case REVERT: updateStateDatastore(request); clearChangedBasePaths(request, maybeOriginalService); clearBasePathsFromUnusedLbs(request, maybeOriginalService); removeOldService(request, maybeOriginalService); clearBasePathsWithNoUpstreams(request); break; case DELETE: clearChangedBasePaths(request, maybeOriginalService); clearBasePathsFromUnusedLbs(request, maybeOriginalService); deleteRemovedServices(request); clearBasePathsWithNoUpstreams(request); break; default: LOG.debug(String.format("No updates to commit for request action %s", action)); break; } updateLastRequestForGroups(request); } private void updateLastRequestForGroups(BaragonRequest request) { for (String loadBalancerGroup : request.getLoadBalancerService().getLoadBalancerGroups()) { loadBalancerDatastore.setLastRequestId(loadBalancerGroup, request.getLoadBalancerRequestId()); } } private Optional<BaragonService> getOriginalService(BaragonRequest request) { Optional<BaragonService> maybeOriginalService = Optional.absent(); if (request.getReplaceServiceId().isPresent()) { maybeOriginalService = stateDatastore.getService(request.getReplaceServiceId().get()); } if (!maybeOriginalService.isPresent()) { maybeOriginalService = stateDatastore.getService(request.getLoadBalancerService().getServiceId()); } return maybeOriginalService; } private void deleteRemovedServices(BaragonRequest request) { stateDatastore.removeService(request.getLoadBalancerService().getServiceId()); if (request.getReplaceServiceId().isPresent() && stateDatastore.getService(request.getReplaceServiceId().get()).isPresent()) { stateDatastore.removeService(request.getReplaceServiceId().get()); } stateDatastore.incrementStateVersion(); } private void updateStateDatastore(BaragonRequest request) throws Exception { stateDatastore.updateService(request); try { stateDatastore.incrementStateVersion(); } catch (Exception e) { LOG.error("Error updating state datastore", e); } } private void removeOldService(BaragonRequest request, Optional<BaragonService> maybeOriginalService) { if (maybeOriginalService.isPresent() && !maybeOriginalService.get().getServiceId().equals(request.getLoadBalancerService().getServiceId())) { stateDatastore.removeService(maybeOriginalService.get().getServiceId()); } } private void clearBasePathsWithNoUpstreams(BaragonRequest request) { try { if (stateDatastore.getUpstreams(request.getLoadBalancerService().getServiceId()).isEmpty()) { for (String loadBalancerGroup : request.getLoadBalancerService().getLoadBalancerGroups()) { for (String path : request.getLoadBalancerService().getAllPaths()) { if (loadBalancerDatastore.getBasePathServiceId(loadBalancerGroup, path).or("").equals(request.getLoadBalancerService().getServiceId())) { loadBalancerDatastore.clearBasePath(loadBalancerGroup, path); } } } } } catch (Exception e) { LOG.error("Error clearing base path", e); } } private void clearChangedBasePaths(BaragonRequest request, Optional<BaragonService> maybeOriginalService) { if (maybeOriginalService.isPresent()) { try { List<String> newPaths = request.getLoadBalancerService().getAllPaths(); for (String oldPath : maybeOriginalService.get().getAllPaths()) { if (!newPaths.contains(oldPath)) { for (String loadBalancerGroup : maybeOriginalService.get().getLoadBalancerGroups()) { if (loadBalancerDatastore.getBasePathServiceId(loadBalancerGroup, oldPath).or("").equals(maybeOriginalService.get().getServiceId())) { loadBalancerDatastore.clearBasePath(loadBalancerGroup, oldPath); } } } } } catch (Exception e) { LOG.error("Error clearing base path", e); } } } private void clearBasePathsFromUnusedLbs(BaragonRequest request, Optional<BaragonService> maybeOriginalService) { if (maybeOriginalService.isPresent()) { Set<String> removedLbGroups = maybeOriginalService.get().getLoadBalancerGroups(); removedLbGroups.removeAll(request.getLoadBalancerService().getLoadBalancerGroups()); if (!removedLbGroups.isEmpty()) { try { for (String loadBalancerGroup : removedLbGroups) { for (String path : maybeOriginalService.get().getAllPaths()) { if (loadBalancerDatastore.getBasePathServiceId(loadBalancerGroup, path).or("").equals(maybeOriginalService.get().getServiceId())) { loadBalancerDatastore.clearBasePath(loadBalancerGroup, path); } } } } catch (Exception e) { LOG.error("Error clearing base path", e); } } } } }