org.graylog.plugins.sidecar.rest.resources.SidecarResource Maven / Gradle / Ivy
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* .
*/
package org.graylog.plugins.sidecar.rest.resources;
import com.codahale.metrics.annotation.Timed;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.graylog.plugins.sidecar.audit.SidecarAuditEventTypes;
import org.graylog.plugins.sidecar.filter.ActiveSidecarFilter;
import org.graylog.plugins.sidecar.mapper.SidecarStatusMapper;
import org.graylog.plugins.sidecar.permissions.SidecarRestPermissions;
import org.graylog.plugins.sidecar.rest.models.CollectorAction;
import org.graylog.plugins.sidecar.rest.models.CollectorActions;
import org.graylog.plugins.sidecar.rest.models.NodeConfiguration;
import org.graylog.plugins.sidecar.rest.models.Sidecar;
import org.graylog.plugins.sidecar.rest.models.SidecarRegistrationConfiguration;
import org.graylog.plugins.sidecar.rest.models.SidecarSummary;
import org.graylog.plugins.sidecar.rest.requests.ConfigurationAssignment;
import org.graylog.plugins.sidecar.rest.requests.NodeConfigurationRequest;
import org.graylog.plugins.sidecar.rest.requests.RegistrationRequest;
import org.graylog.plugins.sidecar.rest.responses.RegistrationResponse;
import org.graylog.plugins.sidecar.rest.responses.SidecarListResponse;
import org.graylog.plugins.sidecar.services.ActionService;
import org.graylog.plugins.sidecar.services.EtagService;
import org.graylog.plugins.sidecar.services.SidecarService;
import org.graylog.plugins.sidecar.system.SidecarConfiguration;
import org.graylog2.audit.jersey.AuditEvent;
import org.graylog2.audit.jersey.NoAuditEvent;
import org.graylog2.database.PaginatedList;
import org.graylog2.plugin.cluster.ClusterConfigService;
import org.graylog2.plugin.rest.PluginRestResource;
import org.graylog2.search.SearchQuery;
import org.graylog2.search.SearchQueryField;
import org.graylog2.search.SearchQueryParser;
import org.graylog2.shared.rest.resources.RestResource;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static org.graylog2.shared.rest.documentation.generator.Generator.CLOUD_VISIBLE;
@Api(value = "Sidecar", description = "Manage Sidecar fleet", tags = {CLOUD_VISIBLE})
@Path("/sidecars")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@RequiresAuthentication
public class SidecarResource extends RestResource implements PluginRestResource {
protected static final ImmutableMap SEARCH_FIELD_MAPPING = ImmutableMap.builder()
.put("id", SearchQueryField.create("_id", SearchQueryField.Type.OBJECT_ID))
.put("node_id", SearchQueryField.create(Sidecar.FIELD_NODE_ID))
.put("name", SearchQueryField.create(Sidecar.FIELD_NODE_NAME))
.put("sidecar_version", SearchQueryField.create(Sidecar.FIELD_SIDECAR_VERSION))
.put("last_seen", SearchQueryField.create(Sidecar.FIELD_LAST_SEEN, SearchQueryField.Type.DATE))
.put("operating_system", SearchQueryField.create(Sidecar.FIELD_OPERATING_SYSTEM))
.put("status", SearchQueryField.create(Sidecar.FIELD_STATUS, SearchQueryField.Type.INT))
.build();
private final SidecarService sidecarService;
private final ActionService actionService;
private final EtagService etagService;
private final ActiveSidecarFilter activeSidecarFilter;
private final SearchQueryParser searchQueryParser;
private final SidecarStatusMapper sidecarStatusMapper;
private final SidecarConfiguration sidecarConfiguration;
@Inject
public SidecarResource(SidecarService sidecarService,
ActionService actionService,
ClusterConfigService clusterConfigService,
SidecarStatusMapper sidecarStatusMapper,
EtagService etagService) {
this.sidecarService = sidecarService;
this.sidecarConfiguration = clusterConfigService.getOrDefault(SidecarConfiguration.class, SidecarConfiguration.defaultConfiguration());
this.actionService = actionService;
this.activeSidecarFilter = new ActiveSidecarFilter(sidecarConfiguration.sidecarInactiveThreshold());
this.sidecarStatusMapper = sidecarStatusMapper;
this.etagService = etagService;
this.searchQueryParser = new SearchQueryParser(Sidecar.FIELD_NODE_NAME, SEARCH_FIELD_MAPPING);
}
@GET
@Timed
@Path("/all")
@ApiOperation(value = "Lists all existing Sidecar registrations")
@RequiresPermissions(SidecarRestPermissions.SIDECARS_READ)
public SidecarListResponse all() {
final List sidecars = sidecarService.all();
final List sidecarSummaries = sidecarService.toSummaryList(sidecars, activeSidecarFilter);
return SidecarListResponse.create("",
PaginatedList.PaginationInfo.create(sidecarSummaries.size(),
sidecarSummaries.size(),
1,
sidecarSummaries.size()),
sidecarSummaries.size(),
false,
null,
null,
sidecarSummaries);
}
@GET
@Timed
@ApiOperation(value = "Lists existing Sidecar registrations using pagination")
@RequiresPermissions(SidecarRestPermissions.SIDECARS_READ)
public SidecarListResponse sidecars(@ApiParam(name = "page") @QueryParam("page") @DefaultValue("1") int page,
@ApiParam(name = "per_page") @QueryParam("per_page") @DefaultValue("50") int perPage,
@ApiParam(name = "query") @QueryParam("query") @DefaultValue("") String query,
@ApiParam(name = "sort",
value = "The field to sort the result on",
required = true,
allowableValues = "title,description,name,id")
@DefaultValue(Sidecar.FIELD_NODE_NAME) @QueryParam("sort") String sort,
@ApiParam(name = "order", value = "The sort direction", allowableValues = "asc, desc")
@DefaultValue("asc") @QueryParam("order") String order,
@ApiParam(name = "only_active") @QueryParam("only_active") @DefaultValue("false") boolean onlyActive) {
final String mappedQuery = sidecarStatusMapper.replaceStringStatusSearchQuery(query);
SearchQuery searchQuery;
try {
searchQuery = searchQueryParser.parse(mappedQuery);
} catch (IllegalArgumentException e) {
throw new BadRequestException("Invalid argument in search query: " + e.getMessage());
}
final PaginatedList sidecars = onlyActive ?
sidecarService.findPaginated(searchQuery, activeSidecarFilter, page, perPage, sort, order) :
sidecarService.findPaginated(searchQuery, page, perPage, sort, order);
final List collectorSummaries = sidecarService.toSummaryList(sidecars, activeSidecarFilter);
final long total = sidecarService.count();
return SidecarListResponse.create(query, sidecars.pagination(), total, onlyActive, sort, order, collectorSummaries);
}
@GET
@Timed
@Path("/{sidecarId}")
@ApiOperation(value = "Returns at most one Sidecar summary for the specified id")
@ApiResponses(value = {
@ApiResponse(code = 404, message = "No Sidecar with the specified id exists")
})
@RequiresPermissions(SidecarRestPermissions.SIDECARS_READ)
public SidecarSummary get(@ApiParam(name = "sidecarId", required = true)
@PathParam("sidecarId") @NotEmpty String sidecarId) {
final Sidecar sidecar = sidecarService.findByNodeId(sidecarId);
if (sidecar == null) {
throw new NotFoundException("Could not find sidecar <" + sidecarId + ">");
}
return sidecar.toSummary(activeSidecarFilter);
}
@PUT
@Timed
@Path("/{sidecarId}")
@ApiOperation(value = "Create/update a Sidecar registration",
notes = "This is a stateless method which upserts a Sidecar registration")
@ApiResponses(value = {
@ApiResponse(code = 400, message = "The supplied request is not valid.")
})
@RequiresPermissions(SidecarRestPermissions.SIDECARS_UPDATE)
@NoAuditEvent("this is only a ping from Sidecars, and would overflow the audit log")
public Response register(@ApiParam(name = "sidecarId", value = "The id this Sidecar is registering as.", required = true)
@PathParam("sidecarId") @NotEmpty String nodeId,
@ApiParam(name = "JSON body", required = true)
@Valid @NotNull RegistrationRequest request,
@HeaderParam(value = "If-None-Match") String ifNoneMatch,
@HeaderParam(value = "X-Graylog-Sidecar-Version") @NotEmpty String sidecarVersion) throws JsonProcessingException {
Sidecar sidecar;
final Sidecar oldSidecar = sidecarService.findByNodeId(nodeId);
if (oldSidecar != null) {
sidecar = oldSidecar.toBuilder()
.nodeName(request.nodeName())
.nodeDetails(request.nodeDetails())
.sidecarVersion(sidecarVersion)
.lastSeen(DateTime.now(DateTimeZone.UTC))
.build();
} else {
sidecar = sidecarService.fromRequest(nodeId, request, sidecarVersion);
}
// If the sidecar has the recent registration, return with HTTP 304
if (ifNoneMatch != null) {
EntityTag etag = new EntityTag(ifNoneMatch.replaceAll("\"", ""));
if (etagService.registrationIsCached(sidecar.nodeId(), etag.toString())) {
sidecarService.save(sidecar);
return Response.notModified().tag(etag).build();
}
}
final Sidecar updated = sidecarService.updateTaggedConfigurationAssignments(sidecar);
sidecarService.save(updated);
sidecar = updated;
final CollectorActions collectorActions = actionService.findActionBySidecar(nodeId, true);
List collectorAction = null;
if (collectorActions != null) {
collectorAction = collectorActions.action();
}
RegistrationResponse sidecarRegistrationResponse = RegistrationResponse.create(
SidecarRegistrationConfiguration.create(
sidecarConfiguration.sidecarUpdateInterval().toStandardDuration().getStandardSeconds(),
sidecarConfiguration.sidecarSendStatus()),
sidecarConfiguration.sidecarConfigurationOverride(),
collectorAction,
sidecar.assignments());
// add new etag to cache
EntityTag registrationEtag = etagService.buildEntityTagForResponse(sidecarRegistrationResponse);
etagService.addSidecarRegistration(sidecar.nodeId(), registrationEtag.toString());
return Response.accepted(sidecarRegistrationResponse).tag(registrationEtag).build();
}
@PUT
@Timed
@Path("/configurations")
@ApiOperation(value = "Assign configurations to sidecars")
@RequiresPermissions({SidecarRestPermissions.SIDECARS_READ, SidecarRestPermissions.SIDECARS_UPDATE})
@AuditEvent(type = SidecarAuditEventTypes.SIDECAR_UPDATE)
public Response assignConfiguration(@ApiParam(name = "JSON body", required = true)
@Valid @NotNull NodeConfigurationRequest request) throws NotFoundException {
List nodeIdList = request.nodes().stream()
.filter(distinctByKey(NodeConfiguration::nodeId))
.map(NodeConfiguration::nodeId)
.collect(Collectors.toList());
for (String nodeId : nodeIdList) {
List nodeRelations = request.nodes().stream()
.filter(a -> a.nodeId().equals(nodeId))
.flatMap(a -> a.assignments().stream())
.collect(Collectors.toList());
try {
Sidecar sidecar = sidecarService.applyManualAssignments(nodeId, nodeRelations);
sidecarService.save(sidecar);
etagService.invalidateRegistration(sidecar.nodeId());
} catch (org.graylog2.database.NotFoundException e) {
throw new NotFoundException(e.getMessage());
}
}
return Response.accepted().build();
}
private static Predicate distinctByKey(Function super T, Object> keyExtractor) {
Map
© 2015 - 2024 Weber Informatics LLC | Privacy Policy