Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/unreleased/pr-26581.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type = "fixed"
message = "Fix content pack installation failing with \"Missing Stream for widget entity\" when a view references a stream that cannot be resolved. The unresolvable stream reference is now skipped with a warning instead of aborting the whole installation."

issues = ["Graylog2/graylog-plugin-enterprise#12987"]
pulls = ["26581"]
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
import org.graylog2.contentpacks.model.entities.references.ValueReference;
import org.graylog2.plugin.indexer.searches.timeranges.TimeRange;
import org.graylog2.plugin.streams.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
Expand All @@ -54,13 +56,16 @@
import static com.google.common.collect.ImmutableSortedSet.of;
import static java.util.stream.Collectors.toSet;
import static org.graylog2.contentpacks.facades.StreamReferenceFacade.resolveStreamEntityObject;
import static org.graylog2.shared.utilities.StringUtils.f;

@AutoValue
@JsonAutoDetect
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonDeserialize(builder = QueryEntity.Builder.class)
public abstract class QueryEntity implements NativeEntityConverter<Query> {

private static final Logger LOG = LoggerFactory.getLogger(QueryEntity.class);

@JsonProperty
public abstract String id();

Expand Down Expand Up @@ -150,15 +155,27 @@ private Filter shallowMappedFilter(Map<EntityDescriptor, Object> nativeEntities)
.map(filter -> {
if (filter.type().matches(StreamFilter.NAME)) {
final StreamFilter streamFilter = (StreamFilter) filter;
final Stream stream = (Stream) resolveStreamEntityObject(streamFilter.streamId(), nativeEntities);
if (Objects.isNull(stream)) {
throw new ContentPackException("Could not find matching stream id: " +
streamFilter.streamId());
final Object object = resolveStreamEntityObject(streamFilter.streamId(), nativeEntities);
if (object == null) {
// Skip a dangling stream reference instead of aborting the whole content pack
// installation. This mirrors the export side, which also drops unresolvable references.
LOG.warn("Skipping unresolvable stream reference <{}> in query filter for query <{}> during content pack installation",
streamFilter.streamId(), id());
return null;
} else if (object instanceof final Stream stream) {
return streamFilter.toBuilder().streamId(stream.getId()).build();
} else {
// A non-null, non-Stream value indicates a corrupt entity map rather than a
// missing reference, so abort the installation like the other view resolvers.
throw new ContentPackException(
f("Invalid type for stream <%s> in query <%s>: %s",
streamFilter.streamId(), id(), object.getClass()));
}
return streamFilter.toBuilder().streamId(stream.getId()).build();
}
return filter;
}).collect(Collectors.toSet());
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
return optFilter.toGenericBuilder().filters(newFilters).build();
})
.orElse(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import org.graylog2.contentpacks.exceptions.ContentPackException;
import org.graylog2.contentpacks.model.entities.references.ValueReference;
import org.graylog2.plugin.streams.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.util.Collections;
Expand All @@ -40,6 +42,7 @@
import java.util.stream.Collectors;

import static org.graylog2.contentpacks.facades.StreamReferenceFacade.resolveStreamEntityObject;
import static org.graylog2.shared.utilities.StringUtils.f;

@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
Expand All @@ -49,6 +52,8 @@
defaultImpl = SearchTypeEntity.Fallback.class)
@JsonAutoDetect
public interface SearchTypeEntity extends NativeEntityConverter<SearchType> {
Logger LOG = LoggerFactory.getLogger(SearchTypeEntity.class);

String TYPE_FIELD = "type";
String FIELD_SEARCH_FILTERS = "filters";

Expand Down Expand Up @@ -207,17 +212,29 @@ public SearchType toNativeEntity(Map<String, ValueReference> parameters, Map<Ent

default Set<String> mappedStreams(Map<EntityDescriptor, Object> nativeEntities) {
return streams().stream()
.map(id -> resolveStreamEntityObject(id, nativeEntities))
.map(object -> {
if (object == null) {
throw new ContentPackException("Missing Stream for event definition");
} else if (object instanceof Stream) {
Stream stream = (Stream) object;
return stream.getId();
} else {
throw new ContentPackException(
"Invalid type for stream Stream for event definition: " + object.getClass());
}
}).collect(Collectors.toSet());
.map(streamId -> resolveStreamReference(streamId, nativeEntities))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toSet());
}

/**
* Resolves a single stream reference from the search type's {@code streams} set to the native stream id.
* <p>
* A reference that cannot be resolved (e.g. a dangling reference left over in an exported content pack) is
* skipped with a warning rather than aborting the whole content pack installation. This mirrors the export
* side, which also silently drops stream references it cannot map.
*/
default Optional<String> resolveStreamReference(String streamId, Map<EntityDescriptor, Object> nativeEntities) {
final Object object = resolveStreamEntityObject(streamId, nativeEntities);
if (object == null) {
LOG.warn("Skipping unresolvable stream reference <{}> for search type <{}> during content pack installation", streamId, id());
return Optional.empty();
} else if (object instanceof Stream) {
return Optional.of(((Stream) object).getId());
} else {
throw new ContentPackException(
f("Invalid type for stream <%s> in search type <%s>: %s", streamId, id(), object.getClass()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
import org.graylog2.contentpacks.model.entities.references.ValueReference;
import org.graylog2.plugin.indexer.searches.timeranges.TimeRange;
import org.graylog2.plugin.streams.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.util.Collections;
Expand All @@ -64,10 +66,13 @@
import java.util.stream.Collectors;

import static org.graylog2.contentpacks.facades.StreamReferenceFacade.resolveStreamEntityObject;
import static org.graylog2.shared.utilities.StringUtils.f;

@AutoValue
@JsonDeserialize(builder = WidgetEntity.Builder.class)
public abstract class WidgetEntity implements NativeEntityConverter<WidgetDTO> {
private static final Logger LOG = LoggerFactory.getLogger(WidgetEntity.class);

public static final String FIELD_ID = "id";
public static final String FIELD_TYPE = "type";
public static final String FIELD_FILTER = "filter";
Expand Down Expand Up @@ -181,17 +186,10 @@ public WidgetDTO toNativeEntity(Map<String, ValueReference> parameters, Map<Enti
.filters(filters().stream().map(filter -> filter.toNativeEntity(parameters, nativeEntities)).toList())
.id(this.id())
.streams(this.streams().stream()
.map(id -> resolveStreamEntityObject(id, nativeEntities))
.map(object -> {
if (object == null) {
throw new ContentPackException("Missing Stream for widget entity");
} else if (object instanceof final Stream stream) {
return stream.getId();
} else {
throw new ContentPackException(
"Invalid type for stream Stream for event definition: " + object.getClass());
}
}).collect(Collectors.toSet()))
.map(streamId -> resolveStreamReference(streamId, nativeEntities))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toSet()))
.streamCategories(this.streamCategories())
.type(this.type());
if (this.query().isPresent()) {
Expand All @@ -203,6 +201,27 @@ public WidgetDTO toNativeEntity(Map<String, ValueReference> parameters, Map<Enti
return widgetBuilder.build();
}

/**
* Resolves a single stream reference from the widget's {@code streams} set to the native stream id.
* <p>
* A reference that cannot be resolved (e.g. a dangling reference left over in an exported content pack) is
* skipped with a warning rather than aborting the whole content pack installation. This mirrors the export
* side ({@link org.graylog.plugins.views.search.views.WidgetDTO#toContentPackEntity}), which also silently
* drops stream references it cannot map.
*/
private Optional<String> resolveStreamReference(String streamId, Map<EntityDescriptor, Object> nativeEntities) {
final Object object = resolveStreamEntityObject(streamId, nativeEntities);
if (object == null) {
LOG.warn("Skipping unresolvable stream reference <{}> for widget <{}> during content pack installation", streamId, id());
return Optional.empty();
} else if (object instanceof final Stream stream) {
return Optional.of(stream.getId());
} else {
throw new ContentPackException(
f("Invalid type for stream <%s> in widget <%s>: %s", streamId, id(), object.getClass()));
}
}

@Override
public void resolveForInstallation(EntityV1 entity,
Map<String, ValueReference> parameters,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,28 @@
package org.graylog2.contentpacks.model.entities;

import com.google.common.collect.ImmutableList;
import org.graylog.plugins.views.search.Filter;
import org.graylog.plugins.views.search.elasticsearch.ElasticsearchQueryString;
import org.graylog.plugins.views.search.filter.OrFilter;
import org.graylog.plugins.views.search.filter.StreamFilter;
import org.graylog.plugins.views.search.searchfilters.model.DBSearchFilter;
import org.graylog.plugins.views.search.searchfilters.model.InlineQueryStringSearchFilter;
import org.graylog.plugins.views.search.searchfilters.model.ReferencedQueryStringSearchFilter;
import org.graylog.plugins.views.search.searchfilters.model.UsedSearchFilter;
import org.graylog2.contentpacks.exceptions.ContentPackException;
import org.graylog2.contentpacks.model.ModelTypes;
import org.graylog2.plugin.indexer.searches.timeranges.RelativeRange;
import org.graylog2.plugin.streams.Stream;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.Map;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class QueryEntityTest {

Expand Down Expand Up @@ -76,6 +85,62 @@ public void testLoadsSearchFiltersCollectionFromContentPack() {
.isEqualTo(expectedSearchFilters);
}

@Test
public void dropsUnresolvableStreamFilterInsteadOfFailing() {
final QueryEntity query = QueryEntity.Builder
.createWithDefaults()
.id("nvmd")
.timerange(RelativeRange.allTime())
.query(ElasticsearchQueryString.empty())
.filter(OrFilter.or(StreamFilter.ofId("missing-stream-id")))
.build();

assertThat(query.toNativeEntity(Collections.emptyMap(), Collections.emptyMap()).filter().filters())
.noneMatch(f -> f instanceof StreamFilter);
}

@Test
public void keepsResolvableStreamFilterAndDropsUnresolvable() {
final Stream stream = mock(Stream.class);
when(stream.getId()).thenReturn("native-stream-id");
final Map<EntityDescriptor, Object> nativeEntities =
Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), stream);

final QueryEntity query = QueryEntity.Builder
.createWithDefaults()
.id("nvmd")
.timerange(RelativeRange.allTime())
.query(ElasticsearchQueryString.empty())
.filter(OrFilter.or(StreamFilter.ofId("cp-stream-id"), StreamFilter.ofId("missing-stream-id")))
.build();

final Set<Filter> mappedFilters =
query.toNativeEntity(Collections.emptyMap(), nativeEntities).filter().filters();

assertThat(mappedFilters).hasSize(1);
assertThat(((StreamFilter) mappedFilters.iterator().next()).streamId()).isEqualTo("native-stream-id");
}

@Test
public void failsOnWrongTypeStreamReference() {
// A non-null value that is not a Stream indicates a corrupt entity map, which must still abort the install.
final Map<EntityDescriptor, Object> nativeEntities =
Map.of(EntityDescriptor.create("cp-stream-id", ModelTypes.STREAM_REF_V1), "not-a-stream");

final QueryEntity query = QueryEntity.Builder
.createWithDefaults()
.id("nvmd")
.timerange(RelativeRange.allTime())
.query(ElasticsearchQueryString.empty())
.filter(OrFilter.or(StreamFilter.ofId("cp-stream-id")))
.build();

assertThatThrownBy(() -> query.toNativeEntity(Collections.emptyMap(), nativeEntities))
.isInstanceOf(ContentPackException.class)
.hasMessageContaining("cp-stream-id")
.hasMessageContaining("nvmd");
}

private class TestDBSearchFilter implements DBSearchFilter {
String id;

Expand Down
Loading
Loading