From 639e87010cea056caafec89e46c1eef475ab7371 Mon Sep 17 00:00:00 2001 From: Marco Pfatschbacher Date: Thu, 8 Sep 2022 13:45:35 +0200 Subject: [PATCH 01/82] feature branch for multiple configs per collector and tags From 450343eb598e167334a5589054f0f428a78a2363 Mon Sep 17 00:00:00 2001 From: Othello Maurer Date: Thu, 8 Sep 2022 14:14:48 +0200 Subject: [PATCH 02/82] Add "tags" field to DTOs (#13367) --- .../graylog/plugins/sidecar/rest/models/NodeDetails.java | 9 +++++++-- .../graylog/plugins/sidecar/services/SidecarService.java | 9 ++++++--- .../sidecar/collectors/SidecarServiceImplTest.java | 1 + .../sidecar/collectors/rest/SidecarResourceTest.java | 4 ++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java index 202825c47d71..096a1cd1bbd0 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java @@ -50,12 +50,17 @@ public abstract class NodeDetails { @Nullable public abstract CollectorStatusList statusList(); + @JsonProperty("tags") + @Nullable + public abstract List tags(); + @JsonCreator public static NodeDetails create(@JsonProperty("operating_system") String operatingSystem, @JsonProperty("ip") @Nullable String ip, @JsonProperty("metrics") @Nullable NodeMetrics metrics, @JsonProperty("log_file_list") @Nullable List logFileList, - @JsonProperty("status") @Nullable CollectorStatusList statusList) { - return new AutoValue_NodeDetails(operatingSystem, ip, metrics, logFileList, statusList); + @JsonProperty("status") @Nullable CollectorStatusList statusList, + @JsonProperty("tags") @Nullable List tags) { + return new AutoValue_NodeDetails(operatingSystem, ip, metrics, logFileList, statusList, tags); } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java index 5646225fdbd3..080e5b68238a 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java @@ -89,8 +89,9 @@ public Sidecar save(Sidecar sidecar) { } else { throw new IllegalArgumentException("Specified object failed validation: " + violations); } - } else + } else { throw new IllegalArgumentException("Specified object is not of correct implementation type (" + sidecar.getClass() + ")!"); + } } public List all() { @@ -168,7 +169,8 @@ public int markExpired(Period period, String message) { nodeDetails.ip(), nodeDetails.metrics(), nodeDetails.logFileList(), - statusListToSave); + statusListToSave, + nodeDetails.tags()); Sidecar toSave = collector.toBuilder() .nodeDetails(nodeDetailsToSave) @@ -194,7 +196,8 @@ public Sidecar fromRequest(String nodeId, RegistrationRequest request, String co request.nodeDetails().ip(), request.nodeDetails().metrics(), request.nodeDetails().logFileList(), - request.nodeDetails().statusList()), + request.nodeDetails().statusList(), + request.nodeDetails().tags()), collectorVersion); } diff --git a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java index 26f54bca730a..d4357f2dd674 100644 --- a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java +++ b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java @@ -95,6 +95,7 @@ public void testSaveFirstRecord() throws Exception { null, null, null, + null, null), version ); diff --git a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java index 89d0ea731b45..0e8f8076b954 100644 --- a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java +++ b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java @@ -125,6 +125,7 @@ public void testRegister() throws Exception { null, null, null, + null, null ) ); @@ -144,6 +145,7 @@ public void testRegisterInvalidCollectorId() throws Exception { null, null, null, + null, null ) ); @@ -164,6 +166,7 @@ public void testRegisterInvalidNodeId() throws Exception { null, null, null, + null, null ) ); @@ -198,6 +201,7 @@ public void testRegisterMissingOperatingSystem() throws Exception { null, null, null, + null, null ) ); From 57e75d0a5b352917d17669fdc97d3b4073660d41 Mon Sep 17 00:00:00 2001 From: Marco Pfatschbacher Date: Wed, 14 Sep 2022 16:23:40 +0200 Subject: [PATCH 03/82] Add spooldir variable to sidecars and their default collector configs (#13349) * Add support for multiple configurations per collector This will need a new sidcar release. Old sidecars are still supported with just a single configuration. * Use Java 9 Lists * Add support for ${sidecar.spoolDir} variable * Fix tests * Revert "Add support for multiple configurations per collector" This reverts commit 0a17743a7775ca25e65d978e06343847042bbafb. * Use ${sidecar.spoolDir} for fresh collector templates And add explanation to the UI * make eslint happy * convert tags to Set and fix Autovalue * Support configuration_id in CollectorStatus Co-authored-by: Othello Maurer --- .../V20180212165000_AddDefaultCollectors.java | 28 +++++++++---------- .../sidecar/rest/models/CollectorStatus.java | 13 +++++++-- .../sidecar/rest/models/NodeDetails.java | 12 ++++++-- .../services/ConfigurationService.java | 5 ++++ .../sidecar/services/SidecarService.java | 8 ++++-- .../collectors/SidecarServiceImplTest.java | 1 + .../collectors/rest/SidecarResourceTest.java | 4 +++ .../configuration-forms/TemplatesHelper.jsx | 14 ++++++---- 8 files changed, 57 insertions(+), 28 deletions(-) diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/migrations/V20180212165000_AddDefaultCollectors.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/migrations/V20180212165000_AddDefaultCollectors.java index dae8ef1dbdff..98d2b65bd702 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/migrations/V20180212165000_AddDefaultCollectors.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/migrations/V20180212165000_AddDefaultCollectors.java @@ -79,8 +79,8 @@ public void upgrade() { "output.logstash:\n" + " hosts: [\"192.168.1.1:5044\"]\n" + "path:\n" + - " data: /var/lib/graylog-sidecar/collectors/filebeat/data\n" + - " logs: /var/lib/graylog-sidecar/collectors/filebeat/log" + " data: ${sidecar.spoolDir}/data\n" + + " logs: ${sidecar.spoolDir}/log" ); ensureCollector( "filebeat", @@ -98,8 +98,8 @@ public void upgrade() { "output.logstash:\n" + " hosts: [\"192.168.1.1:5044\"]\n" + "path:\n" + - " data: /var/lib/graylog-sidecar/collectors/filebeat/data\n" + - " logs: /var/lib/graylog-sidecar/collectors/filebeat/log" + " data: ${sidecar.spoolDir}/data\n" + + " logs: ${sidecar.spoolDir}/log" ); ensureCollector( "filebeat", @@ -117,8 +117,8 @@ public void upgrade() { "output.logstash:\n" + " hosts: [\"192.168.1.1:5044\"]\n" + "path:\n" + - " data: /var/lib/graylog-sidecar/collectors/filebeat/data\n" + - " logs: /var/lib/graylog-sidecar/collectors/filebeat/log" + " data: ${sidecar.spoolDir}/data\n" + + " logs: ${sidecar.spoolDir}/log" ); ensureCollector( "winlogbeat", @@ -131,8 +131,8 @@ public void upgrade() { "output.logstash:\n" + " hosts: [\"192.168.1.1:5044\"]\n" + "path:\n" + - " data: C:\\Program Files\\Graylog\\sidecar\\cache\\winlogbeat\\data\n" + - " logs: C:\\Program Files\\Graylog\\sidecar\\logs\n" + + " data: ${sidecar.spoolDir}\\data\n" + + " logs: ${sidecar.spoolDir}\\logs\n" + "tags:\n" + " - windows\n" + "winlogbeat:\n" + @@ -164,9 +164,9 @@ public void upgrade() { "Group nxlog\n" + "\n" + "Moduledir /usr/lib/nxlog/modules\n" + - "CacheDir /var/spool/nxlog/data\n" + - "PidFile /var/run/nxlog/nxlog.pid\n" + - "LogFile /var/log/nxlog/nxlog.log\n" + + "CacheDir ${sidecar.spoolDir}/data\n" + + "PidFile ${sidecar.spoolDir}/nxlog.pid\n" + + "LogFile ${sidecar.spoolDir}/nxlog.log\n" + "LogLevel INFO\n" + "\n" + "\n" + @@ -217,7 +217,7 @@ public void upgrade() { "C:\\Program Files (x86)\\nxlog\\nxlog.exe", "-c \"%s\"", "-v -f -c \"%s\"", - "define ROOT C:\\Program Files (x86)\\nxlog\n" + + "define ROOT ${sidecar.spoolDir}\\nxlog\n" + "\n" + "Moduledir %ROOT%\\modules\n" + "CacheDir %ROOT%\\data\n" + @@ -302,8 +302,8 @@ public void upgrade() { "output.logstash:\n" + " hosts: [\"192.168.1.1:5044\"]\n" + "path:\n" + - " data: C:\\Program Files\\Graylog\\sidecar\\cache\\filebeat\\data\n" + - " logs: C:\\Program Files\\Graylog\\sidecar\\logs\n" + + " data: ${sidecar.spoolDir}\\data\n" + + " logs: ${sidecar.spoolDir}\\logs\n" + "tags:\n" + " - windows\n" + "filebeat.inputs:\n" + diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/CollectorStatus.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/CollectorStatus.java index 650b5cfa0ca0..551cce983654 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/CollectorStatus.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/CollectorStatus.java @@ -21,6 +21,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.google.auto.value.AutoValue; +import javax.annotation.Nullable; + @AutoValue @JsonAutoDetect public abstract class CollectorStatus { @@ -36,11 +38,16 @@ public abstract class CollectorStatus { @JsonProperty("verbose_message") public abstract String verboseMessage(); + @JsonProperty("configuration_id") + @Nullable + public abstract String configurationId(); + @JsonCreator public static CollectorStatus create(@JsonProperty("collector_id") String collectorId, @JsonProperty("status") int status, @JsonProperty("message") String message, - @JsonProperty("verbose_message") String verboseMessage) { - return new AutoValue_CollectorStatus(collectorId, status, message, verboseMessage); + @JsonProperty("verbose_message") String verboseMessage, + @JsonProperty("configuration_id") @Nullable String configurationId) { + return new AutoValue_CollectorStatus(collectorId, status, message, verboseMessage, configurationId); } -} \ No newline at end of file +} diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java index 096a1cd1bbd0..8439b69f5cc9 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java @@ -25,6 +25,7 @@ import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.util.List; +import java.util.Set; @AutoValue @JsonAutoDetect @@ -52,7 +53,11 @@ public abstract class NodeDetails { @JsonProperty("tags") @Nullable - public abstract List tags(); + public abstract Set tags(); + + @JsonProperty("collector_configuration_directory") + @Nullable + public abstract String collectorConfigurationDirectory(); @JsonCreator public static NodeDetails create(@JsonProperty("operating_system") String operatingSystem, @@ -60,7 +65,8 @@ public static NodeDetails create(@JsonProperty("operating_system") String operat @JsonProperty("metrics") @Nullable NodeMetrics metrics, @JsonProperty("log_file_list") @Nullable List logFileList, @JsonProperty("status") @Nullable CollectorStatusList statusList, - @JsonProperty("tags") @Nullable List tags) { - return new AutoValue_NodeDetails(operatingSystem, ip, metrics, logFileList, statusList, tags); + @JsonProperty("tags") @Nullable Set tags, + @JsonProperty("collector_configuration_directory") @Nullable String configDir) { + return new AutoValue_NodeDetails(operatingSystem, ip, metrics, logFileList, statusList, tags, configDir); } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ConfigurationService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ConfigurationService.java index 321eacc10702..cccf81573014 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ConfigurationService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ConfigurationService.java @@ -161,6 +161,10 @@ public Configuration renderConfigurationForCollector(Sidecar sidecar, Configurat context.put("nodeName", sidecar.nodeName()); context.put("sidecarVersion", sidecar.sidecarVersion()); context.put("operatingSystem", sidecar.nodeDetails().operatingSystem()); + if (sidecar.nodeDetails().collectorConfigurationDirectory() != null) { + String pathDelim = sidecar.nodeDetails().operatingSystem().equalsIgnoreCase("windows") ? "\\" : "/"; + context.put("spoolDir", sidecar.nodeDetails().collectorConfigurationDirectory() + pathDelim + configuration.id()); + } return Configuration.create( configuration.id(), @@ -177,6 +181,7 @@ public String renderPreview(String template) throws RenderTemplateException { context.put("nodeName", ""); context.put("sidecarVersion", ""); context.put("operatingSystem", ""); + context.put("spoolDir", ""); String previewName = UUID.randomUUID().toString(); stringTemplateLoader.putTemplate(previewName, template); diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java index 080e5b68238a..865ceadce079 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java @@ -157,7 +157,7 @@ public int markExpired(Period period, String message) { collectorStatuses.add(CollectorStatus.create( collectorStatus.collectorId(), Sidecar.Status.UNKNOWN.getStatusCode(), - message, "")); + message, "", collectorStatus.configurationId())); } CollectorStatusList statusListToSave = CollectorStatusList.create( Sidecar.Status.UNKNOWN.getStatusCode(), @@ -170,7 +170,8 @@ public int markExpired(Period period, String message) { nodeDetails.metrics(), nodeDetails.logFileList(), statusListToSave, - nodeDetails.tags()); + nodeDetails.tags(), + nodeDetails.collectorConfigurationDirectory()); Sidecar toSave = collector.toBuilder() .nodeDetails(nodeDetailsToSave) @@ -197,7 +198,8 @@ public Sidecar fromRequest(String nodeId, RegistrationRequest request, String co request.nodeDetails().metrics(), request.nodeDetails().logFileList(), request.nodeDetails().statusList(), - request.nodeDetails().tags()), + request.nodeDetails().tags(), + request.nodeDetails().collectorConfigurationDirectory()), collectorVersion); } diff --git a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java index d4357f2dd674..592a6c2581fe 100644 --- a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java +++ b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java @@ -96,6 +96,7 @@ public void testSaveFirstRecord() throws Exception { null, null, null, + null, null), version ); diff --git a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java index 0e8f8076b954..a832eb4b7b6f 100644 --- a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java +++ b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java @@ -126,6 +126,7 @@ public void testRegister() throws Exception { null, null, null, + null, null ) ); @@ -146,6 +147,7 @@ public void testRegisterInvalidCollectorId() throws Exception { null, null, null, + null, null ) ); @@ -167,6 +169,7 @@ public void testRegisterInvalidNodeId() throws Exception { null, null, null, + null, null ) ); @@ -202,6 +205,7 @@ public void testRegisterMissingOperatingSystem() throws Exception { null, null, null, + null, null ) ); diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx index cbba698bb92b..9063e7c07cfe 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx @@ -19,7 +19,7 @@ import React from 'react'; import { Table } from 'components/bootstrap'; class TemplatesHelper extends React.Component { - _buildVariableName = (name) => { + static _buildVariableName = (name) => { return `\${sidecar.${name}}`; }; @@ -35,21 +35,25 @@ class TemplatesHelper extends React.Component { - {this._buildVariableName('operatingSystem')} + {TemplatesHelper._buildVariableName('operatingSystem')} Name of the operating system the sidecar is running on, e.g. "Linux", "Windows" - {this._buildVariableName('nodeName')} + {TemplatesHelper._buildVariableName('nodeName')} The name of the sidecar, defaults to hostname if not set. - {this._buildVariableName('nodeId')} + {TemplatesHelper._buildVariableName('nodeId')} UUID of the sidecar. - {this._buildVariableName('sidecarVersion')} + {TemplatesHelper._buildVariableName('sidecarVersion')} Version string of the running sidecar. + + {TemplatesHelper._buildVariableName('spoolDir')} + A directory that is unique per configuration and can be used to store collector data. + From 920fec3f81300182440dabf1a34071928f95ff19 Mon Sep 17 00:00:00 2001 From: Marco Pfatschbacher Date: Mon, 19 Sep 2022 16:03:03 +0200 Subject: [PATCH 04/82] Sidecar tagged assignments (#13409) * add assignedFromTags property to assignment * Don't fail for unknown properties on NodeDetails This makes it easier to extend the sidecar in the future * Create sidecar config assignments based on tags This is a first naive approach. Every registration request will rebuild the assignments and this is probably too expensive. * fix test * Fix more Tests * Fine grained EtagService Use three different caches for configs, collectors and assignments. * Add Etag caching for UpdateRegistration results The response to the registration PUT request contains configuration assignments, which can be cached as well. This requires us to invalidate the sidecar etag cache in more situations: - Configration assignments - User triggered collector actions (stop, start, restart) * Cleanup old assignment caching commit * optimize * fix after merge * replace deprecated notempty annotation * cache assignments per sidecar id using just the md5 of an assignment result might leed to sidecars missing out on updates. An assignment is always meant for a single sidecar, caching the entire assignment over all sidecars is wrong. A per sidecar cache entry allows more fine grained cache invalidations. Bump the cacheMaxSize to 5000 entries, which is a more meaningful number, considered that this might have to hold all sidecars. * Refactor sidecar register() call. Only update the tagged assignments when we miss the cache. * change cache naming * more refactoring * improve cache invalidation * use fancy java Co-authored-by: Othello Maurer * Add more cases for tag invalidation and fix tests * add tags to Config summary * Generate EntityTags with a better hash algorithm As @thll noticed, Object.hashCode() is prone to having collisions * Handle manual assignments via API * rename * Make tags available as config variables. This allows configs to be written with conditionals. E.g.: ``` <#if sidecar.tags.apache??> - /var/log/apache/*.log ``` * dont lose tags on UI configuration updates Co-authored-by: Othello Maurer --- .../common/SidecarPluginConfiguration.java | 2 +- .../sidecar/rest/models/Configuration.java | 23 +++- .../rest/models/ConfigurationSummary.java | 13 +- .../sidecar/rest/models/NodeDetails.java | 5 +- .../plugins/sidecar/rest/models/Sidecar.java | 4 +- .../requests/ConfigurationAssignment.java | 11 +- .../resources/AdministrationResource.java | 7 +- .../rest/resources/CollectorResource.java | 27 ++-- .../rest/resources/ConfigurationResource.java | 37 +++--- .../ConfigurationVariableResource.java | 4 +- .../rest/resources/SidecarResource.java | 59 ++++++--- .../services/ConfigurationService.java | 29 ++++- .../services/EtagCacheInvalidation.java | 11 +- .../plugins/sidecar/services/EtagService.java | 118 +++++++++++++++--- .../sidecar/services/SidecarService.java | 74 ++++++++--- .../SidecarCollectorConfigurationFacade.java | 5 +- .../collectors/ConfigurationServiceTest.java | 4 +- .../collectors/SidecarServiceImplTest.java | 5 + .../collectors/rest/SidecarResourceTest.java | 43 ++++--- .../configuration-forms/ConfigurationForm.jsx | 1 + 20 files changed, 343 insertions(+), 139 deletions(-) diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/common/SidecarPluginConfiguration.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/common/SidecarPluginConfiguration.java index a3c54a8aa141..4f880c84a172 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/common/SidecarPluginConfiguration.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/common/SidecarPluginConfiguration.java @@ -33,7 +33,7 @@ public class SidecarPluginConfiguration implements PluginConfigBean { private Duration cacheTime = Duration.hours(1L); @Parameter(value = PREFIX + "cache_max_size", validator = PositiveIntegerValidator.class) - private int cacheMaxSize = 100; + private int cacheMaxSize = 5000; public Duration getCacheTime() { return cacheTime; diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/Configuration.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/Configuration.java index ff7238efee70..78f0434a971e 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/Configuration.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/Configuration.java @@ -25,6 +25,7 @@ import org.mongojack.ObjectId; import javax.annotation.Nullable; +import java.util.Set; @AutoValue @WithBeanGetter @@ -35,6 +36,7 @@ public abstract class Configuration { public static final String FIELD_NAME = "name"; public static final String FIELD_COLOR = "color"; public static final String FIELD_TEMPLATE = "template"; + public static final String FIELD_TAGS = "tags"; @Id @ObjectId @@ -54,30 +56,37 @@ public abstract class Configuration { @JsonProperty(FIELD_TEMPLATE) public abstract String template(); + @JsonProperty(FIELD_TAGS) + public abstract Set tags(); + @JsonCreator public static Configuration create(@JsonProperty(FIELD_ID) String id, @JsonProperty(FIELD_COLLECTOR_ID) String collectorId, @JsonProperty(FIELD_NAME) String name, @JsonProperty(FIELD_COLOR) String color, - @JsonProperty(FIELD_TEMPLATE) String template) { + @JsonProperty(FIELD_TEMPLATE) String template, + @JsonProperty(FIELD_TAGS) @Nullable Set tags) { return builder() .id(id) .collectorId(collectorId) .name(name) .color(color) .template(template) + .tags(tags == null ? Set.of() : tags) .build(); } - public static Configuration create(String collectorId, - String name, - String color, - String template) { + public static Configuration createWithoutId(String collectorId, + String name, + String color, + String template, + Set tags) { return create(new org.bson.types.ObjectId().toHexString(), collectorId, name, color, - template); + template, + tags); } public static Builder builder() { @@ -98,6 +107,8 @@ public abstract static class Builder { public abstract Builder template(String template); + public abstract Builder tags(Set tags); + public abstract Configuration build(); } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/ConfigurationSummary.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/ConfigurationSummary.java index 4a1f407fb0e9..7d88d65e5baf 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/ConfigurationSummary.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/ConfigurationSummary.java @@ -22,6 +22,8 @@ import org.mongojack.Id; import org.mongojack.ObjectId; +import java.util.Set; + @AutoValue public abstract class ConfigurationSummary { @JsonProperty("id") @@ -38,12 +40,16 @@ public abstract class ConfigurationSummary { @JsonProperty("color") public abstract String color(); + @JsonProperty("tags") + public abstract Set tags(); + @JsonCreator public static ConfigurationSummary create(@JsonProperty("id") @Id @ObjectId String id, @JsonProperty("name") String name, @JsonProperty("collector_id") String collectorId, - @JsonProperty("color") String color) { - return new AutoValue_ConfigurationSummary(id, name, collectorId, color); + @JsonProperty("color") String color, + @JsonProperty("tags") Set tags) { + return new AutoValue_ConfigurationSummary(id, name, collectorId, color, tags); } public static ConfigurationSummary create(Configuration configuration) { @@ -51,7 +57,8 @@ public static ConfigurationSummary create(Configuration configuration) { configuration.id(), configuration.name(), configuration.collectorId(), - configuration.color()); + configuration.color(), + configuration.tags()); } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java index 8439b69f5cc9..9418b7faeab4 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.auto.value.AutoValue; @@ -29,6 +30,7 @@ @AutoValue @JsonAutoDetect +@JsonIgnoreProperties(ignoreUnknown = true) public abstract class NodeDetails { @JsonProperty("operating_system") @NotNull @@ -52,7 +54,6 @@ public abstract class NodeDetails { public abstract CollectorStatusList statusList(); @JsonProperty("tags") - @Nullable public abstract Set tags(); @JsonProperty("collector_configuration_directory") @@ -67,6 +68,6 @@ public static NodeDetails create(@JsonProperty("operating_system") String operat @JsonProperty("status") @Nullable CollectorStatusList statusList, @JsonProperty("tags") @Nullable Set tags, @JsonProperty("collector_configuration_directory") @Nullable String configDir) { - return new AutoValue_NodeDetails(operatingSystem, ip, metrics, logFileList, statusList, tags, configDir); + return new AutoValue_NodeDetails(operatingSystem, ip, metrics, logFileList, statusList, tags == null ? Set.of() : tags, configDir); } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/Sidecar.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/Sidecar.java index 76777511cb5d..bfb4909d597c 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/Sidecar.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/Sidecar.java @@ -92,7 +92,6 @@ public static Status fromString(String statusString) { public abstract NodeDetails nodeDetails(); @JsonProperty - @Nullable public abstract List assignments(); @JsonProperty @@ -133,7 +132,7 @@ public static Sidecar create(@JsonProperty(FIELD_ID) @Id @ObjectId String id, .nodeId(nodeId) .nodeName(nodeName) .nodeDetails(nodeDetails) - .assignments(assignments) + .assignments(assignments == null ? List.of() : assignments) .sidecarVersion(sidecarVersion) .lastSeen(lastSeen) .build(); @@ -151,6 +150,7 @@ public static Sidecar create(@JsonProperty(FIELD_NODE_ID) String nodeId, .nodeDetails(nodeDetails) .sidecarVersion(sidecarVersion) .lastSeen(DateTime.now(DateTimeZone.UTC)) + .assignments(List.of()) .build(); } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/requests/ConfigurationAssignment.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/requests/ConfigurationAssignment.java index 1aa264919aaa..ee14796e310e 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/requests/ConfigurationAssignment.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/requests/ConfigurationAssignment.java @@ -21,6 +21,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.google.auto.value.AutoValue; +import javax.annotation.Nullable; +import java.util.Set; + @AutoValue @JsonAutoDetect public abstract class ConfigurationAssignment { @@ -30,9 +33,13 @@ public abstract class ConfigurationAssignment { @JsonProperty public abstract String configurationId(); + @JsonProperty + public abstract Set assignedFromTags(); + @JsonCreator public static ConfigurationAssignment create(@JsonProperty("collector_id") String collectorId, - @JsonProperty("configuration_id") String configurationId) { - return new AutoValue_ConfigurationAssignment(collectorId, configurationId); + @JsonProperty("configuration_id") String configurationId, + @JsonProperty("assigned_from_tags") @Nullable Set assignedFromTags) { + return new AutoValue_ConfigurationAssignment(collectorId, configurationId, assignedFromTags == null ? Set.of() : assignedFromTags); } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/AdministrationResource.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/AdministrationResource.java index 32b30f0d7d25..951aa770faa7 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/AdministrationResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/AdministrationResource.java @@ -44,6 +44,7 @@ import org.graylog.plugins.sidecar.services.ActionService; import org.graylog.plugins.sidecar.services.CollectorService; import org.graylog.plugins.sidecar.services.ConfigurationService; +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; @@ -90,6 +91,7 @@ public class AdministrationResource extends RestResource implements PluginRestRe private final AdministrationFiltersFactory administrationFiltersFactory; private final ActiveSidecarFilter activeSidecarFilter; private final SidecarStatusMapper sidecarStatusMapper; + private final EtagService etagService; @Inject public AdministrationResource(SidecarService sidecarService, @@ -98,7 +100,8 @@ public AdministrationResource(SidecarService sidecarService, ActionService actionService, AdministrationFiltersFactory administrationFiltersFactory, ClusterConfigService clusterConfigService, - SidecarStatusMapper sidecarStatusMapper) { + SidecarStatusMapper sidecarStatusMapper, + EtagService etagService) { final SidecarConfiguration sidecarConfiguration = clusterConfigService.getOrDefault(SidecarConfiguration.class, SidecarConfiguration.defaultConfiguration()); this.sidecarService = sidecarService; this.configurationService = configurationService; @@ -106,6 +109,7 @@ public AdministrationResource(SidecarService sidecarService, this.actionService = actionService; this.administrationFiltersFactory = administrationFiltersFactory; this.sidecarStatusMapper = sidecarStatusMapper; + this.etagService = etagService; this.activeSidecarFilter = new ActiveSidecarFilter(sidecarConfiguration.sidecarInactiveThreshold()); this.searchQueryParser = new SearchQueryParser(Sidecar.FIELD_NODE_NAME, SidecarResource.SEARCH_FIELD_MAPPING); } @@ -166,6 +170,7 @@ public Response setAction(@ApiParam(name = "JSON body", required = true) .collect(Collectors.toList()); final CollectorActions collectorActions = actionService.fromRequest(bulkActionRequest.sidecarId(), actions); actionService.saveAction(collectorActions); + etagService.invalidateRegistration(bulkActionRequest.sidecarId()); } return Response.accepted().build(); diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/CollectorResource.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/CollectorResource.java index e35ffe32904f..517e4ed25076 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/CollectorResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/CollectorResource.java @@ -17,9 +17,9 @@ package org.graylog.plugins.sidecar.rest.resources; import com.codahale.metrics.annotation.Timed; +import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.google.common.hash.Hashing; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -136,15 +136,15 @@ public Collector getCollector(@ApiParam(name = "id", required = true) @RequiresPermissions(SidecarRestPermissions.COLLECTORS_READ) @Produces(MediaType.APPLICATION_JSON) @ApiOperation(value = "List all collectors") - public Response listCollectors(@Context HttpHeaders httpHeaders) { + public Response listCollectors(@Context HttpHeaders httpHeaders) throws JsonProcessingException { String ifNoneMatch = httpHeaders.getHeaderString("If-None-Match"); Boolean etagCached = false; Response.ResponseBuilder builder = Response.noContent(); - // check if client is up to date with a known valid etag + // check if client is up-to-date with a known valid etag if (ifNoneMatch != null) { EntityTag etag = new EntityTag(ifNoneMatch.replaceAll("\"", "")); - if (etagService.isPresent(etag.toString())) { + if (etagService.collectorsAreCached(etag.toString())) { etagCached = true; builder = Response.notModified(); builder.tag(etag); @@ -157,12 +157,10 @@ public Response listCollectors(@Context HttpHeaders httpHeaders) { CollectorListResponse collectorListResponse = CollectorListResponse.create(result.size(), result); // add new etag to cache - String etagString = collectorsToEtag(collectorListResponse); - - EntityTag collectorsEtag = new EntityTag(etagString); + EntityTag collectorsEtag = etagService.buildEntityTagForResponse(collectorListResponse); builder = Response.ok(collectorListResponse); builder.tag(collectorsEtag); - etagService.put(collectorsEtag.toString()); + etagService.registerCollector(collectorsEtag.toString()); } // set cache control @@ -211,7 +209,7 @@ public Response createCollector(@ApiParam(name = "JSON body", required = true) if (validationResult.failed()) { return Response.status(Response.Status.BAD_REQUEST).entity(validationResult).build(); } - etagService.invalidateAll(); + etagService.invalidateAllCollectors(); return Response.ok().entity(collectorService.save(collector)).build(); } @@ -230,7 +228,7 @@ public Response updateCollector(@ApiParam(name = "id", required = true) if (validationResult.failed()) { return Response.status(Response.Status.BAD_REQUEST).entity(validationResult).build(); } - etagService.invalidateAll(); + etagService.invalidateAllCollectors(); return Response.ok().entity(collectorService.save(collector)).build(); } @@ -248,8 +246,8 @@ public Response copyCollector(@ApiParam(name = "id", required = true) if (validationResult.failed()) { return Response.status(Response.Status.BAD_REQUEST).entity(validationResult).build(); } - etagService.invalidateAll(); collectorService.save(collector); + etagService.invalidateAllCollectors(); return Response.accepted().build(); } @@ -272,7 +270,7 @@ public Response deleteCollector(@ApiParam(name = "id", required = true) if (deleted == 0) { return Response.notModified().build(); } - etagService.invalidateAll(); + etagService.invalidateAllCollectors(); return Response.accepted().build(); } @@ -351,9 +349,4 @@ private boolean validatePath(String path) { return VALID_PATH_PATTERN.matcher(path).matches(); } - private String collectorsToEtag(CollectorListResponse collectors) { - return Hashing.md5() - .hashInt(collectors.hashCode()) // avoid negative values - .toString(); - } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationResource.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationResource.java index 238f7e2a9bb7..808bfe08e0f0 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationResource.java @@ -17,8 +17,8 @@ package org.graylog.plugins.sidecar.rest.resources; import com.codahale.metrics.annotation.Timed; +import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.collect.ImmutableMap; -import com.google.common.hash.Hashing; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -207,15 +207,15 @@ public Response renderConfiguration(@Context HttpHeaders httpHeaders, @ApiParam(name = "sidecarId", required = true) @PathParam("sidecarId") String sidecarId, @ApiParam(name = "configurationId", required = true) - @PathParam("configurationId") String configurationId) throws RenderTemplateException { + @PathParam("configurationId") String configurationId) throws RenderTemplateException, JsonProcessingException { String ifNoneMatch = httpHeaders.getHeaderString("If-None-Match"); - Boolean etagCached = false; + boolean etagCached = false; Response.ResponseBuilder builder = Response.noContent(); - // check if client is up to date with a known valid etag + // check if client is up-to-date with a known valid etag if (ifNoneMatch != null) { EntityTag etag = new EntityTag(ifNoneMatch.replaceAll("\"", "")); - if (etagService.isPresent(etag.toString())) { + if (etagService.configurationsAreCached(etag.toString())) { etagCached = true; builder = Response.notModified(); builder.tag(etag); @@ -236,13 +236,10 @@ public Response renderConfiguration(@Context HttpHeaders httpHeaders, Configuration collectorConfiguration = this.configurationService.renderConfigurationForCollector(sidecar, configuration); // add new etag to cache - String etagString = configurationToEtag(collectorConfiguration); - - EntityTag collectorConfigurationEtag = new EntityTag(etagString); + EntityTag collectorConfigurationEtag = etagService.buildEntityTagForResponse(collectorConfiguration); builder = Response.ok(collectorConfiguration); builder.tag(collectorConfigurationEtag); - etagService.put(collectorConfigurationEtag.toString()); - + etagService.registerConfiguration(collectorConfigurationEtag.toString()); } // set cache control @@ -283,7 +280,12 @@ public Response createConfiguration(@ApiParam(name = "JSON body", required = tru return Response.status(Response.Status.BAD_REQUEST).entity(validationResult).build(); } - return Response.ok().entity(configurationService.save(configuration)).build(); + final Configuration config = configurationService.save(configuration); + if (!config.tags().isEmpty()) { + etagService.invalidateAllRegistrations(); + } + + return Response.ok().entity(config).build(); } @POST @@ -330,7 +332,10 @@ public Response updateConfiguration(@ApiParam(name = "id", required = true) if (validationResult.failed()) { return Response.status(Response.Status.BAD_REQUEST).entity(validationResult).build(); } - etagService.invalidateAll(); + etagService.invalidateAllConfigurations(); + if (! previousConfiguration.tags().equals(updatedConfiguration.tags())) { + etagService.invalidateAllRegistrations(); + } return Response.ok().entity(configurationService.save(updatedConfiguration)).build(); } @@ -351,7 +356,7 @@ public Response deleteConfiguration(@ApiParam(name = "id", required = true) if (deleted == 0) { return Response.notModified().build(); } - etagService.invalidateAll(); + etagService.invalidateAllConfigurations(); return Response.accepted().build(); } @@ -405,12 +410,6 @@ private boolean isConfigurationAssignedToSidecar(String configurationId, Sidecar return assignments.stream().anyMatch(assignment -> assignment.configurationId().equals(configurationId)); } - private String configurationToEtag(Configuration configuration) { - return Hashing.md5() - .hashInt(configuration.hashCode()) // avoid negative values - .toString(); - } - private Configuration configurationFromRequest(String id, Configuration request) { Configuration configuration; if (id == null) { diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationVariableResource.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationVariableResource.java index 04ea7c9df735..a1019ecbb27e 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationVariableResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationVariableResource.java @@ -132,7 +132,7 @@ public Response updateConfigurationVariable(@ApiParam(name = "id", required = tr configurationService.replaceVariableNames(previousConfigurationVariable.fullName(), request.fullName()); } final ConfigurationVariable updatedConfigurationVariable = persistConfigurationVariable(id, request); - etagService.invalidateAll(); + etagService.invalidateAllConfigurations(); return Response.ok().entity(updatedConfigurationVariable).build(); } @@ -168,7 +168,7 @@ public Response deleteConfigurationVariable(@ApiParam(name = "id", required = tr if (deleted == 0) { return Response.notModified().build(); } - etagService.invalidateAll(); + etagService.invalidateAllConfigurations(); return Response.accepted().build(); } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/SidecarResource.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/SidecarResource.java index 74f5b7b473a0..6cfbbc693991 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/SidecarResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/SidecarResource.java @@ -17,6 +17,7 @@ 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; @@ -42,6 +43,7 @@ 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; @@ -53,12 +55,12 @@ import org.graylog2.search.SearchQueryField; import org.graylog2.search.SearchQueryParser; import org.graylog2.shared.rest.resources.RestResource; -import org.hibernate.validator.constraints.NotEmpty; 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; @@ -71,6 +73,7 @@ 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; @@ -99,6 +102,7 @@ public class SidecarResource extends RestResource implements PluginRestResource private final SidecarService sidecarService; private final ActionService actionService; + private final EtagService etagService; private final ActiveSidecarFilter activeSidecarFilter; private final SearchQueryParser searchQueryParser; private final SidecarStatusMapper sidecarStatusMapper; @@ -108,12 +112,14 @@ public class SidecarResource extends RestResource implements PluginRestResource public SidecarResource(SidecarService sidecarService, ActionService actionService, ClusterConfigService clusterConfigService, - SidecarStatusMapper sidecarStatusMapper) { + 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); } @@ -195,39 +201,55 @@ public SidecarSummary get(@ApiParam(name = "sidecarId", required = true) @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 sidecarId, + @PathParam("sidecarId") @NotEmpty String nodeId, @ApiParam(name = "JSON body", required = true) @Valid @NotNull RegistrationRequest request, - @HeaderParam(value = "X-Graylog-Sidecar-Version") @NotEmpty String sidecarVersion) { - final Sidecar newSidecar; - final Sidecar oldSidecar = sidecarService.findByNodeId(sidecarId); - List assignments = null; + @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) { - assignments = oldSidecar.assignments(); - newSidecar = oldSidecar.toBuilder() + sidecar = oldSidecar.toBuilder() .nodeName(request.nodeName()) .nodeDetails(request.nodeDetails()) .sidecarVersion(sidecarVersion) .lastSeen(DateTime.now(DateTimeZone.UTC)) .build(); } else { - newSidecar = sidecarService.fromRequest(sidecarId, request, sidecarVersion); + sidecar = sidecarService.fromRequest(nodeId, request, sidecarVersion); } - sidecarService.save(newSidecar); - final CollectorActions collectorActions = actionService.findActionBySidecar(sidecarId, true); + // If the sidecar has the recent registration, return with HTTP 304 + if (ifNoneMatch != null) { + EntityTag etag = new EntityTag(ifNoneMatch.replaceAll("\"", "")); + if (etagService.registrationIsCached(sidecar.id(), 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( - this.sidecarConfiguration.sidecarUpdateInterval().toStandardDuration().getStandardSeconds(), - this.sidecarConfiguration.sidecarSendStatus()), - this.sidecarConfiguration.sidecarConfigurationOverride(), + sidecarConfiguration.sidecarUpdateInterval().toStandardDuration().getStandardSeconds(), + sidecarConfiguration.sidecarSendStatus()), + sidecarConfiguration.sidecarConfigurationOverride(), collectorAction, - assignments); - return Response.accepted(sidecarRegistrationResponse).build(); + sidecar.assignments()); + // add new etag to cache + EntityTag registrationEtag = etagService.buildEntityTagForResponse(sidecarRegistrationResponse); + etagService.addSidecarRegistration(sidecar.id(), registrationEtag.toString()); + + return Response.accepted(sidecarRegistrationResponse).tag(registrationEtag).build(); } @PUT @@ -249,8 +271,9 @@ public Response assignConfiguration(@ApiParam(name = "JSON body", required = tru .flatMap(a -> a.assignments().stream()) .collect(Collectors.toList()); try { - Sidecar sidecar = sidecarService.assignConfiguration(nodeId, nodeRelations); + Sidecar sidecar = sidecarService.applyManualAssignments(nodeId, nodeRelations); sidecarService.save(sidecar); + etagService.invalidateRegistration(sidecar.id()); } catch (org.graylog2.database.NotFoundException e) { throw new NotFoundException(e.getMessage()); } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ConfigurationService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ConfigurationService.java index cccf81573014..da1ef990d41a 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ConfigurationService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ConfigurationService.java @@ -17,6 +17,7 @@ package org.graylog.plugins.sidecar.services; import com.mongodb.BasicDBObject; +import com.mongodb.DBCollection; import freemarker.cache.MultiTemplateLoader; import freemarker.cache.StringTemplateLoader; import freemarker.cache.TemplateLoader; @@ -47,6 +48,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -60,7 +62,7 @@ public class ConfigurationService extends PaginatedDbService { private static final freemarker.template.Configuration templateConfiguration = new freemarker.template.Configuration(freemarker.template.Configuration.VERSION_2_3_28); private static final StringTemplateLoader stringTemplateLoader = new StringTemplateLoader(); - private ConfigurationVariableService configurationVariableService; + private final ConfigurationVariableService configurationVariableService; private static final String COLLECTION_NAME = "sidecar_configurations"; @@ -70,6 +72,7 @@ public ConfigurationService(MongoConnection mongoConnection, ConfigurationVariableService configurationVariableService) { super(mongoConnection, mapper, Configuration.class, COLLECTION_NAME); MongoDbTemplateLoader mongoDbTemplateLoader = new MongoDbTemplateLoader(db); + DBCollection collection = db.getDB().getCollection(COLLECTION_NAME); MultiTemplateLoader multiTemplateLoader = new MultiTemplateLoader(new TemplateLoader[] { mongoDbTemplateLoader, stringTemplateLoader }); @@ -79,6 +82,10 @@ public ConfigurationService(MongoConnection mongoConnection, templateConfiguration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); templateConfiguration.setLogTemplateExceptions(false); this.configurationVariableService = configurationVariableService; + + collection.createIndex(new BasicDBObject(Configuration.FIELD_ID, 1)); + collection.createIndex(new BasicDBObject(Configuration.FIELD_COLLECTOR_ID, 1)); + collection.createIndex(new BasicDBObject(Configuration.FIELD_TAGS, 1)); } public Configuration find(String id) { @@ -116,6 +123,10 @@ public List findByConfigurationVariable(ConfigurationVariable con return findByQuery(query); } + public List findByTags(Set tags) { + return findByQuery(DBQuery.in(Configuration.FIELD_TAGS, tags)); + } + public void replaceVariableNames(String oldName, String newName) { final DBQuery.Query query = DBQuery.regex(Configuration.FIELD_TEMPLATE, Pattern.compile(Pattern.quote(oldName))); List configurations = findByQuery(query); @@ -134,15 +145,17 @@ public Configuration save(Configuration configuration) { public Configuration copyConfiguration(String id, String name) { Configuration configuration = find(id); - return Configuration.create(configuration.collectorId(), name, configuration.color(), configuration.template()); + // Tags are not copied on purpose + return Configuration.createWithoutId(configuration.collectorId(), name, configuration.color(), configuration.template(), Set.of()); } public Configuration fromRequest(Configuration request) { - return Configuration.create( + return Configuration.createWithoutId( request.collectorId(), request.name(), request.color(), - request.template()); + request.template(), + request.tags()); } public Configuration fromRequest(String id, Configuration request) { @@ -151,7 +164,8 @@ public Configuration fromRequest(String id, Configuration request) { request.collectorId(), request.name(), request.color(), - request.template()); + request.template(), + request.tags()); } public Configuration renderConfigurationForCollector(Sidecar sidecar, Configuration configuration) throws RenderTemplateException { @@ -165,13 +179,15 @@ public Configuration renderConfigurationForCollector(Sidecar sidecar, Configurat String pathDelim = sidecar.nodeDetails().operatingSystem().equalsIgnoreCase("windows") ? "\\" : "/"; context.put("spoolDir", sidecar.nodeDetails().collectorConfigurationDirectory() + pathDelim + configuration.id()); } + context.put("tags", sidecar.nodeDetails().tags().stream().collect(Collectors.toMap(t -> t, t -> true))); return Configuration.create( configuration.id(), configuration.collectorId(), configuration.name(), configuration.color(), - renderTemplate(configuration.id(), context) + renderTemplate(configuration.id(), context), + configuration.tags() ); } @@ -182,6 +198,7 @@ public String renderPreview(String template) throws RenderTemplateException { context.put("sidecarVersion", ""); context.put("operatingSystem", ""); context.put("spoolDir", ""); + context.put("tags", Map.of()); String previewName = UUID.randomUUID().toString(); stringTemplateLoader.putTemplate(previewName, template); diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagCacheInvalidation.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagCacheInvalidation.java index 9d6123dd1dbe..5d249ff8a464 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagCacheInvalidation.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagCacheInvalidation.java @@ -23,12 +23,15 @@ @AutoValue public abstract class EtagCacheInvalidation { - @JsonProperty("etag") - public abstract String etag(); + @JsonProperty("cache_context") + public abstract EtagService.CacheContext cacheContext(); + + @JsonProperty("cache_key") + public abstract String cacheKey(); @JsonCreator - public static EtagCacheInvalidation etag(@JsonProperty("etag") String etag) { - return new AutoValue_EtagCacheInvalidation(etag); + public static EtagCacheInvalidation create(@JsonProperty("cache_context") EtagService.CacheContext context, @JsonProperty("cache_key") String cacheKey) { + return new AutoValue_EtagCacheInvalidation(context, cacheKey); } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagService.java index 489df2c00bcf..a833c4761f52 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagService.java @@ -17,21 +17,29 @@ package org.graylog.plugins.sidecar.services; import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.joschi.jadconfig.util.Duration; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; +import com.google.common.hash.Hashing; import com.google.common.util.concurrent.AbstractIdleService; import org.graylog.plugins.sidecar.common.SidecarPluginConfiguration; import org.graylog2.events.ClusterEventBus; import org.graylog2.metrics.CacheStatsSet; +import org.graylog2.shared.bindings.providers.ObjectMapperProvider; import org.graylog2.shared.metrics.MetricUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Singleton; +import javax.ws.rs.core.EntityTag; +import java.nio.charset.StandardCharsets; import static com.codahale.metrics.MetricRegistry.name; @@ -39,21 +47,48 @@ public class EtagService extends AbstractIdleService { private static final Logger LOG = LoggerFactory.getLogger(EtagService.class); - private final Cache cache; - private MetricRegistry metricRegistry; - private EventBus eventBus; - private ClusterEventBus clusterEventBus; + private final Cache collectorCache; + private final Cache configurationCache; + private final Cache registrationCache; + private final MetricRegistry metricRegistry; + private final EventBus eventBus; + private final ClusterEventBus clusterEventBus; + private final ObjectMapper objectMapper; + + @JsonAutoDetect + enum CacheContext { + @JsonProperty("collector") + COLLECTOR, + @JsonProperty("configuration") + CONFIGURATION, + @JsonProperty("registration") + REGISTRATION + } @Inject public EtagService(SidecarPluginConfiguration pluginConfiguration, MetricRegistry metricRegistry, EventBus eventBus, - ClusterEventBus clusterEventBus) { + ClusterEventBus clusterEventBus, + ObjectMapperProvider objectMapperProvider) { this.metricRegistry = metricRegistry; this.eventBus = eventBus; this.clusterEventBus = clusterEventBus; + this.objectMapper = objectMapperProvider.get(); Duration cacheTime = pluginConfiguration.getCacheTime(); - cache = CacheBuilder.newBuilder() + collectorCache = CacheBuilder.newBuilder() + .recordStats() + .expireAfterWrite(cacheTime.getQuantity(), cacheTime.getUnit()) + .maximumSize(pluginConfiguration.getCacheMaxSize()) + .build(); + + configurationCache = CacheBuilder.newBuilder() + .recordStats() + .expireAfterWrite(cacheTime.getQuantity(), cacheTime.getUnit()) + .maximumSize(pluginConfiguration.getCacheMaxSize()) + .build(); + + registrationCache = CacheBuilder.newBuilder() .recordStats() .expireAfterWrite(cacheTime.getQuantity(), cacheTime.getUnit()) .maximumSize(pluginConfiguration.getCacheMaxSize()) @@ -62,41 +97,84 @@ public EtagService(SidecarPluginConfiguration pluginConfiguration, @Subscribe public void handleEtagInvalidation(EtagCacheInvalidation event) { - if (event.etag().equals("")) { - LOG.trace("Invalidating all collector configuration etags"); + var cache = switch (event.cacheContext()) { + case COLLECTOR -> collectorCache; + case CONFIGURATION -> configurationCache; + case REGISTRATION -> registrationCache; + }; + + if (event.cacheKey().equals("")) { + LOG.trace("Invalidating {} cache for all keys", event.cacheContext()); cache.invalidateAll(); } else { - LOG.trace("Invalidating collector configuration etag {}", event.etag()); - cache.invalidate(event.etag()); + LOG.trace("Invalidating {} cache for cacheKey {}", event.cacheContext(), event.cacheKey()); + cache.invalidate(event.cacheKey()); } } - public boolean isPresent(String etag) { - return cache.getIfPresent(etag) != null; + public boolean collectorsAreCached(String etag) { + return collectorCache.getIfPresent(etag) != null; + } + + public boolean configurationsAreCached(String etag) { + return configurationCache.getIfPresent(etag) != null; + } + + public boolean registrationIsCached(String sidecarId, String etag) { + return etag.equals(registrationCache.getIfPresent(sidecarId)); + } + + public void registerCollector(String etag) { + collectorCache.put(etag, Boolean.TRUE); + } + + public void registerConfiguration(String etag) { + configurationCache.put(etag, Boolean.TRUE); + } + + public void addSidecarRegistration(String sidecarId, String etag) { + registrationCache.put(sidecarId, etag); + } + + + public void invalidateAllConfigurations() { + configurationCache.invalidateAll(); + clusterEventBus.post(EtagCacheInvalidation.create(CacheContext.CONFIGURATION, "")); + } + + public void invalidateAllCollectors() { + collectorCache.invalidateAll(); + clusterEventBus.post(EtagCacheInvalidation.create(CacheContext.COLLECTOR, "")); } - public void put(String etag) { - cache.put(etag, Boolean.TRUE); + public void invalidateAllRegistrations() { + registrationCache.invalidateAll(); + clusterEventBus.post(EtagCacheInvalidation.create(CacheContext.REGISTRATION, "")); } - public void invalidate(String etag) { - clusterEventBus.post(EtagCacheInvalidation.etag(etag)); + public void invalidateRegistration(String sidecarId) { + registrationCache.invalidate(sidecarId); + clusterEventBus.post(EtagCacheInvalidation.create(CacheContext.REGISTRATION, sidecarId)); } - public void invalidateAll() { - cache.invalidateAll(); - clusterEventBus.post(EtagCacheInvalidation.etag("")); + public EntityTag buildEntityTagForResponse(Object o) throws JsonProcessingException { + final String json = objectMapper.writeValueAsString(o); + return new EntityTag(Hashing.murmur3_128().hashString(json, StandardCharsets.UTF_8).toString()); } @Override protected void startUp() throws Exception { eventBus.register(this); - MetricUtils.safelyRegisterAll(metricRegistry, new CacheStatsSet(name(ConfigurationService.class, "etag-cache"), cache)); + MetricUtils.safelyRegisterAll(metricRegistry, new CacheStatsSet(name(ConfigurationService.class, "etag-cache"), configurationCache)); + MetricUtils.safelyRegisterAll(metricRegistry, new CacheStatsSet(name(CollectorService.class, "etag-cache"), collectorCache)); + MetricUtils.safelyRegisterAll(metricRegistry, new CacheStatsSet(name(SidecarService.class, "etag-cache"), registrationCache)); } @Override protected void shutDown() throws Exception { eventBus.unregister(this); metricRegistry.removeMatching((name, metric) -> name.startsWith(name(ConfigurationService.class, "etag-cache"))); + metricRegistry.removeMatching((name, metric) -> name.startsWith(name(CollectorService.class, "etag-cache"))); + metricRegistry.removeMatching((name, metric) -> name.startsWith(name(SidecarService.class, "etag-cache"))); } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java index 865ceadce079..8675cea6994a 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java @@ -16,8 +16,10 @@ */ package org.graylog.plugins.sidecar.services; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.mongodb.BasicDBObject; +import org.apache.commons.collections4.CollectionUtils; import org.graylog.plugins.sidecar.rest.models.Collector; import org.graylog.plugins.sidecar.rest.models.CollectorStatus; import org.graylog.plugins.sidecar.rest.models.CollectorStatusList; @@ -42,6 +44,8 @@ import javax.inject.Inject; import javax.validation.ConstraintViolation; import javax.validation.Validator; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Set; import java.util.function.Predicate; @@ -75,23 +79,49 @@ public long count() { @Override public Sidecar save(Sidecar sidecar) { - if (sidecar != null) { - final Set> violations = validator.validate(sidecar); - if (violations.isEmpty()) { - return db.findAndModify( - DBQuery.is(Sidecar.FIELD_NODE_ID, sidecar.nodeId()), - new BasicDBObject(), - new BasicDBObject(), - false, - sidecar, - true, - true); - } else { - throw new IllegalArgumentException("Specified object failed validation: " + violations); - } - } else { - throw new IllegalArgumentException("Specified object is not of correct implementation type (" + sidecar.getClass() + ")!"); + Preconditions.checkNotNull(sidecar, "sidecar was null"); + + final Set> violations = validator.validate(sidecar); + if (!violations.isEmpty()) { + throw new IllegalArgumentException("Specified object failed validation: " + violations); } + + return db.findAndModify( + DBQuery.is(Sidecar.FIELD_NODE_ID, sidecar.nodeId()), + new BasicDBObject(), + new BasicDBObject(), + false, + sidecar, + true, + true); + } + + // Create new assignments based on tags and existing manual assignments' + public Sidecar updateTaggedConfigurationAssignments(Sidecar sidecar) { + final Set sidecarTags = sidecar.nodeDetails().tags(); + + // find all configurations that match the tags + // TODO only find configs that match the right collector os + final List taggedConfigs = configurationService.findByTags(sidecarTags); + + final List tagAssigned = taggedConfigs.stream().map(c -> { + // fill in ConfigurationAssignment.assignedFromTags() + // If we only support one tag on a configuration, this can be simplified + final Set matchedTags = c.tags().stream().filter(sidecarTags::contains).collect(Collectors.toSet()); + return ConfigurationAssignment.create(c.collectorId(), c.id(), matchedTags); + }).toList(); + + final List manuallyAssigned = sidecar.assignments().stream().filter(a -> { + // also overwrite manually assigned configs that would now be assigned through tags + if (tagAssigned.stream().anyMatch(tagAssignment -> tagAssignment.configurationId().equals(a.configurationId()))) { + return false; + } + return a.assignedFromTags().isEmpty(); + }).toList(); + + // return a sidecar with updated assignments + final Collection union = CollectionUtils.union(manuallyAssigned, tagAssigned); + return sidecar.toBuilder().assignments(new ArrayList<>(union)).build(); } public List all() { @@ -203,7 +233,7 @@ public Sidecar fromRequest(String nodeId, RegistrationRequest request, String co collectorVersion); } - public Sidecar assignConfiguration(String collectorNodeId, List assignments) throws NotFoundException{ + public Sidecar applyManualAssignments(String collectorNodeId, List assignments) throws NotFoundException{ Sidecar sidecar = findByNodeId(collectorNodeId); if (sidecar == null) { throw new NotFoundException("Couldn't find collector with ID " + collectorNodeId); @@ -222,8 +252,16 @@ public Sidecar assignConfiguration(String collectorNodeId, List taggedAssignments = sidecar.assignments().stream().filter(a -> !a.assignedFromTags().isEmpty()).toList(); + final List configIdsAssignedThroughTags = taggedAssignments.stream().map(ConfigurationAssignment::configurationId).toList(); + + final List filteredAssignments = assignments.stream().filter(a -> !configIdsAssignedThroughTags.contains(a.configurationId())).toList(); + final Collection union = CollectionUtils.union(filteredAssignments, taggedAssignments); + Sidecar toSave = sidecar.toBuilder() - .assignments(assignments) + .assignments(new ArrayList<>(union)) .build(); return save(toSave); } diff --git a/graylog2-server/src/main/java/org/graylog2/contentpacks/facades/SidecarCollectorConfigurationFacade.java b/graylog2-server/src/main/java/org/graylog2/contentpacks/facades/SidecarCollectorConfigurationFacade.java index 91dd72e35350..e8d0c24eb3e7 100644 --- a/graylog2-server/src/main/java/org/graylog2/contentpacks/facades/SidecarCollectorConfigurationFacade.java +++ b/graylog2-server/src/main/java/org/graylog2/contentpacks/facades/SidecarCollectorConfigurationFacade.java @@ -91,11 +91,12 @@ public NativeEntity createNativeEntity(Entity entity, private NativeEntity decode(EntityV1 entity, Map parameters) { final SidecarCollectorConfigurationEntity configurationEntity = objectMapper.convertValue(entity.data(), SidecarCollectorConfigurationEntity.class); - final Configuration configuration = Configuration.create( + final Configuration configuration = Configuration.createWithoutId( configurationEntity.collectorId().asString(parameters), configurationEntity.title().asString(parameters), configurationEntity.color().asString(parameters), - configurationEntity.template().asString(parameters)); + configurationEntity.template().asString(parameters), + Set.of()); final Configuration savedConfiguration = configurationService.save(configuration); return NativeEntity.create(entity.id(), savedConfiguration.id(), TYPE_V1, configuration.name(), savedConfiguration); diff --git a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/ConfigurationServiceTest.java b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/ConfigurationServiceTest.java index b02c69dd3974..d2c3fed73837 100644 --- a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/ConfigurationServiceTest.java +++ b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/ConfigurationServiceTest.java @@ -33,6 +33,8 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.Set; + import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.when; @@ -56,7 +58,7 @@ public class ConfigurationServiceTest { private Configuration buildTestConfig(String template) { - return Configuration.create(FILEBEAT_CONF_ID, "collId", "filebeat", "#ffffff", template); + return Configuration.create(FILEBEAT_CONF_ID, "collId", "filebeat", "#ffffff", template, Set.of()); } @Before diff --git a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java index 592a6c2581fe..270250ac6e1f 100644 --- a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java +++ b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java @@ -36,6 +36,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import javax.validation.Validator; import java.util.List; @@ -50,6 +52,9 @@ @UseModules({ObjectMapperModule.class, ValidatorModule.class, TestPasswordSecretModule.class}) public class SidecarServiceImplTest { private static final String collectionName = "sidecars"; + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock private CollectorService collectorService; diff --git a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java index a832eb4b7b6f..d189da40dbe4 100644 --- a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java +++ b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java @@ -26,6 +26,7 @@ import org.graylog.plugins.sidecar.rest.requests.RegistrationRequest; import org.graylog.plugins.sidecar.rest.resources.SidecarResource; 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.plugin.cluster.ClusterConfigService; @@ -38,6 +39,7 @@ import org.mockito.runners.MockitoJUnitRunner; import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.EntityTag; import javax.ws.rs.core.Response; import java.util.List; @@ -45,6 +47,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -66,6 +69,9 @@ public class SidecarResourceTest extends RestResourceBaseTest { @Mock private ClusterConfigService clusterConfigService; + @Mock + private EtagService etagService; + @Mock private SidecarConfiguration sidecarConfiguration; @@ -74,11 +80,13 @@ public void setUp() throws Exception { this.sidecars = getDummyCollectorList(); when(clusterConfigService.getOrDefault(SidecarConfiguration.class, SidecarConfiguration.defaultConfiguration())).thenReturn(sidecarConfiguration); when(sidecarConfiguration.sidecarUpdateInterval()).thenReturn(Period.seconds(30)); + when(etagService.buildEntityTagForResponse(any())).thenReturn(new EntityTag("hash browns")); this.resource = new SidecarResource( sidecarService, actionService, clusterConfigService, - statusMapper); + statusMapper, + etagService); } @Test(expected = NotFoundException.class) @@ -118,20 +126,25 @@ private List getDummyCollectorList() { @Test public void testRegister() throws Exception { + final NodeDetails nodeDetails = NodeDetails.create( + "DummyOS 1.0", + null, + null, + null, + null, + null, + null + ); final RegistrationRequest input = RegistrationRequest.create( "nodeName", - NodeDetails.create( - "DummyOS 1.0", - null, - null, - null, - null, - null, - null - ) + nodeDetails + ); + when(sidecarService.fromRequest(any(), any(RegistrationRequest.class), anyString())).thenReturn( + Sidecar.create("nodeId", "name", nodeDetails, "0.0.1") ); + when(sidecarService.updateTaggedConfigurationAssignments(any(Sidecar.class))).thenAnswer(invocation -> invocation.getArgument(0)); - final Response response = this.resource.register("sidecarId", input, "0.0.1"); + final Response response = this.resource.register("sidecarId", input, null, "0.0.1"); assertThat(response).isSuccess(); } @@ -152,7 +165,7 @@ public void testRegisterInvalidCollectorId() throws Exception { ) ); - final Response response = this.resource.register("", invalid, "0.0.1"); + final Response response = this.resource.register("", invalid, null, "0.0.1"); assertThat(response).isError(); assertThat(response).isStatus(Response.Status.BAD_REQUEST); @@ -174,7 +187,7 @@ public void testRegisterInvalidNodeId() throws Exception { ) ); - final Response response = this.resource.register("sidecarId", invalid, "0.0.1"); + final Response response = this.resource.register("sidecarId", invalid, null, "0.0.1"); assertThat(response).isError(); assertThat(response).isStatus(Response.Status.BAD_REQUEST); @@ -188,7 +201,7 @@ public void testRegisterMissingNodeDetails() throws Exception { null ); - final Response response = this.resource.register("sidecarId", invalid, "0.0.1"); + final Response response = this.resource.register("sidecarId", invalid, null, "0.0.1"); assertThat(response).isError(); assertThat(response).isStatus(Response.Status.BAD_REQUEST); @@ -210,7 +223,7 @@ public void testRegisterMissingOperatingSystem() throws Exception { ) ); - final Response response = this.resource.register("sidecarId", invalid, "0.0.1"); + final Response response = this.resource.register("sidecarId", invalid, null, "0.0.1"); assertThat(response).isError(); assertThat(response).isStatus(Response.Status.BAD_REQUEST); diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx index b0d9f2be683f..55fab55a4a4a 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx @@ -65,6 +65,7 @@ const ConfigurationForm = createReactClass({ color: configuration.color, collector_id: configuration.collector_id, template: configuration.template || '', + tags: configuration.tags }, }; }, From 1ce6a4f65ff37ea44b7286e057d0bff39cfd4722 Mon Sep 17 00:00:00 2001 From: Marco Pfatschbacher Date: Wed, 21 Sep 2022 17:32:07 +0200 Subject: [PATCH 05/82] Sidecar tags tests (#13502) * Only assign tagged configs that match the OS * rename test * add tests for SidecarService.updateTaggedConfigurationAssignments() * add tests for SidecarService.applyManualAssignments() --- .../sidecar/services/SidecarService.java | 13 +- .../collectors/SidecarServiceImplTest.java | 169 -------- .../collectors/SidecarServiceTest.java | 381 ++++++++++++++++++ .../collectors/collectorsSingleDataset.json | 14 - .../plugins/sidecar/collectors/sidecars.json | 16 + 5 files changed, 405 insertions(+), 188 deletions(-) delete mode 100644 graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java create mode 100644 graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceTest.java delete mode 100644 graylog2-server/src/test/resources/org/graylog/plugins/sidecar/collectors/collectorsSingleDataset.json create mode 100644 graylog2-server/src/test/resources/org/graylog/plugins/sidecar/collectors/sidecars.json diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java index 8675cea6994a..4c886ea10007 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java @@ -101,10 +101,13 @@ public Sidecar updateTaggedConfigurationAssignments(Sidecar sidecar) { final Set sidecarTags = sidecar.nodeDetails().tags(); // find all configurations that match the tags - // TODO only find configs that match the right collector os final List taggedConfigs = configurationService.findByTags(sidecarTags); + final Set matchingOsCollectorIds = collectorService.all().stream() + .filter(c -> c.nodeOperatingSystem().equalsIgnoreCase(sidecar.nodeDetails().operatingSystem())) + .map(Collector::id).collect(Collectors.toSet()); - final List tagAssigned = taggedConfigs.stream().map(c -> { + final List tagAssigned = taggedConfigs.stream() + .filter(c -> matchingOsCollectorIds.contains(c.collectorId())).map(c -> { // fill in ConfigurationAssignment.assignedFromTags() // If we only support one tag on a configuration, this can be simplified final Set matchedTags = c.tags().stream().filter(sidecarTags::contains).collect(Collectors.toSet()); @@ -233,10 +236,10 @@ public Sidecar fromRequest(String nodeId, RegistrationRequest request, String co collectorVersion); } - public Sidecar applyManualAssignments(String collectorNodeId, List assignments) throws NotFoundException{ - Sidecar sidecar = findByNodeId(collectorNodeId); + public Sidecar applyManualAssignments(String sidecarNodeId, List assignments) throws NotFoundException{ + Sidecar sidecar = findByNodeId(sidecarNodeId); if (sidecar == null) { - throw new NotFoundException("Couldn't find collector with ID " + collectorNodeId); + throw new NotFoundException("Couldn't find sidecar with nodeId " + sidecarNodeId); } for (ConfigurationAssignment assignment : assignments) { Collector collector = collectorService.find(assignment.collectorId()); diff --git a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java deleted file mode 100644 index 270250ac6e1f..000000000000 --- a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * 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.collectors; - -import com.mongodb.client.MongoCollection; -import org.bson.Document; -import org.graylog.plugins.sidecar.rest.models.NodeDetails; -import org.graylog.plugins.sidecar.rest.models.Sidecar; -import org.graylog.plugins.sidecar.services.CollectorService; -import org.graylog.plugins.sidecar.services.ConfigurationService; -import org.graylog.plugins.sidecar.services.SidecarService; -import org.graylog.testing.inject.TestPasswordSecretModule; -import org.graylog.testing.mongodb.MongoDBFixtures; -import org.graylog.testing.mongodb.MongoDBInstance; -import org.graylog2.bindings.providers.MongoJackObjectMapperProvider; -import org.graylog2.shared.bindings.ObjectMapperModule; -import org.graylog2.shared.bindings.ValidatorModule; -import org.jukito.JukitoRunner; -import org.jukito.UseModules; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -import javax.validation.Validator; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -@RunWith(JukitoRunner.class) -@UseModules({ObjectMapperModule.class, ValidatorModule.class, TestPasswordSecretModule.class}) -public class SidecarServiceImplTest { - private static final String collectionName = "sidecars"; - - @Rule - public final MockitoRule mockitoRule = MockitoJUnit.rule(); - @Mock - private CollectorService collectorService; - - @Mock private ConfigurationService configurationService; - - @Rule - public final MongoDBInstance mongodb = MongoDBInstance.createForClass(); - - private SidecarService sidecarService; - - @Before - public void setUp(MongoJackObjectMapperProvider mapperProvider, - Validator validator) throws Exception { - this.sidecarService = new SidecarService(collectorService, configurationService, mongodb.mongoConnection(), mapperProvider, validator); - } - - @Test - public void testCountEmptyCollection() throws Exception { - final long result = this.sidecarService.count(); - - assertEquals(0, result); - } - - @Test - @MongoDBFixtures("collectorsMultipleDocuments.json") - public void testCountNonEmptyCollection() throws Exception { - final long result = this.sidecarService.count(); - - assertEquals(3, result); - } - - @Test - public void testSaveFirstRecord() throws Exception { - String nodeId = "nodeId"; - String nodeName = "nodeName"; - String version = "0.0.1"; - String os = "DummyOS 1.0"; - final Sidecar sidecar = Sidecar.create( - nodeId, - nodeName, - NodeDetails.create( - os, - null, - null, - null, - null, - null, - null), - version - ); - - final Sidecar result = this.sidecarService.save(sidecar); - MongoCollection collection = mongodb.mongoConnection().getMongoDatabase().getCollection(collectionName); - Document document = collection.find().first(); - Document nodeDetails = document.get("node_details", Document.class); - - assertNotNull(result); - assertEquals(nodeId, document.get("node_id")); - assertEquals(nodeName, document.get("node_name")); - assertEquals(version, document.get("sidecar_version")); - assertEquals(os, nodeDetails.get("operating_system")); - } - - @Test - @MongoDBFixtures("collectorsMultipleDocuments.json") - public void testAll() throws Exception { - final List sidecars = this.sidecarService.all(); - - assertNotNull(sidecars); - assertEquals(3, sidecars.size()); - } - - @Test - public void testAllEmptyCollection() throws Exception { - final List sidecars = this.sidecarService.all(); - - assertNotNull(sidecars); - assertEquals(0, sidecars.size()); - } - - @Test - @MongoDBFixtures("collectorsMultipleDocuments.json") - public void testFindById() throws Exception { - final String collector1id = "uniqueid1"; - - final Sidecar sidecar = this.sidecarService.findByNodeId(collector1id); - - assertNotNull(sidecar); - assertEquals(collector1id, sidecar.nodeId()); - } - - @Test - @MongoDBFixtures("collectorsMultipleDocuments.json") - public void testFindByIdNonexisting() throws Exception { - final String collector1id = "nonexisting"; - - final Sidecar sidecar = this.sidecarService.findByNodeId(collector1id); - - assertNull(sidecar); - } - - @Test - @MongoDBFixtures("collectorsMultipleDocuments.json") - public void testDestroy() throws Exception { - final Sidecar sidecar = mock(Sidecar.class); - when(sidecar.id()).thenReturn("581b3bff8e4dc4270055dfcb"); - - final int result = this.sidecarService.delete(sidecar.id()); - assertEquals(1, result); - assertEquals(2, mongodb.mongoConnection().getMongoDatabase().getCollection(collectionName).countDocuments()); - } -} diff --git a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceTest.java b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceTest.java new file mode 100644 index 000000000000..5240e1753ba5 --- /dev/null +++ b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceTest.java @@ -0,0 +1,381 @@ +/* + * 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.collectors; + +import com.mongodb.client.MongoCollection; +import org.bson.Document; +import org.graylog.plugins.sidecar.rest.models.Collector; +import org.graylog.plugins.sidecar.rest.models.Configuration; +import org.graylog.plugins.sidecar.rest.models.NodeDetails; +import org.graylog.plugins.sidecar.rest.models.Sidecar; +import org.graylog.plugins.sidecar.rest.requests.ConfigurationAssignment; +import org.graylog.plugins.sidecar.services.CollectorService; +import org.graylog.plugins.sidecar.services.ConfigurationService; +import org.graylog.plugins.sidecar.services.SidecarService; +import org.graylog.testing.inject.TestPasswordSecretModule; +import org.graylog.testing.mongodb.MongoDBFixtures; +import org.graylog.testing.mongodb.MongoDBInstance; +import org.graylog2.bindings.providers.MongoJackObjectMapperProvider; +import org.graylog2.database.NotFoundException; +import org.graylog2.shared.bindings.ObjectMapperModule; +import org.graylog2.shared.bindings.ValidatorModule; +import org.jukito.JukitoRunner; +import org.jukito.UseModules; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import javax.validation.Validator; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(JukitoRunner.class) +@UseModules({ObjectMapperModule.class, ValidatorModule.class, TestPasswordSecretModule.class}) +public class SidecarServiceTest { + private static final String collectionName = "sidecars"; + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + @Mock + private CollectorService collectorService; + + @Mock private ConfigurationService configurationService; + + @Rule + public final MongoDBInstance mongodb = MongoDBInstance.createForClass(); + + private SidecarService sidecarService; + + @Before + public void setUp(MongoJackObjectMapperProvider mapperProvider, + Validator validator) throws Exception { + this.sidecarService = new SidecarService(collectorService, configurationService, mongodb.mongoConnection(), mapperProvider, validator); + } + + @Test + public void testCountEmptyCollection() throws Exception { + final long result = this.sidecarService.count(); + + assertEquals(0, result); + } + + @Test + @MongoDBFixtures("collectorsMultipleDocuments.json") + public void testCountNonEmptyCollection() throws Exception { + final long result = this.sidecarService.count(); + + assertEquals(3, result); + } + + @Test + public void testSaveFirstRecord() throws Exception { + String nodeId = "nodeId"; + String nodeName = "nodeName"; + String version = "0.0.1"; + String os = "DummyOS 1.0"; + final Sidecar sidecar = Sidecar.create( + nodeId, + nodeName, + NodeDetails.create( + os, + null, + null, + null, + null, + null, + null), + version + ); + + final Sidecar result = this.sidecarService.save(sidecar); + MongoCollection collection = mongodb.mongoConnection().getMongoDatabase().getCollection(collectionName); + Document document = collection.find().first(); + Document nodeDetails = document.get("node_details", Document.class); + + assertNotNull(result); + assertEquals(nodeId, document.get("node_id")); + assertEquals(nodeName, document.get("node_name")); + assertEquals(version, document.get("sidecar_version")); + assertEquals(os, nodeDetails.get("operating_system")); + } + + @Test + @MongoDBFixtures("collectorsMultipleDocuments.json") + public void testAll() throws Exception { + final List sidecars = this.sidecarService.all(); + + assertNotNull(sidecars); + assertEquals(3, sidecars.size()); + } + + @Test + public void testAllEmptyCollection() throws Exception { + final List sidecars = this.sidecarService.all(); + + assertNotNull(sidecars); + assertEquals(0, sidecars.size()); + } + + @Test + @MongoDBFixtures("collectorsMultipleDocuments.json") + public void testFindById() throws Exception { + final String collector1id = "uniqueid1"; + + final Sidecar sidecar = this.sidecarService.findByNodeId(collector1id); + + assertNotNull(sidecar); + assertEquals(collector1id, sidecar.nodeId()); + } + + @Test + @MongoDBFixtures("collectorsMultipleDocuments.json") + public void testFindByIdNonexisting() throws Exception { + final String collector1id = "nonexisting"; + + final Sidecar sidecar = this.sidecarService.findByNodeId(collector1id); + + assertNull(sidecar); + } + + @Test + @MongoDBFixtures("collectorsMultipleDocuments.json") + public void testDestroy() throws Exception { + final Sidecar sidecar = mock(Sidecar.class); + when(sidecar.id()).thenReturn("581b3bff8e4dc4270055dfcb"); + + final int result = this.sidecarService.delete(sidecar.id()); + assertEquals(1, result); + assertEquals(2, mongodb.mongoConnection().getMongoDatabase().getCollection(collectionName).countDocuments()); + } + + @Test + public void simpleTagAssignment() { + final Configuration configuration = getConfiguration(); + when(configurationService.findByTags(anySet())).thenReturn(List.of(configuration)); + final Collector collector = getCollector(); + + when(collectorService.all()).thenReturn(List.of(collector)); + Sidecar sidecar = getTestSidecar(); + sidecar = sidecar.toBuilder().nodeDetails(getNodeDetails("linux", Set.of("tag1"))).build(); + + sidecar = sidecarService.updateTaggedConfigurationAssignments(sidecar); + + assertThat(sidecar.assignments()).hasSize(1); + assertThat(sidecar.assignments().get(0).assignedFromTags()).isEqualTo(Set.of("tag1")); + } + + @Test + public void mergeWithManualAssignments() { + final Configuration configuration = getConfiguration(); + when(configurationService.findByTags(anySet())).thenReturn(List.of(configuration)); + final Collector collector = getCollector(); + + when(collectorService.all()).thenReturn(List.of(collector)); + Sidecar sidecar = getTestSidecar(); + sidecar = sidecar.toBuilder().nodeDetails(getNodeDetails("linux", Set.of("tag1"))) + .assignments(List.of(ConfigurationAssignment.create("some collector", "some config", null))) + .build(); + + sidecar = sidecarService.updateTaggedConfigurationAssignments(sidecar); + + assertThat(sidecar.assignments()).hasSize(2); + assertThat(sidecar.assignments()).satisfies(assignments -> { + assertThat(assignments.stream().filter(a -> a.assignedFromTags().equals(Set.of())).findAny()).isPresent(); + assertThat(assignments.stream().filter(a -> a.assignedFromTags().equals(Set.of("tag1"))).findAny()).isPresent(); + }); + } + + @Test + public void updateExistingAssignment() { + final Configuration configuration = getConfiguration(); + when(configurationService.findByTags(anySet())).thenReturn(List.of(configuration)); + final Collector collector = getCollector(); + + when(collectorService.all()).thenReturn(List.of(collector)); + Sidecar sidecar = getTestSidecar(); + final ConfigurationAssignment existingAssignment = ConfigurationAssignment.create(collector.id(), configuration.id(), Set.of("tag1")); + sidecar = sidecar.toBuilder().nodeDetails(getNodeDetails("linux", Set.of("tag1"))) + .assignments(List.of(existingAssignment)) + .build(); + + sidecar = sidecarService.updateTaggedConfigurationAssignments(sidecar); + + assertThat(sidecar.assignments()).hasSize(1); + assertThat(sidecar.assignments()).first().isEqualTo(existingAssignment); + } + + @Test + public void updateExistingAssignments() { + final Configuration configuration = getConfiguration(); + when(configurationService.findByTags(anySet())).thenReturn(List.of(configuration)); + final Collector collector = getCollector(); + + when(collectorService.all()).thenReturn(List.of(collector)); + Sidecar sidecar = getTestSidecar(); + final ConfigurationAssignment existingTag3Assignment = ConfigurationAssignment.create(collector.id(), configuration.id(), Set.of("tag3")); + final ConfigurationAssignment existingManualAssignment = ConfigurationAssignment.create("some-collector", "some-config", null); + sidecar = sidecar.toBuilder().nodeDetails(getNodeDetails("linux", Set.of("tag1"))) + .assignments(List.of(existingTag3Assignment, existingManualAssignment)) + .build(); + + sidecar = sidecarService.updateTaggedConfigurationAssignments(sidecar); + + assertThat(sidecar.assignments()).hasSize(2); + assertThat(sidecar.assignments()).satisfies(assignments -> { + assertThat(assignments.stream().filter(a -> a.assignedFromTags().equals(Set.of())).findAny()).isPresent(); + assertThat(assignments.stream().filter(a -> a.assignedFromTags().equals(Set.of("tag3"))).findAny()).isEmpty(); + assertThat(assignments.stream().filter(a -> a.assignedFromTags().equals(Set.of("tag1"))).findAny()).isPresent(); + }); + } + + @Test + public void ignoresTagsWithWrongOS() { + final Configuration configuration = getConfiguration(); + when(configurationService.findByTags(anySet())).thenReturn(List.of(configuration)); + final Collector collector = getCollector(); + + when(collectorService.all()).thenReturn(List.of(collector)); + Sidecar sidecar = getTestSidecar(); + sidecar = sidecar.toBuilder().nodeDetails(getNodeDetails("windows", Set.of("tag1"))).build(); + + sidecar = sidecarService.updateTaggedConfigurationAssignments(sidecar); + + // The tagged config is linux only + assertThat(sidecar.assignments()).hasSize(0); + } + + @Test + public void replacesManualAssignmentsWithTaggedOnes() { + final Configuration configuration = getConfiguration(); + when(configurationService.findByTags(anySet())).thenReturn(List.of(configuration)); + final Collector collector = getCollector(); + + when(collectorService.all()).thenReturn(List.of(collector)); + Sidecar sidecar = getTestSidecar(); + final ConfigurationAssignment manualAssignment = ConfigurationAssignment.create(collector.id(), configuration.id(), null); + sidecar = sidecar.toBuilder().nodeDetails(getNodeDetails("linux", Set.of("tag1"))) + .assignments(List.of(manualAssignment)) + .build(); + + sidecar = sidecarService.updateTaggedConfigurationAssignments(sidecar); + + assertThat(sidecar.assignments()).hasSize(1); + assertThat(sidecar.assignments()).satisfies(assignments -> { + assertThat(assignments.stream().filter(a -> a.assignedFromTags().equals(Set.of("tag1"))).findAny()).isPresent(); + }); + } + + @Test + @MongoDBFixtures("sidecars.json") + public void applyManualAssignment() throws NotFoundException { + Sidecar sidecar = sidecarService.findByNodeId("node-id"); + + final Configuration configuration = getConfiguration(); + when(configurationService.find(anyString())).thenReturn(configuration); + final Collector collector = getCollector(); + when(collectorService.find(anyString())).thenReturn(collector); + + assertThat(sidecar.assignments()).isEmpty(); + final ConfigurationAssignment manualAssignment = ConfigurationAssignment.create(collector.id(), configuration.id(), null); + sidecar = sidecarService.applyManualAssignments(sidecar.nodeId(), List.of(manualAssignment)); + + assertThat(sidecar.assignments()).hasSize(1); + } + + @Test + @MongoDBFixtures("sidecars.json") + public void applyManualAssignmentKeepTagged() throws NotFoundException { + Sidecar sidecar = sidecarService.findByNodeId("node-id"); + final ConfigurationAssignment taggedAssignment = ConfigurationAssignment.create("some-collector", "some-config", Set.of("tag")); + sidecar = sidecarService.save(sidecar.toBuilder().assignments(List.of(taggedAssignment)).build()); + + final Configuration configuration = getConfiguration(); + when(configurationService.find(anyString())).thenReturn(configuration); + final Collector collector = getCollector(); + when(collectorService.find(anyString())).thenReturn(collector); + + assertThat(sidecar.assignments()).hasSize(1); + final ConfigurationAssignment manualAssignment = ConfigurationAssignment.create(collector.id(), configuration.id(), null); + sidecar = sidecarService.applyManualAssignments(sidecar.nodeId(), List.of(manualAssignment)); + + assertThat(sidecar.assignments()).hasSize(2); + } + + @Test + @MongoDBFixtures("sidecars.json") + public void ignoreModificationOfTaggedAssignments() throws NotFoundException { + Sidecar sidecar = sidecarService.findByNodeId("node-id"); + + final Configuration configuration = getConfiguration(); + when(configurationService.find(anyString())).thenReturn(configuration); + final Collector collector = getCollector(); + when(collectorService.find(anyString())).thenReturn(collector); + final ConfigurationAssignment taggedAssignment = ConfigurationAssignment.create(collector.id(), configuration.id(), Set.of("tag")); + sidecar = sidecarService.save(sidecar.toBuilder().assignments(List.of(taggedAssignment)).build()); + + + assertThat(sidecar.assignments()).hasSize(1); + final ConfigurationAssignment manualAssignment = ConfigurationAssignment.create(collector.id(), configuration.id(), null); + sidecar = sidecarService.applyManualAssignments(sidecar.nodeId(), List.of(manualAssignment)); + + assertThat(sidecar.assignments()).hasSize(1); + // Tagged assignment is kept intact + assertThat(sidecar.assignments().get(0).assignedFromTags()).isEqualTo(Set.of("tag")); + } + + private static Configuration getConfiguration() { + return Configuration.create("config-id", "collector-id", "config-name", "color", "template", Set.of("tag1")); + } + + private static Collector getCollector() { + return Collector.create("collector-id", "collector-name", "service", "linux", + "/path", "param", "valid param", ""); + } + + private Sidecar getTestSidecar() { + return Sidecar.create( + "node-id", + "node-name", + getNodeDetails("linux", null), + "1.3.0" + ); + } + + private NodeDetails getNodeDetails(String os, Set tags) { + return NodeDetails.create( + os, + null, + null, + null, + null, + tags, + null); + } +} diff --git a/graylog2-server/src/test/resources/org/graylog/plugins/sidecar/collectors/collectorsSingleDataset.json b/graylog2-server/src/test/resources/org/graylog/plugins/sidecar/collectors/collectorsSingleDataset.json deleted file mode 100644 index 6b906729885d..000000000000 --- a/graylog2-server/src/test/resources/org/graylog/plugins/sidecar/collectors/collectorsSingleDataset.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "sidecars": [ - { - "_id": "581b3bff8e4dc4270055dfcb", - "node_id": "nodeId", - "node_name": "nodeName", - "sidecar_version": "0.0.1", - "node_details": { - "operating_system": "DummyOS 1.0" - }, - "last_seen": "" - } - ] -} diff --git a/graylog2-server/src/test/resources/org/graylog/plugins/sidecar/collectors/sidecars.json b/graylog2-server/src/test/resources/org/graylog/plugins/sidecar/collectors/sidecars.json new file mode 100644 index 000000000000..d47fc9369ac3 --- /dev/null +++ b/graylog2-server/src/test/resources/org/graylog/plugins/sidecar/collectors/sidecars.json @@ -0,0 +1,16 @@ +{ + "sidecars": [ + { + "_id": { + "$oid": "581b3bff8e4dc4270055dfcb" + }, + "node_id": "node-id", + "node_name": "node-name", + "sidecar_version": "1.3.0", + "node_details": { + "operating_system": "linux" + }, + "last_seen": "2015-04-01T11:50:20.195Z" + } + ] +} From a1b29392bc40696598ba7a6f62b96bd5498fabe1 Mon Sep 17 00:00:00 2001 From: Marco Pfatschbacher Date: Wed, 21 Sep 2022 17:57:21 +0200 Subject: [PATCH 06/82] Fix cache invalidation for collector actions (#13504) * Fix cache invalidation for collector actions The registration cache was using the sidecar "_id", while the ActionService was invalidating the sidecar "node_id". Change the entire cache to using the "node_id" * Rename parameter from sidecarId to sidecarNodeId Co-authored-by: Othello Maurer --- .../sidecar/rest/requests/BulkActionRequest.java | 6 +++--- .../rest/resources/AdministrationResource.java | 7 +------ .../sidecar/rest/resources/SidecarResource.java | 6 +++--- .../plugins/sidecar/services/ActionService.java | 10 ++++++++-- .../plugins/sidecar/services/EtagService.java | 14 +++++++------- 5 files changed, 22 insertions(+), 21 deletions(-) diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/requests/BulkActionRequest.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/requests/BulkActionRequest.java index 37c3f6f42cfb..594c3218cc51 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/requests/BulkActionRequest.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/requests/BulkActionRequest.java @@ -33,8 +33,8 @@ public abstract class BulkActionRequest { public abstract List collectorIds(); @JsonCreator - public static BulkActionRequest create(@JsonProperty("sidecar_id") String collectorId, + public static BulkActionRequest create(@JsonProperty("sidecar_id") String sidecarId, @JsonProperty("collector_ids") List collectorIds) { - return new AutoValue_BulkActionRequest(collectorId, collectorIds); + return new AutoValue_BulkActionRequest(sidecarId, collectorIds); } -} \ No newline at end of file +} diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/AdministrationResource.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/AdministrationResource.java index 951aa770faa7..32b30f0d7d25 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/AdministrationResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/AdministrationResource.java @@ -44,7 +44,6 @@ import org.graylog.plugins.sidecar.services.ActionService; import org.graylog.plugins.sidecar.services.CollectorService; import org.graylog.plugins.sidecar.services.ConfigurationService; -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; @@ -91,7 +90,6 @@ public class AdministrationResource extends RestResource implements PluginRestRe private final AdministrationFiltersFactory administrationFiltersFactory; private final ActiveSidecarFilter activeSidecarFilter; private final SidecarStatusMapper sidecarStatusMapper; - private final EtagService etagService; @Inject public AdministrationResource(SidecarService sidecarService, @@ -100,8 +98,7 @@ public AdministrationResource(SidecarService sidecarService, ActionService actionService, AdministrationFiltersFactory administrationFiltersFactory, ClusterConfigService clusterConfigService, - SidecarStatusMapper sidecarStatusMapper, - EtagService etagService) { + SidecarStatusMapper sidecarStatusMapper) { final SidecarConfiguration sidecarConfiguration = clusterConfigService.getOrDefault(SidecarConfiguration.class, SidecarConfiguration.defaultConfiguration()); this.sidecarService = sidecarService; this.configurationService = configurationService; @@ -109,7 +106,6 @@ public AdministrationResource(SidecarService sidecarService, this.actionService = actionService; this.administrationFiltersFactory = administrationFiltersFactory; this.sidecarStatusMapper = sidecarStatusMapper; - this.etagService = etagService; this.activeSidecarFilter = new ActiveSidecarFilter(sidecarConfiguration.sidecarInactiveThreshold()); this.searchQueryParser = new SearchQueryParser(Sidecar.FIELD_NODE_NAME, SidecarResource.SEARCH_FIELD_MAPPING); } @@ -170,7 +166,6 @@ public Response setAction(@ApiParam(name = "JSON body", required = true) .collect(Collectors.toList()); final CollectorActions collectorActions = actionService.fromRequest(bulkActionRequest.sidecarId(), actions); actionService.saveAction(collectorActions); - etagService.invalidateRegistration(bulkActionRequest.sidecarId()); } return Response.accepted().build(); diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/SidecarResource.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/SidecarResource.java index 6cfbbc693991..b91e2081dbc3 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/SidecarResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/SidecarResource.java @@ -223,7 +223,7 @@ public Response register(@ApiParam(name = "sidecarId", value = "The id this Side // If the sidecar has the recent registration, return with HTTP 304 if (ifNoneMatch != null) { EntityTag etag = new EntityTag(ifNoneMatch.replaceAll("\"", "")); - if (etagService.registrationIsCached(sidecar.id(), etag.toString())) { + if (etagService.registrationIsCached(sidecar.nodeId(), etag.toString())) { sidecarService.save(sidecar); return Response.notModified().tag(etag).build(); } @@ -247,7 +247,7 @@ public Response register(@ApiParam(name = "sidecarId", value = "The id this Side sidecar.assignments()); // add new etag to cache EntityTag registrationEtag = etagService.buildEntityTagForResponse(sidecarRegistrationResponse); - etagService.addSidecarRegistration(sidecar.id(), registrationEtag.toString()); + etagService.addSidecarRegistration(sidecar.nodeId(), registrationEtag.toString()); return Response.accepted(sidecarRegistrationResponse).tag(registrationEtag).build(); } @@ -273,7 +273,7 @@ public Response assignConfiguration(@ApiParam(name = "JSON body", required = tru try { Sidecar sidecar = sidecarService.applyManualAssignments(nodeId, nodeRelations); sidecarService.save(sidecar); - etagService.invalidateRegistration(sidecar.id()); + etagService.invalidateRegistration(sidecar.nodeId()); } catch (org.graylog2.database.NotFoundException e) { throw new NotFoundException(e.getMessage()); } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ActionService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ActionService.java index 91fd257118a9..9167adb44edd 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ActionService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ActionService.java @@ -35,9 +35,13 @@ public class ActionService { private static final String COLLECTION_NAME = "sidecar_collector_actions"; private final JacksonDBCollection dbCollection; + private final EtagService etagService; + @Inject public ActionService(MongoConnection mongoConnection, - MongoJackObjectMapperProvider mapper){ + MongoJackObjectMapperProvider mapper, + EtagService etagService){ + this.etagService = etagService; dbCollection = JacksonDBCollection.wrap( mongoConnection.getDatabase().getCollection(COLLECTION_NAME), CollectorActions.class, @@ -70,7 +74,7 @@ public CollectorActions fromRequest(String sidecarId, List acti } public CollectorActions saveAction(CollectorActions collectorActions) { - return dbCollection.findAndModify( + final CollectorActions actions = dbCollection.findAndModify( DBQuery.is("sidecar_id", collectorActions.sidecarId()), new BasicDBObject(), new BasicDBObject(), @@ -78,6 +82,8 @@ public CollectorActions saveAction(CollectorActions collectorActions) { collectorActions, true, true); + etagService.invalidateRegistration(collectorActions.sidecarId()); + return actions; } public CollectorActions findActionBySidecar(String sidecarId, boolean remove) { diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagService.java index a833c4761f52..8b8b4ecfc360 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagService.java @@ -120,8 +120,8 @@ public boolean configurationsAreCached(String etag) { return configurationCache.getIfPresent(etag) != null; } - public boolean registrationIsCached(String sidecarId, String etag) { - return etag.equals(registrationCache.getIfPresent(sidecarId)); + public boolean registrationIsCached(String sidecarNodeId, String etag) { + return etag.equals(registrationCache.getIfPresent(sidecarNodeId)); } public void registerCollector(String etag) { @@ -132,8 +132,8 @@ public void registerConfiguration(String etag) { configurationCache.put(etag, Boolean.TRUE); } - public void addSidecarRegistration(String sidecarId, String etag) { - registrationCache.put(sidecarId, etag); + public void addSidecarRegistration(String sidecarNodeId, String etag) { + registrationCache.put(sidecarNodeId, etag); } @@ -152,9 +152,9 @@ public void invalidateAllRegistrations() { clusterEventBus.post(EtagCacheInvalidation.create(CacheContext.REGISTRATION, "")); } - public void invalidateRegistration(String sidecarId) { - registrationCache.invalidate(sidecarId); - clusterEventBus.post(EtagCacheInvalidation.create(CacheContext.REGISTRATION, sidecarId)); + public void invalidateRegistration(String sidecarNodeId) { + registrationCache.invalidate(sidecarNodeId); + clusterEventBus.post(EtagCacheInvalidation.create(CacheContext.REGISTRATION, sidecarNodeId)); } public EntityTag buildEntityTagForResponse(Object o) throws JsonProcessingException { From ac799b11957222caee959ba7e930b3d9afe200f7 Mon Sep 17 00:00:00 2001 From: Othello Maurer Date: Thu, 22 Sep 2022 15:37:01 +0200 Subject: [PATCH 07/82] Invalidate registration cache only for affected sidecars (#13514) * Invalidate registration cache only for affected sidecars * Use node-id for cache invalidation The cache key has been changed since this PR has been started. * Consider OS for cache invalidation Also use symmetric difference of tags to further reduce unnecessary invalidations * Invalidate less when creating new configs --- .../rest/resources/ConfigurationResource.java | 25 ++++++++++++++++--- .../sidecar/services/SidecarService.java | 8 ++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationResource.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationResource.java index 808bfe08e0f0..312937e1d101 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationResource.java @@ -19,6 +19,7 @@ import com.codahale.metrics.annotation.Timed; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -26,6 +27,7 @@ import org.apache.shiro.authz.annotation.RequiresPermissions; import org.graylog.plugins.sidecar.audit.SidecarAuditEventTypes; import org.graylog.plugins.sidecar.permissions.SidecarRestPermissions; +import org.graylog.plugins.sidecar.rest.models.Collector; import org.graylog.plugins.sidecar.rest.models.CollectorUpload; import org.graylog.plugins.sidecar.rest.models.Configuration; import org.graylog.plugins.sidecar.rest.models.ConfigurationSummary; @@ -36,6 +38,7 @@ import org.graylog.plugins.sidecar.rest.responses.ConfigurationListResponse; import org.graylog.plugins.sidecar.rest.responses.ConfigurationPreviewRenderResponse; import org.graylog.plugins.sidecar.rest.responses.ConfigurationSidecarsResponse; +import org.graylog.plugins.sidecar.services.CollectorService; import org.graylog.plugins.sidecar.services.ConfigurationService; import org.graylog.plugins.sidecar.services.EtagService; import org.graylog.plugins.sidecar.services.ImportService; @@ -77,6 +80,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -98,6 +102,7 @@ public class ConfigurationResource extends RestResource implements PluginRestRes private final SidecarService sidecarService; private final EtagService etagService; private final ImportService importService; + private final CollectorService collectorService; private final SearchQueryParser searchQueryParser; private static final ImmutableMap SEARCH_FIELD_MAPPING = ImmutableMap.builder() .put("id", SearchQueryField.create(Configuration.FIELD_ID)) @@ -109,12 +114,14 @@ public class ConfigurationResource extends RestResource implements PluginRestRes public ConfigurationResource(ConfigurationService configurationService, SidecarService sidecarService, EtagService etagService, - ImportService importService) { + ImportService importService, + CollectorService collectorService) { this.configurationService = configurationService; this.sidecarService = sidecarService; this.etagService = etagService; this.importService = importService; - this.searchQueryParser = new SearchQueryParser(Configuration.FIELD_NAME, SEARCH_FIELD_MAPPING);; + this.collectorService = collectorService; + this.searchQueryParser = new SearchQueryParser(Configuration.FIELD_NAME, SEARCH_FIELD_MAPPING); } @GET @@ -282,7 +289,11 @@ public Response createConfiguration(@ApiParam(name = "JSON body", required = tru final Configuration config = configurationService.save(configuration); if (!config.tags().isEmpty()) { - etagService.invalidateAllRegistrations(); + final String os = Optional.ofNullable(collectorService.find(request.collectorId())) + .map(Collector::nodeOperatingSystem).orElse(""); + sidecarService.findByTagsAndOS(config.tags(), os) + .map(Sidecar::nodeId) + .forEach(etagService::invalidateRegistration); } return Response.ok().entity(config).build(); @@ -333,8 +344,14 @@ public Response updateConfiguration(@ApiParam(name = "id", required = true) return Response.status(Response.Status.BAD_REQUEST).entity(validationResult).build(); } etagService.invalidateAllConfigurations(); + if (! previousConfiguration.tags().equals(updatedConfiguration.tags())) { - etagService.invalidateAllRegistrations(); + final Set tags = Sets.symmetricDifference(previousConfiguration.tags(), updatedConfiguration.tags()); + final String os = Optional.ofNullable(collectorService.find(request.collectorId())) + .map(Collector::nodeOperatingSystem).orElse(""); + sidecarService.findByTagsAndOS(tags, os) + .map(Sidecar::nodeId) + .forEach(etagService::invalidateRegistration); } return Response.ok().entity(configurationService.save(updatedConfiguration)).build(); diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java index 4c886ea10007..6871c2aa2cea 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java @@ -49,6 +49,7 @@ import java.util.List; import java.util.Set; import java.util.function.Predicate; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -274,4 +275,11 @@ public List toSummaryList(List sidecars, Predicate collector.toSummary(isActiveFunction)) .collect(Collectors.toList()); } + + public Stream findByTagsAndOS(Collection tags, String os) { + return streamQuery(DBQuery.and( + DBQuery.in("node_details.tags", tags), + DBQuery.regex("node_details.operating_system", Pattern.compile("^" + Pattern.quote(os) + "$", Pattern.CASE_INSENSITIVE)) + )); + } } From 951091138fa461d2281809f472e1914cc8c24ca9 Mon Sep 17 00:00:00 2001 From: Marco Pfatschbacher Date: Mon, 26 Sep 2022 15:06:29 +0200 Subject: [PATCH 08/82] Add description for tag variables --- .../sidecars/configuration-forms/TemplatesHelper.jsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx index 9063e7c07cfe..1032620152f6 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx @@ -54,6 +54,12 @@ class TemplatesHelper extends React.Component { {TemplatesHelper._buildVariableName('spoolDir')} A directory that is unique per configuration and can be used to store collector data. + + {TemplatesHelper._buildVariableName('tags.')} + A map of tags that are set for the sidecar. This can be used to render conditional configuration snippets. e.g.:
+ <#if sidecar.tags.webserver??>
  - /var/log/apache/*.log
</#if>
+ + From d65bbc53e47dff70597af437e26efc830d272838 Mon Sep 17 00:00:00 2001 From: Othello Maurer Date: Thu, 8 Sep 2022 14:14:48 +0200 Subject: [PATCH 09/82] Add "tags" field to DTOs (#13367) --- .../graylog/plugins/sidecar/rest/models/NodeDetails.java | 9 +++++++-- .../graylog/plugins/sidecar/services/SidecarService.java | 9 ++++++--- .../sidecar/collectors/SidecarServiceImplTest.java | 1 + .../sidecar/collectors/rest/SidecarResourceTest.java | 4 ++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java index 202825c47d71..096a1cd1bbd0 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java @@ -50,12 +50,17 @@ public abstract class NodeDetails { @Nullable public abstract CollectorStatusList statusList(); + @JsonProperty("tags") + @Nullable + public abstract List tags(); + @JsonCreator public static NodeDetails create(@JsonProperty("operating_system") String operatingSystem, @JsonProperty("ip") @Nullable String ip, @JsonProperty("metrics") @Nullable NodeMetrics metrics, @JsonProperty("log_file_list") @Nullable List logFileList, - @JsonProperty("status") @Nullable CollectorStatusList statusList) { - return new AutoValue_NodeDetails(operatingSystem, ip, metrics, logFileList, statusList); + @JsonProperty("status") @Nullable CollectorStatusList statusList, + @JsonProperty("tags") @Nullable List tags) { + return new AutoValue_NodeDetails(operatingSystem, ip, metrics, logFileList, statusList, tags); } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java index 5646225fdbd3..080e5b68238a 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java @@ -89,8 +89,9 @@ public Sidecar save(Sidecar sidecar) { } else { throw new IllegalArgumentException("Specified object failed validation: " + violations); } - } else + } else { throw new IllegalArgumentException("Specified object is not of correct implementation type (" + sidecar.getClass() + ")!"); + } } public List all() { @@ -168,7 +169,8 @@ public int markExpired(Period period, String message) { nodeDetails.ip(), nodeDetails.metrics(), nodeDetails.logFileList(), - statusListToSave); + statusListToSave, + nodeDetails.tags()); Sidecar toSave = collector.toBuilder() .nodeDetails(nodeDetailsToSave) @@ -194,7 +196,8 @@ public Sidecar fromRequest(String nodeId, RegistrationRequest request, String co request.nodeDetails().ip(), request.nodeDetails().metrics(), request.nodeDetails().logFileList(), - request.nodeDetails().statusList()), + request.nodeDetails().statusList(), + request.nodeDetails().tags()), collectorVersion); } diff --git a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java index 26f54bca730a..d4357f2dd674 100644 --- a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java +++ b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java @@ -95,6 +95,7 @@ public void testSaveFirstRecord() throws Exception { null, null, null, + null, null), version ); diff --git a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java index 89d0ea731b45..0e8f8076b954 100644 --- a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java +++ b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java @@ -125,6 +125,7 @@ public void testRegister() throws Exception { null, null, null, + null, null ) ); @@ -144,6 +145,7 @@ public void testRegisterInvalidCollectorId() throws Exception { null, null, null, + null, null ) ); @@ -164,6 +166,7 @@ public void testRegisterInvalidNodeId() throws Exception { null, null, null, + null, null ) ); @@ -198,6 +201,7 @@ public void testRegisterMissingOperatingSystem() throws Exception { null, null, null, + null, null ) ); From 80f175da588ce7ae1688ce533fd6d23a59723c81 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Tue, 13 Sep 2022 13:41:45 +0200 Subject: [PATCH 10/82] show CollectorProcessControl as an action button --- .../sidecars/administration/CollectorProcessControl.jsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.jsx index 14c8718b22d9..e60f97989b59 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.jsx @@ -15,6 +15,7 @@ * . */ import React from 'react'; +// eslint-disable-next-line no-restricted-imports import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import lodash from 'lodash'; @@ -25,6 +26,7 @@ import { Pluralize, SelectPopover } from 'components/common'; const PROCESS_ACTIONS = ['start', 'restart', 'stop']; const CollectorProcessControl = createReactClass({ + // eslint-disable-next-line react/no-unused-class-component-methods propTypes: { selectedSidecarCollectorPairs: PropTypes.array.isRequired, onProcessAction: PropTypes.func.isRequired, @@ -37,6 +39,7 @@ const CollectorProcessControl = createReactClass({ }; }, + // eslint-disable-next-line react/sort-comp resetSelectedAction() { this.setState({ selectedAction: undefined }); }, @@ -133,10 +136,8 @@ const CollectorProcessControl = createReactClass({ Process - -)} + + )} items={PROCESS_ACTIONS} itemFormatter={actionFormatter} selectedItems={selectedAction ? [selectedAction] : []} From 3aa38799aa91b5109f154938a173873e7f986ec7 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Tue, 13 Sep 2022 13:43:32 +0200 Subject: [PATCH 11/82] added CollectorConfigurationModal --- .../CollectorConfigurationModal.tsx | 265 ++++++++++++++++++ .../CollectorsAdministrationActions.jsx | 43 ++- 2 files changed, 285 insertions(+), 23 deletions(-) create mode 100644 graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx new file mode 100644 index 000000000000..af8bcaa763ae --- /dev/null +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -0,0 +1,265 @@ +/* eslint-disable react/prop-types */ +/* + * 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 + * . + */ +import { naturalSortIgnoreCase } from 'util/SortUtils'; + +import React from 'react'; +import PropTypes from 'prop-types'; +import lodash from 'lodash'; +import styled from 'styled-components'; + +import { Table, BootstrapModalConfirm, BootstrapModalWrapper, Button, Modal } from 'components/bootstrap'; +import { SearchForm, Icon } from 'components/common'; +import CollectorIndicator from 'components/sidecars/common/CollectorIndicator'; +import ColorLabel from 'components/sidecars/common/ColorLabel'; + +const ConfigurationContainer = styled.div` + overflow: auto; + height: 300px; + margin-top: 8px +`; + +const ConfigurationTable = styled(Table)` + margin-bottom: 0 +`; + +const ConfigurationButton = styled(Button)` + margin-right: 6px +`; + +const TableRow = styled.tr` + cursor: pointer; + border-bottom: 1px solid lightgray; + height: 32px; +`; + +const IconTableCell = styled.td` + width: 32px; +`; + +const CollectorTableCell = styled.td` + width: 140px; + text-align: right; +`; + +const ConfigurationTableCell = styled.td` + flex: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 0; +`; + +const UnselectTableCell = styled.td` + width: 32px; + text-align: center; +`; + +const CollectorConfigurationSelector = (props) => { + const [nextAssignedConfigurations, setNextAssignedConfigurations] = React.useState([]); + const [show, setShow] = React.useState(false); + + const modalConfirm = React.useRef(null); + const onCancel = React.useCallback(() => setShow(false), []); + + const getAssignedConfigurations = (selectedSidecarCollectorPairs, configurations) => { + const assignments = selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar).reduce((accumulator, sidecar) => accumulator.concat(sidecar.assignments), []); + + return assignments.map((assignment) => configurations.find((configuration) => configuration.id === assignment.configuration_id)); + }; + + const onSave = (configurationNames: string[]) => { + setNextAssignedConfigurations(configurationNames); + modalConfirm.current.open(); + }; + + const confirmConfigurationChange = (doneCallback) => { + const { onConfigurationSelectionChange, configurations } = props; + const assignedConfigurationsToSave = configurations.filter((c) => nextAssignedConfigurations.includes(c.name)); + + onConfigurationSelectionChange(assignedConfigurationsToSave, doneCallback); + onCancel(); + }; + + const cancelConfigurationChange = () => { + setNextAssignedConfigurations([]); + }; + + const getConfiguration = (name: string) => { + return props.configurations.find((c) => c.name === name); + }; + + const getCollector = (name: string) => { + const configuration = getConfiguration(name); + + return props.collectors.find((b) => b.id === configuration.collector_id); + }; + + const renderConfigurationSummary = (_previousAssignedConfigurations, _nextAssignedConfigurations, _selectedSidecarCollectorPairs) => { + const toAdd = lodash.difference(_nextAssignedConfigurations, _previousAssignedConfigurations); + const toRemove = lodash.difference(_previousAssignedConfigurations, _nextAssignedConfigurations); + console.log(toAdd, toRemove); + const exampleSidecarCollectorPair = _selectedSidecarCollectorPairs[0]; + const collectorIndicator = ( + + + + ); + + let toAddSummary; + let toRemoveSummary; + + if (toRemove.length > 0) { + toAddSummary =

You are going to remove the configuration {toRemove.join(', ')} for collector {collectorIndicator}

; + } + + if (toAdd.length > 0) { + toRemoveSummary =

You are going to apply the configuration {toAdd.join(', ')} for collector {collectorIndicator}

; + } + + const formattedSummary = _selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name).join(', '); + + return ( + +
+ {toAddSummary} + {toRemoveSummary} +

Are you sure you want to proceed with this action for {formattedSummary}?

+
+
+ ); + }; + + const { configurations, selectedSidecarCollectorPairs } = props; + + // Do not allow configuration changes when more than one log collector type is selected + const selectedLogCollectors = lodash.uniq(selectedSidecarCollectorPairs.map(({ collector }) => collector)) as any[]; + + const assignedConfigurations = getAssignedConfigurations(selectedSidecarCollectorPairs, configurations) + .filter((configuration) => selectedLogCollectors[0].id === configuration.collector_id) + .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) + .map((config) => config.name); + + const nonAssignedConfigurations = configurations.filter((c) => !assignedConfigurations.includes(c.name)); + + const MemoizedModalForm = React.useMemo(() => { + // eslint-disable-next-line react/no-unstable-nested-components + const ModalForm = () => { + const [searchQuery, setSearchQuery] = React.useState(''); + const [selectedConfigurations, setSelectedConfigurations] = React.useState(assignedConfigurations); + + const isSelected = (name: string) => selectedConfigurations.includes(name); + + const options = nonAssignedConfigurations + .filter((configuration) => (selectedLogCollectors[0].id === configuration.collector_id)) + .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) + .map((c) => c.name); + + const filteredOptions = [...assignedConfigurations, ...options].filter((configuration) => configuration.match(new RegExp(searchQuery, 'i'))); + + const rows = filteredOptions.map((option) => { + const selected = isSelected(option); + const config = getConfiguration(option); + const collector = getCollector(option); + + return ( + setSelectedConfigurations(selected ? selectedConfigurations.filter((c) => c !== option) : [...selectedConfigurations, option])}> + {selected && } + + {option} + + + {collector + ? + : Unknown collector} + + + {selected && } + + ); + }); + + return ( + + + Configure + + + setSearchQuery(q)} topMargin={0} queryWidth="100%" /> + + + + {rows} + + + + + + + + + + ); + }; + + return ; + }, [show, onCancel]); + + return ( + <> + setShow(true)}>Configure + {MemoizedModalForm} + {renderConfigurationSummary(assignedConfigurations, nextAssignedConfigurations, selectedSidecarCollectorPairs)} + + ); + + // if (selectedLogCollectors.length > 1) { + // return ( + // Configure } + // items={[`Cannot change configurations of ${selectedLogCollectors.map((collector) => collector.name).join(', ')} collectors simultaneously`]} + // displayDataFilter={false} + // disabled /> + // ); + // } + + // if (configurationIds.length === 0) { + // return ( + // Configure } + // items={['No configurations available for the selected log collector']} + // displayDataFilter={false} + // disabled /> + // ); + // } +}; + +CollectorConfigurationSelector.propTypes = { + collectors: PropTypes.array.isRequired, + configurations: PropTypes.array.isRequired, + selectedSidecarCollectorPairs: PropTypes.array.isRequired, + onConfigurationSelectionChange: PropTypes.func.isRequired, +}; + +export default CollectorConfigurationSelector; diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx index 20b41a1a937d..16f14bed2e40 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx @@ -15,36 +15,33 @@ * . */ import React from 'react'; -import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { ButtonToolbar } from 'components/bootstrap'; -import CollectorConfigurationSelector from './CollectorConfigurationSelector'; +import CollectorConfigurationModal from './CollectorConfigurationModal'; import CollectorProcessControl from './CollectorProcessControl'; -const CollectorsAdministrationActions = createReactClass({ - propTypes: { - collectors: PropTypes.array.isRequired, - configurations: PropTypes.array.isRequired, - selectedSidecarCollectorPairs: PropTypes.array.isRequired, - onConfigurationSelectionChange: PropTypes.func.isRequired, - onProcessAction: PropTypes.func.isRequired, - }, +const CollectorsAdministrationActions = (props) => { + const { collectors, configurations, selectedSidecarCollectorPairs, onConfigurationSelectionChange, onProcessAction } = props; - render() { - const { collectors, configurations, selectedSidecarCollectorPairs, onConfigurationSelectionChange, onProcessAction } = this.props; + return ( + + + + + ); +}; - return ( - - - - - ); - }, -}); +CollectorsAdministrationActions.propTypes = { + collectors: PropTypes.array.isRequired, + configurations: PropTypes.array.isRequired, + selectedSidecarCollectorPairs: PropTypes.array.isRequired, + onConfigurationSelectionChange: PropTypes.func.isRequired, + onProcessAction: PropTypes.func.isRequired, +}; export default CollectorsAdministrationActions; From cc507a32e0519b6e4c3371dfc815da4c7b6cc889 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Tue, 13 Sep 2022 13:45:50 +0200 Subject: [PATCH 12/82] made search button in SearchForm optional --- .../src/components/common/SearchForm.jsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/graylog2-web-interface/src/components/common/SearchForm.jsx b/graylog2-web-interface/src/components/common/SearchForm.jsx index 468e0ef2df2f..a9c96731fb14 100644 --- a/graylog2-web-interface/src/components/common/SearchForm.jsx +++ b/graylog2-web-interface/src/components/common/SearchForm.jsx @@ -62,7 +62,7 @@ class SearchForm extends React.Component { * Callback when a search was submitted. The function receives the query * and a callback to reset the loading state of the form as arguments. */ - onSearch: PropTypes.func.isRequired, + onSearch: PropTypes.func, /** Callback when the input was reset. The function is called with no arguments. */ onReset: PropTypes.func, /** Search field label. */ @@ -120,6 +120,7 @@ class SearchForm extends React.Component { query: '', className: '', onQueryChange: () => {}, + onSearch: null, onReset: null, label: null, placeholder: 'Enter search query...', @@ -227,6 +228,7 @@ class SearchForm extends React.Component { buttonLeftMargin, label, onReset, + onSearch, wrapperClass, topMargin, searchButtonLabel, @@ -239,7 +241,7 @@ class SearchForm extends React.Component {
-
+
{label && (
- + {onSearch && ( + + )} {onReset && (
From 353c1132428f6c7650b34507ab63e35c46cfad9a Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Thu, 15 Sep 2022 11:27:57 +0200 Subject: [PATCH 14/82] handle edge case very long config names --- .../administration/CollectorConfigurationModal.tsx | 9 ++++++--- .../src/components/sidecars/common/ColorLabel.tsx | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index af8bcaa763ae..02582cfb0a1b 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -41,6 +41,10 @@ const ConfigurationButton = styled(Button)` margin-right: 6px `; +const ConfigurationSummary = styled.div` + word-break: break-all; +`; + const TableRow = styled.tr` cursor: pointer; border-bottom: 1px solid lightgray; @@ -112,7 +116,6 @@ const CollectorConfigurationSelector = (props) => { const renderConfigurationSummary = (_previousAssignedConfigurations, _nextAssignedConfigurations, _selectedSidecarCollectorPairs) => { const toAdd = lodash.difference(_nextAssignedConfigurations, _previousAssignedConfigurations); const toRemove = lodash.difference(_previousAssignedConfigurations, _nextAssignedConfigurations); - console.log(toAdd, toRemove); const exampleSidecarCollectorPair = _selectedSidecarCollectorPairs[0]; const collectorIndicator = ( @@ -139,11 +142,11 @@ const CollectorConfigurationSelector = (props) => { title="Configuration summary" onConfirm={confirmConfigurationChange} onCancel={cancelConfigurationChange}> -
+ {toAddSummary} {toRemoveSummary}

Are you sure you want to proceed with this action for {formattedSummary}?

-
+ ); }; diff --git a/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx b/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx index e8df1a35bd99..79af296f437a 100644 --- a/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx +++ b/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx @@ -41,6 +41,7 @@ const ColorLabelWrap = styled.span(({ size, theme }: ColorLabelWrapProps) => { const fontSize = size === 'small' ? small : body; return css` + display: flex; vertical-align: middle; font-size: ${size === 'xsmall' ? tiny : fontSize}; `; @@ -56,6 +57,8 @@ const ColorLabel = ({ color, size, text, theme }: ColorLabelProps) => { backgroundColor: color, border: `1px solid ${borderColor}`, color: textColor, + overflow: 'hidden', + textOverflow: 'ellipsis', }}> {text} From 35b06b30aa0cdf2583adf5e2307728d466cf26da Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Thu, 15 Sep 2022 16:22:45 +0200 Subject: [PATCH 15/82] handle edge cases when its not possible to configure --- .../CollectorConfigurationModal.tsx | 53 +++++++++---------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 02582cfb0a1b..b585e0e23547 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -41,6 +41,11 @@ const ConfigurationButton = styled(Button)` margin-right: 6px `; +const NoConfigurationMessage = styled.div` + display: flex; + justify-content: center; +`; + const ConfigurationSummary = styled.div` word-break: break-all; `; @@ -171,6 +176,8 @@ const CollectorConfigurationSelector = (props) => { const isSelected = (name: string) => selectedConfigurations.includes(name); + const isNotDirty = lodash.isEqual(selectedConfigurations, assignedConfigurations); + const options = nonAssignedConfigurations .filter((configuration) => (selectedLogCollectors[0].id === configuration.collector_id)) .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) @@ -209,16 +216,20 @@ const CollectorConfigurationSelector = (props) => { setSearchQuery(q)} topMargin={0} queryWidth="100%" /> - - - {rows} - - + {(rows.length === 0) ? ( + No configurations available for the selected log collector. + ) : ( + + + {rows} + + + )} - + ); @@ -229,33 +240,17 @@ const CollectorConfigurationSelector = (props) => { return ( <> - setShow(true)}>Configure + 1) ? `Cannot change configurations of ${selectedLogCollectors.map((c) => c.name).join(', ')} collectors simultaneously` : undefined} + bsStyle="primary" + bsSize="small" + disabled={selectedLogCollectors.length !== 1} + onClick={() => setShow(true)}> + Configure + {MemoizedModalForm} {renderConfigurationSummary(assignedConfigurations, nextAssignedConfigurations, selectedSidecarCollectorPairs)} ); - - // if (selectedLogCollectors.length > 1) { - // return ( - // Configure } - // items={[`Cannot change configurations of ${selectedLogCollectors.map((collector) => collector.name).join(', ')} collectors simultaneously`]} - // displayDataFilter={false} - // disabled /> - // ); - // } - - // if (configurationIds.length === 0) { - // return ( - // Configure } - // items={['No configurations available for the selected log collector']} - // displayDataFilter={false} - // disabled /> - // ); - // } }; CollectorConfigurationSelector.propTypes = { From 1cea93288efdef26aa3d6f9f36098aa0e640d7de Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Sun, 18 Sep 2022 13:37:17 +0200 Subject: [PATCH 16/82] change config list styling --- .../sidecars/administration/CollectorsAdministration.css | 6 +++--- .../src/components/sidecars/common/ColorLabel.tsx | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.css b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.css index e3c359aaa15a..a3bafcab70de 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.css +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.css @@ -21,9 +21,9 @@ } :local(.additionalContent) { - display: block; - height: 20px; - margin: 5px 0; + display: flex; + margin-top: 5px; + flex-wrap: wrap; } :local(.paginatedList) .page-size { diff --git a/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx b/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx index 79af296f437a..77408fdb1240 100644 --- a/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx +++ b/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx @@ -59,6 +59,9 @@ const ColorLabel = ({ color, size, text, theme }: ColorLabelProps) => { color: textColor, overflow: 'hidden', textOverflow: 'ellipsis', + maxWidth: '128px', + marginRight: '4px', + marginBottom: '4px', }}> {text} From 0aa3e66a5c2609163128d530ae87326c135a631e Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Sun, 18 Sep 2022 13:38:46 +0200 Subject: [PATCH 17/82] make CollectorConfigurationModal a reusable component --- .../CollectorConfigurationModal.tsx | 90 ++++++++++++------- .../CollectorsAdministration.jsx | 43 +++++---- 2 files changed, 84 insertions(+), 49 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index b585e0e23547..420baa7061ae 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -22,14 +22,16 @@ import PropTypes from 'prop-types'; import lodash from 'lodash'; import styled from 'styled-components'; +import Routes from 'routing/Routes'; import { Table, BootstrapModalConfirm, BootstrapModalWrapper, Button, Modal } from 'components/bootstrap'; import { SearchForm, Icon } from 'components/common'; import CollectorIndicator from 'components/sidecars/common/CollectorIndicator'; import ColorLabel from 'components/sidecars/common/ColorLabel'; +import { Link } from 'components/common/router'; const ConfigurationContainer = styled.div` overflow: auto; - height: 300px; + height: 360px; margin-top: 8px `; @@ -37,13 +39,15 @@ const ConfigurationTable = styled(Table)` margin-bottom: 0 `; -const ConfigurationButton = styled(Button)` - margin-right: 6px +const NoConfigurationMessage = styled.div` + display: flex; + justify-content: center; `; -const NoConfigurationMessage = styled.div` +const AddNewConfiguration = styled.div` display: flex; justify-content: center; + align-items: center; `; const ConfigurationSummary = styled.div` @@ -56,6 +60,12 @@ const TableRow = styled.tr` height: 32px; `; +const StickyTableRowFooter = styled.tr` + height: 34px; + position: sticky; + bottom: 0; +`; + const IconTableCell = styled.td` width: 32px; `; @@ -78,12 +88,15 @@ const UnselectTableCell = styled.td` text-align: center; `; -const CollectorConfigurationSelector = (props) => { +const ModalTitle = styled(Modal.Title)` + font-size: 1.266rem !important; + line-height: 1.1; +`; + +const CollectorConfigurationModal = (props) => { const [nextAssignedConfigurations, setNextAssignedConfigurations] = React.useState([]); - const [show, setShow] = React.useState(false); const modalConfirm = React.useRef(null); - const onCancel = React.useCallback(() => setShow(false), []); const getAssignedConfigurations = (selectedSidecarCollectorPairs, configurations) => { const assignments = selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar).reduce((accumulator, sidecar) => accumulator.concat(sidecar.assignments), []); @@ -97,7 +110,7 @@ const CollectorConfigurationSelector = (props) => { }; const confirmConfigurationChange = (doneCallback) => { - const { onConfigurationSelectionChange, configurations } = props; + const { onConfigurationSelectionChange, configurations, onCancel } = props; const assignedConfigurationsToSave = configurations.filter((c) => nextAssignedConfigurations.includes(c.name)); onConfigurationSelectionChange(assignedConfigurationsToSave, doneCallback); @@ -122,12 +135,12 @@ const CollectorConfigurationSelector = (props) => { const toAdd = lodash.difference(_nextAssignedConfigurations, _previousAssignedConfigurations); const toRemove = lodash.difference(_previousAssignedConfigurations, _nextAssignedConfigurations); const exampleSidecarCollectorPair = _selectedSidecarCollectorPairs[0]; - const collectorIndicator = ( + const collectorIndicator = exampleSidecarCollectorPair ? ( - ); + ) : null; let toAddSummary; let toRemoveSummary; @@ -162,7 +175,7 @@ const CollectorConfigurationSelector = (props) => { const selectedLogCollectors = lodash.uniq(selectedSidecarCollectorPairs.map(({ collector }) => collector)) as any[]; const assignedConfigurations = getAssignedConfigurations(selectedSidecarCollectorPairs, configurations) - .filter((configuration) => selectedLogCollectors[0].id === configuration.collector_id) + .filter((configuration) => selectedLogCollectors[0]?.id === configuration.collector_id) .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) .map((config) => config.name); @@ -179,7 +192,7 @@ const CollectorConfigurationSelector = (props) => { const isNotDirty = lodash.isEqual(selectedConfigurations, assignedConfigurations); const options = nonAssignedConfigurations - .filter((configuration) => (selectedLogCollectors[0].id === configuration.collector_id)) + .filter((configuration) => (selectedLogCollectors[0]?.id === configuration.collector_id)) .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) .map((c) => c.name); @@ -209,26 +222,40 @@ const CollectorConfigurationSelector = (props) => { }); return ( - + - Configure + + Edit {selectedLogCollectors[0]?.name} Configurations +
sidecars: {selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name).join(', ')}
+
setSearchQuery(q)} topMargin={0} queryWidth="100%" /> - {(rows.length === 0) ? ( - No configurations available for the selected log collector. - ) : ( - - - {rows} - - - )} + + + {(rows.length === 0) ? ( + + + No configurations available for the selected log collector. + + + ) : ( + rows + )} + + + +  Add a new configuration + + + + + - +
@@ -236,28 +263,23 @@ const CollectorConfigurationSelector = (props) => { }; return ; - }, [show, onCancel]); + }, [props.show]); return ( <> - 1) ? `Cannot change configurations of ${selectedLogCollectors.map((c) => c.name).join(', ')} collectors simultaneously` : undefined} - bsStyle="primary" - bsSize="small" - disabled={selectedLogCollectors.length !== 1} - onClick={() => setShow(true)}> - Configure - {MemoizedModalForm} {renderConfigurationSummary(assignedConfigurations, nextAssignedConfigurations, selectedSidecarCollectorPairs)} ); }; -CollectorConfigurationSelector.propTypes = { +CollectorConfigurationModal.propTypes = { collectors: PropTypes.array.isRequired, configurations: PropTypes.array.isRequired, selectedSidecarCollectorPairs: PropTypes.array.isRequired, onConfigurationSelectionChange: PropTypes.func.isRequired, + show: PropTypes.bool.isRequired, + onCancel: PropTypes.func.isRequired, }; -export default CollectorConfigurationSelector; +export default CollectorConfigurationModal; diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx index 6df5ce0008c9..d24597f5ced8 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx @@ -21,10 +21,11 @@ import PropTypes from 'prop-types'; import lodash from 'lodash'; import styled, { css } from 'styled-components'; +import { naturalSortIgnoreCase } from 'util/SortUtils'; import { Link } from 'components/common/router'; -import { ControlledTableList, PaginatedList } from 'components/common'; +import { ControlledTableList, PaginatedList, Icon } from 'components/common'; import Routes from 'routing/Routes'; -import { Col, Row, Input } from 'components/bootstrap'; +import { Col, Row, Input, Button } from 'components/bootstrap'; import ColorLabel from 'components/sidecars/common/ColorLabel'; import OperatingSystemIcon from 'components/sidecars/common/OperatingSystemIcon'; import SidecarSearchForm from 'components/sidecars/common/SidecarSearchForm'; @@ -33,6 +34,7 @@ import commonStyle from 'components/sidecars/common/CommonSidecarStyles.css'; import CollectorsAdministrationActions from './CollectorsAdministrationActions'; import CollectorsAdministrationFilters from './CollectorsAdministrationFilters'; +import CollectorConfigurationModal from './CollectorConfigurationModal'; import FiltersSummary from './FiltersSummary'; import style from './CollectorsAdministration.css'; @@ -72,6 +74,7 @@ const CollectorsAdministration = createReactClass({ return { enabledCollectors: this.getEnabledCollectors(sidecarCollectorPairs), selected: [], + showConfigurationModal: false, }; }, @@ -144,15 +147,11 @@ const CollectorsAdministration = createReactClass({ onProcessAction(action, selectedCollectors, doneCallback); }, - formatHeader() { - const { collectors, configurations, sidecarCollectorPairs } = this.props; + formatHeader(selectedSidecarCollectorPairs) { + const { collectors, configurations } = this.props; const { selected, enabledCollectors } = this.state; const selectedItems = selected.length; - const selectedSidecarCollectorPairs = selected.map((selectedSidecarCollectorId) => { - return sidecarCollectorPairs.find(({ sidecar, collector }) => this.sidecarCollectorId(sidecar, collector) === selectedSidecarCollectorId); - }); - let headerMenu; if (selectedItems === 0) { @@ -241,8 +240,8 @@ const CollectorsAdministration = createReactClass({ formatCollector(sidecar, collector, configurations) { const sidecarCollectorId = this.sidecarCollectorId(sidecar, collector); - const configAssignment = sidecar.assignments.find((assignment) => assignment.collector_id === collector.id) || {}; - const configuration = configurations.find((config) => config.id === configAssignment.configuration_id); + const configAssignmentIDs = sidecar.assignments.filter((assignment) => assignment.collector_id === collector.id).map((assignment) => assignment.configuration_id); + const configAssignments = configurations.filter((config) => configAssignmentIDs.includes(config.id)).sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)); const { selected } = this.state; let collectorStatus = { status: null, message: null, id: null }; @@ -262,7 +261,7 @@ const CollectorsAdministration = createReactClass({ return ( - + - {configuration && ( + {(configAssignments.length > 0) && ( - + - {configuration && } + {configAssignments.map((configuration) => )} + ); @@ -330,6 +332,11 @@ const CollectorsAdministration = createReactClass({ render() { const { configurations, collectors, onPageChange, pagination, query, sidecarCollectorPairs, filters } = this.props; + const { selected, showConfigurationModal } = this.state; + + const selectedSidecarCollectorPairs = selected.map((selectedSidecarCollectorId) => { + return sidecarCollectorPairs.find(({ sidecar, collector }) => this.sidecarCollectorId(sidecar, collector) === selectedSidecarCollectorId); + }); let formattedCollectors; @@ -365,12 +372,18 @@ const CollectorsAdministration = createReactClass({ - {this.formatHeader()} + {this.formatHeader(selectedSidecarCollectorPairs)} {formattedCollectors} + this.setState({ selected: [], showConfigurationModal: false })} />
); }, From 0c37116d725946773f4d0d5cd0ca72683ce9f781 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Sun, 18 Sep 2022 13:39:42 +0200 Subject: [PATCH 18/82] added CollectorConfigurationModal in CollectorsAdministrationActions --- .../CollectorsAdministrationActions.jsx | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx index 16f14bed2e40..dbf5da6c139f 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx @@ -16,21 +16,41 @@ */ import React from 'react'; import PropTypes from 'prop-types'; +import lodash from 'lodash'; +import styled from 'styled-components'; -import { ButtonToolbar } from 'components/bootstrap'; +import { ButtonToolbar, Button } from 'components/bootstrap'; +import { Icon } from 'components/common'; import CollectorConfigurationModal from './CollectorConfigurationModal'; import CollectorProcessControl from './CollectorProcessControl'; +const ConfigurationButton = styled(Button)` + margin-right: 6px +`; + const CollectorsAdministrationActions = (props) => { + const [showConfigurationModal, setShowConfigurationModal] = React.useState(false); + const onCancelConfigurationModal = React.useCallback(() => setShowConfigurationModal(false), []); + const { collectors, configurations, selectedSidecarCollectorPairs, onConfigurationSelectionChange, onProcessAction } = props; + const selectedLogCollectorsNames = lodash.uniq(selectedSidecarCollectorPairs.map(({ collector }) => collector.name)); return ( + 1) ? `Cannot change configurations of ${selectedLogCollectorsNames.join(', ')} collectors simultaneously` : undefined} + bsStyle="primary" + bsSize="small" + disabled={selectedLogCollectorsNames.length !== 1} + onClick={() => setShowConfigurationModal(true)}> + Edit Configurations + + onConfigurationSelectionChange={onConfigurationSelectionChange} + show={showConfigurationModal} + onCancel={onCancelConfigurationModal} /> ); From b4b79c000b488d5b3e3cbc12f45fb38c4b7cc7d2 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Mon, 19 Sep 2022 10:54:39 +0200 Subject: [PATCH 19/82] icon btn as edit btn progress --- .../sidecars/administration/CollectorsAdministration.jsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx index d24597f5ced8..4ae8a93ed51d 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx @@ -23,9 +23,9 @@ import styled, { css } from 'styled-components'; import { naturalSortIgnoreCase } from 'util/SortUtils'; import { Link } from 'components/common/router'; -import { ControlledTableList, PaginatedList, Icon } from 'components/common'; +import { ControlledTableList, PaginatedList, IconButton } from 'components/common'; import Routes from 'routing/Routes'; -import { Col, Row, Input, Button } from 'components/bootstrap'; +import { Col, Row, Input } from 'components/bootstrap'; import ColorLabel from 'components/sidecars/common/ColorLabel'; import OperatingSystemIcon from 'components/sidecars/common/OperatingSystemIcon'; import SidecarSearchForm from 'components/sidecars/common/SidecarSearchForm'; @@ -280,11 +280,12 @@ const CollectorsAdministration = createReactClass({ + {(configAssignments.length > 0) && this.setState({ selected: [sidecarCollectorId], showConfigurationModal: true })} />} {configAssignments.map((configuration) => )} - + */} ); From ecacf682ea9c9924022a7d7ca2623c182d6ba549 Mon Sep 17 00:00:00 2001 From: Marco Pfatschbacher Date: Mon, 19 Sep 2022 16:03:03 +0200 Subject: [PATCH 20/82] Sidecar tagged assignments (#13409) * add assignedFromTags property to assignment * Don't fail for unknown properties on NodeDetails This makes it easier to extend the sidecar in the future * Create sidecar config assignments based on tags This is a first naive approach. Every registration request will rebuild the assignments and this is probably too expensive. * fix test * Fix more Tests * Fine grained EtagService Use three different caches for configs, collectors and assignments. * Add Etag caching for UpdateRegistration results The response to the registration PUT request contains configuration assignments, which can be cached as well. This requires us to invalidate the sidecar etag cache in more situations: - Configration assignments - User triggered collector actions (stop, start, restart) * Cleanup old assignment caching commit * optimize * fix after merge * replace deprecated notempty annotation * cache assignments per sidecar id using just the md5 of an assignment result might leed to sidecars missing out on updates. An assignment is always meant for a single sidecar, caching the entire assignment over all sidecars is wrong. A per sidecar cache entry allows more fine grained cache invalidations. Bump the cacheMaxSize to 5000 entries, which is a more meaningful number, considered that this might have to hold all sidecars. * Refactor sidecar register() call. Only update the tagged assignments when we miss the cache. * change cache naming * more refactoring * improve cache invalidation * use fancy java Co-authored-by: Othello Maurer * Add more cases for tag invalidation and fix tests * add tags to Config summary * Generate EntityTags with a better hash algorithm As @thll noticed, Object.hashCode() is prone to having collisions * Handle manual assignments via API * rename * Make tags available as config variables. This allows configs to be written with conditionals. E.g.: ``` <#if sidecar.tags.apache??> - /var/log/apache/*.log ``` * dont lose tags on UI configuration updates Co-authored-by: Othello Maurer --- .../common/SidecarPluginConfiguration.java | 2 +- .../sidecar/rest/models/Configuration.java | 23 +++- .../rest/models/ConfigurationSummary.java | 13 +- .../sidecar/rest/models/NodeDetails.java | 5 +- .../plugins/sidecar/rest/models/Sidecar.java | 4 +- .../requests/ConfigurationAssignment.java | 11 +- .../resources/AdministrationResource.java | 7 +- .../rest/resources/CollectorResource.java | 27 ++-- .../rest/resources/ConfigurationResource.java | 37 +++--- .../ConfigurationVariableResource.java | 4 +- .../rest/resources/SidecarResource.java | 59 ++++++--- .../services/ConfigurationService.java | 29 ++++- .../services/EtagCacheInvalidation.java | 11 +- .../plugins/sidecar/services/EtagService.java | 118 +++++++++++++++--- .../sidecar/services/SidecarService.java | 74 ++++++++--- .../SidecarCollectorConfigurationFacade.java | 5 +- .../collectors/ConfigurationServiceTest.java | 4 +- .../collectors/SidecarServiceImplTest.java | 5 + .../collectors/rest/SidecarResourceTest.java | 43 ++++--- .../configuration-forms/ConfigurationForm.jsx | 1 + 20 files changed, 343 insertions(+), 139 deletions(-) diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/common/SidecarPluginConfiguration.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/common/SidecarPluginConfiguration.java index a3c54a8aa141..4f880c84a172 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/common/SidecarPluginConfiguration.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/common/SidecarPluginConfiguration.java @@ -33,7 +33,7 @@ public class SidecarPluginConfiguration implements PluginConfigBean { private Duration cacheTime = Duration.hours(1L); @Parameter(value = PREFIX + "cache_max_size", validator = PositiveIntegerValidator.class) - private int cacheMaxSize = 100; + private int cacheMaxSize = 5000; public Duration getCacheTime() { return cacheTime; diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/Configuration.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/Configuration.java index ff7238efee70..78f0434a971e 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/Configuration.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/Configuration.java @@ -25,6 +25,7 @@ import org.mongojack.ObjectId; import javax.annotation.Nullable; +import java.util.Set; @AutoValue @WithBeanGetter @@ -35,6 +36,7 @@ public abstract class Configuration { public static final String FIELD_NAME = "name"; public static final String FIELD_COLOR = "color"; public static final String FIELD_TEMPLATE = "template"; + public static final String FIELD_TAGS = "tags"; @Id @ObjectId @@ -54,30 +56,37 @@ public abstract class Configuration { @JsonProperty(FIELD_TEMPLATE) public abstract String template(); + @JsonProperty(FIELD_TAGS) + public abstract Set tags(); + @JsonCreator public static Configuration create(@JsonProperty(FIELD_ID) String id, @JsonProperty(FIELD_COLLECTOR_ID) String collectorId, @JsonProperty(FIELD_NAME) String name, @JsonProperty(FIELD_COLOR) String color, - @JsonProperty(FIELD_TEMPLATE) String template) { + @JsonProperty(FIELD_TEMPLATE) String template, + @JsonProperty(FIELD_TAGS) @Nullable Set tags) { return builder() .id(id) .collectorId(collectorId) .name(name) .color(color) .template(template) + .tags(tags == null ? Set.of() : tags) .build(); } - public static Configuration create(String collectorId, - String name, - String color, - String template) { + public static Configuration createWithoutId(String collectorId, + String name, + String color, + String template, + Set tags) { return create(new org.bson.types.ObjectId().toHexString(), collectorId, name, color, - template); + template, + tags); } public static Builder builder() { @@ -98,6 +107,8 @@ public abstract static class Builder { public abstract Builder template(String template); + public abstract Builder tags(Set tags); + public abstract Configuration build(); } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/ConfigurationSummary.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/ConfigurationSummary.java index 4a1f407fb0e9..7d88d65e5baf 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/ConfigurationSummary.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/ConfigurationSummary.java @@ -22,6 +22,8 @@ import org.mongojack.Id; import org.mongojack.ObjectId; +import java.util.Set; + @AutoValue public abstract class ConfigurationSummary { @JsonProperty("id") @@ -38,12 +40,16 @@ public abstract class ConfigurationSummary { @JsonProperty("color") public abstract String color(); + @JsonProperty("tags") + public abstract Set tags(); + @JsonCreator public static ConfigurationSummary create(@JsonProperty("id") @Id @ObjectId String id, @JsonProperty("name") String name, @JsonProperty("collector_id") String collectorId, - @JsonProperty("color") String color) { - return new AutoValue_ConfigurationSummary(id, name, collectorId, color); + @JsonProperty("color") String color, + @JsonProperty("tags") Set tags) { + return new AutoValue_ConfigurationSummary(id, name, collectorId, color, tags); } public static ConfigurationSummary create(Configuration configuration) { @@ -51,7 +57,8 @@ public static ConfigurationSummary create(Configuration configuration) { configuration.id(), configuration.name(), configuration.collectorId(), - configuration.color()); + configuration.color(), + configuration.tags()); } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java index 8439b69f5cc9..9418b7faeab4 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/NodeDetails.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.auto.value.AutoValue; @@ -29,6 +30,7 @@ @AutoValue @JsonAutoDetect +@JsonIgnoreProperties(ignoreUnknown = true) public abstract class NodeDetails { @JsonProperty("operating_system") @NotNull @@ -52,7 +54,6 @@ public abstract class NodeDetails { public abstract CollectorStatusList statusList(); @JsonProperty("tags") - @Nullable public abstract Set tags(); @JsonProperty("collector_configuration_directory") @@ -67,6 +68,6 @@ public static NodeDetails create(@JsonProperty("operating_system") String operat @JsonProperty("status") @Nullable CollectorStatusList statusList, @JsonProperty("tags") @Nullable Set tags, @JsonProperty("collector_configuration_directory") @Nullable String configDir) { - return new AutoValue_NodeDetails(operatingSystem, ip, metrics, logFileList, statusList, tags, configDir); + return new AutoValue_NodeDetails(operatingSystem, ip, metrics, logFileList, statusList, tags == null ? Set.of() : tags, configDir); } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/Sidecar.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/Sidecar.java index 76777511cb5d..bfb4909d597c 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/Sidecar.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/models/Sidecar.java @@ -92,7 +92,6 @@ public static Status fromString(String statusString) { public abstract NodeDetails nodeDetails(); @JsonProperty - @Nullable public abstract List assignments(); @JsonProperty @@ -133,7 +132,7 @@ public static Sidecar create(@JsonProperty(FIELD_ID) @Id @ObjectId String id, .nodeId(nodeId) .nodeName(nodeName) .nodeDetails(nodeDetails) - .assignments(assignments) + .assignments(assignments == null ? List.of() : assignments) .sidecarVersion(sidecarVersion) .lastSeen(lastSeen) .build(); @@ -151,6 +150,7 @@ public static Sidecar create(@JsonProperty(FIELD_NODE_ID) String nodeId, .nodeDetails(nodeDetails) .sidecarVersion(sidecarVersion) .lastSeen(DateTime.now(DateTimeZone.UTC)) + .assignments(List.of()) .build(); } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/requests/ConfigurationAssignment.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/requests/ConfigurationAssignment.java index 1aa264919aaa..ee14796e310e 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/requests/ConfigurationAssignment.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/requests/ConfigurationAssignment.java @@ -21,6 +21,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.google.auto.value.AutoValue; +import javax.annotation.Nullable; +import java.util.Set; + @AutoValue @JsonAutoDetect public abstract class ConfigurationAssignment { @@ -30,9 +33,13 @@ public abstract class ConfigurationAssignment { @JsonProperty public abstract String configurationId(); + @JsonProperty + public abstract Set assignedFromTags(); + @JsonCreator public static ConfigurationAssignment create(@JsonProperty("collector_id") String collectorId, - @JsonProperty("configuration_id") String configurationId) { - return new AutoValue_ConfigurationAssignment(collectorId, configurationId); + @JsonProperty("configuration_id") String configurationId, + @JsonProperty("assigned_from_tags") @Nullable Set assignedFromTags) { + return new AutoValue_ConfigurationAssignment(collectorId, configurationId, assignedFromTags == null ? Set.of() : assignedFromTags); } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/AdministrationResource.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/AdministrationResource.java index 32b30f0d7d25..951aa770faa7 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/AdministrationResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/AdministrationResource.java @@ -44,6 +44,7 @@ import org.graylog.plugins.sidecar.services.ActionService; import org.graylog.plugins.sidecar.services.CollectorService; import org.graylog.plugins.sidecar.services.ConfigurationService; +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; @@ -90,6 +91,7 @@ public class AdministrationResource extends RestResource implements PluginRestRe private final AdministrationFiltersFactory administrationFiltersFactory; private final ActiveSidecarFilter activeSidecarFilter; private final SidecarStatusMapper sidecarStatusMapper; + private final EtagService etagService; @Inject public AdministrationResource(SidecarService sidecarService, @@ -98,7 +100,8 @@ public AdministrationResource(SidecarService sidecarService, ActionService actionService, AdministrationFiltersFactory administrationFiltersFactory, ClusterConfigService clusterConfigService, - SidecarStatusMapper sidecarStatusMapper) { + SidecarStatusMapper sidecarStatusMapper, + EtagService etagService) { final SidecarConfiguration sidecarConfiguration = clusterConfigService.getOrDefault(SidecarConfiguration.class, SidecarConfiguration.defaultConfiguration()); this.sidecarService = sidecarService; this.configurationService = configurationService; @@ -106,6 +109,7 @@ public AdministrationResource(SidecarService sidecarService, this.actionService = actionService; this.administrationFiltersFactory = administrationFiltersFactory; this.sidecarStatusMapper = sidecarStatusMapper; + this.etagService = etagService; this.activeSidecarFilter = new ActiveSidecarFilter(sidecarConfiguration.sidecarInactiveThreshold()); this.searchQueryParser = new SearchQueryParser(Sidecar.FIELD_NODE_NAME, SidecarResource.SEARCH_FIELD_MAPPING); } @@ -166,6 +170,7 @@ public Response setAction(@ApiParam(name = "JSON body", required = true) .collect(Collectors.toList()); final CollectorActions collectorActions = actionService.fromRequest(bulkActionRequest.sidecarId(), actions); actionService.saveAction(collectorActions); + etagService.invalidateRegistration(bulkActionRequest.sidecarId()); } return Response.accepted().build(); diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/CollectorResource.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/CollectorResource.java index e35ffe32904f..517e4ed25076 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/CollectorResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/CollectorResource.java @@ -17,9 +17,9 @@ package org.graylog.plugins.sidecar.rest.resources; import com.codahale.metrics.annotation.Timed; +import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.google.common.hash.Hashing; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -136,15 +136,15 @@ public Collector getCollector(@ApiParam(name = "id", required = true) @RequiresPermissions(SidecarRestPermissions.COLLECTORS_READ) @Produces(MediaType.APPLICATION_JSON) @ApiOperation(value = "List all collectors") - public Response listCollectors(@Context HttpHeaders httpHeaders) { + public Response listCollectors(@Context HttpHeaders httpHeaders) throws JsonProcessingException { String ifNoneMatch = httpHeaders.getHeaderString("If-None-Match"); Boolean etagCached = false; Response.ResponseBuilder builder = Response.noContent(); - // check if client is up to date with a known valid etag + // check if client is up-to-date with a known valid etag if (ifNoneMatch != null) { EntityTag etag = new EntityTag(ifNoneMatch.replaceAll("\"", "")); - if (etagService.isPresent(etag.toString())) { + if (etagService.collectorsAreCached(etag.toString())) { etagCached = true; builder = Response.notModified(); builder.tag(etag); @@ -157,12 +157,10 @@ public Response listCollectors(@Context HttpHeaders httpHeaders) { CollectorListResponse collectorListResponse = CollectorListResponse.create(result.size(), result); // add new etag to cache - String etagString = collectorsToEtag(collectorListResponse); - - EntityTag collectorsEtag = new EntityTag(etagString); + EntityTag collectorsEtag = etagService.buildEntityTagForResponse(collectorListResponse); builder = Response.ok(collectorListResponse); builder.tag(collectorsEtag); - etagService.put(collectorsEtag.toString()); + etagService.registerCollector(collectorsEtag.toString()); } // set cache control @@ -211,7 +209,7 @@ public Response createCollector(@ApiParam(name = "JSON body", required = true) if (validationResult.failed()) { return Response.status(Response.Status.BAD_REQUEST).entity(validationResult).build(); } - etagService.invalidateAll(); + etagService.invalidateAllCollectors(); return Response.ok().entity(collectorService.save(collector)).build(); } @@ -230,7 +228,7 @@ public Response updateCollector(@ApiParam(name = "id", required = true) if (validationResult.failed()) { return Response.status(Response.Status.BAD_REQUEST).entity(validationResult).build(); } - etagService.invalidateAll(); + etagService.invalidateAllCollectors(); return Response.ok().entity(collectorService.save(collector)).build(); } @@ -248,8 +246,8 @@ public Response copyCollector(@ApiParam(name = "id", required = true) if (validationResult.failed()) { return Response.status(Response.Status.BAD_REQUEST).entity(validationResult).build(); } - etagService.invalidateAll(); collectorService.save(collector); + etagService.invalidateAllCollectors(); return Response.accepted().build(); } @@ -272,7 +270,7 @@ public Response deleteCollector(@ApiParam(name = "id", required = true) if (deleted == 0) { return Response.notModified().build(); } - etagService.invalidateAll(); + etagService.invalidateAllCollectors(); return Response.accepted().build(); } @@ -351,9 +349,4 @@ private boolean validatePath(String path) { return VALID_PATH_PATTERN.matcher(path).matches(); } - private String collectorsToEtag(CollectorListResponse collectors) { - return Hashing.md5() - .hashInt(collectors.hashCode()) // avoid negative values - .toString(); - } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationResource.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationResource.java index 238f7e2a9bb7..808bfe08e0f0 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationResource.java @@ -17,8 +17,8 @@ package org.graylog.plugins.sidecar.rest.resources; import com.codahale.metrics.annotation.Timed; +import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.collect.ImmutableMap; -import com.google.common.hash.Hashing; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -207,15 +207,15 @@ public Response renderConfiguration(@Context HttpHeaders httpHeaders, @ApiParam(name = "sidecarId", required = true) @PathParam("sidecarId") String sidecarId, @ApiParam(name = "configurationId", required = true) - @PathParam("configurationId") String configurationId) throws RenderTemplateException { + @PathParam("configurationId") String configurationId) throws RenderTemplateException, JsonProcessingException { String ifNoneMatch = httpHeaders.getHeaderString("If-None-Match"); - Boolean etagCached = false; + boolean etagCached = false; Response.ResponseBuilder builder = Response.noContent(); - // check if client is up to date with a known valid etag + // check if client is up-to-date with a known valid etag if (ifNoneMatch != null) { EntityTag etag = new EntityTag(ifNoneMatch.replaceAll("\"", "")); - if (etagService.isPresent(etag.toString())) { + if (etagService.configurationsAreCached(etag.toString())) { etagCached = true; builder = Response.notModified(); builder.tag(etag); @@ -236,13 +236,10 @@ public Response renderConfiguration(@Context HttpHeaders httpHeaders, Configuration collectorConfiguration = this.configurationService.renderConfigurationForCollector(sidecar, configuration); // add new etag to cache - String etagString = configurationToEtag(collectorConfiguration); - - EntityTag collectorConfigurationEtag = new EntityTag(etagString); + EntityTag collectorConfigurationEtag = etagService.buildEntityTagForResponse(collectorConfiguration); builder = Response.ok(collectorConfiguration); builder.tag(collectorConfigurationEtag); - etagService.put(collectorConfigurationEtag.toString()); - + etagService.registerConfiguration(collectorConfigurationEtag.toString()); } // set cache control @@ -283,7 +280,12 @@ public Response createConfiguration(@ApiParam(name = "JSON body", required = tru return Response.status(Response.Status.BAD_REQUEST).entity(validationResult).build(); } - return Response.ok().entity(configurationService.save(configuration)).build(); + final Configuration config = configurationService.save(configuration); + if (!config.tags().isEmpty()) { + etagService.invalidateAllRegistrations(); + } + + return Response.ok().entity(config).build(); } @POST @@ -330,7 +332,10 @@ public Response updateConfiguration(@ApiParam(name = "id", required = true) if (validationResult.failed()) { return Response.status(Response.Status.BAD_REQUEST).entity(validationResult).build(); } - etagService.invalidateAll(); + etagService.invalidateAllConfigurations(); + if (! previousConfiguration.tags().equals(updatedConfiguration.tags())) { + etagService.invalidateAllRegistrations(); + } return Response.ok().entity(configurationService.save(updatedConfiguration)).build(); } @@ -351,7 +356,7 @@ public Response deleteConfiguration(@ApiParam(name = "id", required = true) if (deleted == 0) { return Response.notModified().build(); } - etagService.invalidateAll(); + etagService.invalidateAllConfigurations(); return Response.accepted().build(); } @@ -405,12 +410,6 @@ private boolean isConfigurationAssignedToSidecar(String configurationId, Sidecar return assignments.stream().anyMatch(assignment -> assignment.configurationId().equals(configurationId)); } - private String configurationToEtag(Configuration configuration) { - return Hashing.md5() - .hashInt(configuration.hashCode()) // avoid negative values - .toString(); - } - private Configuration configurationFromRequest(String id, Configuration request) { Configuration configuration; if (id == null) { diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationVariableResource.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationVariableResource.java index 04ea7c9df735..a1019ecbb27e 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationVariableResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationVariableResource.java @@ -132,7 +132,7 @@ public Response updateConfigurationVariable(@ApiParam(name = "id", required = tr configurationService.replaceVariableNames(previousConfigurationVariable.fullName(), request.fullName()); } final ConfigurationVariable updatedConfigurationVariable = persistConfigurationVariable(id, request); - etagService.invalidateAll(); + etagService.invalidateAllConfigurations(); return Response.ok().entity(updatedConfigurationVariable).build(); } @@ -168,7 +168,7 @@ public Response deleteConfigurationVariable(@ApiParam(name = "id", required = tr if (deleted == 0) { return Response.notModified().build(); } - etagService.invalidateAll(); + etagService.invalidateAllConfigurations(); return Response.accepted().build(); } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/SidecarResource.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/SidecarResource.java index 74f5b7b473a0..6cfbbc693991 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/SidecarResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/SidecarResource.java @@ -17,6 +17,7 @@ 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; @@ -42,6 +43,7 @@ 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; @@ -53,12 +55,12 @@ import org.graylog2.search.SearchQueryField; import org.graylog2.search.SearchQueryParser; import org.graylog2.shared.rest.resources.RestResource; -import org.hibernate.validator.constraints.NotEmpty; 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; @@ -71,6 +73,7 @@ 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; @@ -99,6 +102,7 @@ public class SidecarResource extends RestResource implements PluginRestResource private final SidecarService sidecarService; private final ActionService actionService; + private final EtagService etagService; private final ActiveSidecarFilter activeSidecarFilter; private final SearchQueryParser searchQueryParser; private final SidecarStatusMapper sidecarStatusMapper; @@ -108,12 +112,14 @@ public class SidecarResource extends RestResource implements PluginRestResource public SidecarResource(SidecarService sidecarService, ActionService actionService, ClusterConfigService clusterConfigService, - SidecarStatusMapper sidecarStatusMapper) { + 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); } @@ -195,39 +201,55 @@ public SidecarSummary get(@ApiParam(name = "sidecarId", required = true) @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 sidecarId, + @PathParam("sidecarId") @NotEmpty String nodeId, @ApiParam(name = "JSON body", required = true) @Valid @NotNull RegistrationRequest request, - @HeaderParam(value = "X-Graylog-Sidecar-Version") @NotEmpty String sidecarVersion) { - final Sidecar newSidecar; - final Sidecar oldSidecar = sidecarService.findByNodeId(sidecarId); - List assignments = null; + @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) { - assignments = oldSidecar.assignments(); - newSidecar = oldSidecar.toBuilder() + sidecar = oldSidecar.toBuilder() .nodeName(request.nodeName()) .nodeDetails(request.nodeDetails()) .sidecarVersion(sidecarVersion) .lastSeen(DateTime.now(DateTimeZone.UTC)) .build(); } else { - newSidecar = sidecarService.fromRequest(sidecarId, request, sidecarVersion); + sidecar = sidecarService.fromRequest(nodeId, request, sidecarVersion); } - sidecarService.save(newSidecar); - final CollectorActions collectorActions = actionService.findActionBySidecar(sidecarId, true); + // If the sidecar has the recent registration, return with HTTP 304 + if (ifNoneMatch != null) { + EntityTag etag = new EntityTag(ifNoneMatch.replaceAll("\"", "")); + if (etagService.registrationIsCached(sidecar.id(), 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( - this.sidecarConfiguration.sidecarUpdateInterval().toStandardDuration().getStandardSeconds(), - this.sidecarConfiguration.sidecarSendStatus()), - this.sidecarConfiguration.sidecarConfigurationOverride(), + sidecarConfiguration.sidecarUpdateInterval().toStandardDuration().getStandardSeconds(), + sidecarConfiguration.sidecarSendStatus()), + sidecarConfiguration.sidecarConfigurationOverride(), collectorAction, - assignments); - return Response.accepted(sidecarRegistrationResponse).build(); + sidecar.assignments()); + // add new etag to cache + EntityTag registrationEtag = etagService.buildEntityTagForResponse(sidecarRegistrationResponse); + etagService.addSidecarRegistration(sidecar.id(), registrationEtag.toString()); + + return Response.accepted(sidecarRegistrationResponse).tag(registrationEtag).build(); } @PUT @@ -249,8 +271,9 @@ public Response assignConfiguration(@ApiParam(name = "JSON body", required = tru .flatMap(a -> a.assignments().stream()) .collect(Collectors.toList()); try { - Sidecar sidecar = sidecarService.assignConfiguration(nodeId, nodeRelations); + Sidecar sidecar = sidecarService.applyManualAssignments(nodeId, nodeRelations); sidecarService.save(sidecar); + etagService.invalidateRegistration(sidecar.id()); } catch (org.graylog2.database.NotFoundException e) { throw new NotFoundException(e.getMessage()); } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ConfigurationService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ConfigurationService.java index cccf81573014..da1ef990d41a 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ConfigurationService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ConfigurationService.java @@ -17,6 +17,7 @@ package org.graylog.plugins.sidecar.services; import com.mongodb.BasicDBObject; +import com.mongodb.DBCollection; import freemarker.cache.MultiTemplateLoader; import freemarker.cache.StringTemplateLoader; import freemarker.cache.TemplateLoader; @@ -47,6 +48,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -60,7 +62,7 @@ public class ConfigurationService extends PaginatedDbService { private static final freemarker.template.Configuration templateConfiguration = new freemarker.template.Configuration(freemarker.template.Configuration.VERSION_2_3_28); private static final StringTemplateLoader stringTemplateLoader = new StringTemplateLoader(); - private ConfigurationVariableService configurationVariableService; + private final ConfigurationVariableService configurationVariableService; private static final String COLLECTION_NAME = "sidecar_configurations"; @@ -70,6 +72,7 @@ public ConfigurationService(MongoConnection mongoConnection, ConfigurationVariableService configurationVariableService) { super(mongoConnection, mapper, Configuration.class, COLLECTION_NAME); MongoDbTemplateLoader mongoDbTemplateLoader = new MongoDbTemplateLoader(db); + DBCollection collection = db.getDB().getCollection(COLLECTION_NAME); MultiTemplateLoader multiTemplateLoader = new MultiTemplateLoader(new TemplateLoader[] { mongoDbTemplateLoader, stringTemplateLoader }); @@ -79,6 +82,10 @@ public ConfigurationService(MongoConnection mongoConnection, templateConfiguration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); templateConfiguration.setLogTemplateExceptions(false); this.configurationVariableService = configurationVariableService; + + collection.createIndex(new BasicDBObject(Configuration.FIELD_ID, 1)); + collection.createIndex(new BasicDBObject(Configuration.FIELD_COLLECTOR_ID, 1)); + collection.createIndex(new BasicDBObject(Configuration.FIELD_TAGS, 1)); } public Configuration find(String id) { @@ -116,6 +123,10 @@ public List findByConfigurationVariable(ConfigurationVariable con return findByQuery(query); } + public List findByTags(Set tags) { + return findByQuery(DBQuery.in(Configuration.FIELD_TAGS, tags)); + } + public void replaceVariableNames(String oldName, String newName) { final DBQuery.Query query = DBQuery.regex(Configuration.FIELD_TEMPLATE, Pattern.compile(Pattern.quote(oldName))); List configurations = findByQuery(query); @@ -134,15 +145,17 @@ public Configuration save(Configuration configuration) { public Configuration copyConfiguration(String id, String name) { Configuration configuration = find(id); - return Configuration.create(configuration.collectorId(), name, configuration.color(), configuration.template()); + // Tags are not copied on purpose + return Configuration.createWithoutId(configuration.collectorId(), name, configuration.color(), configuration.template(), Set.of()); } public Configuration fromRequest(Configuration request) { - return Configuration.create( + return Configuration.createWithoutId( request.collectorId(), request.name(), request.color(), - request.template()); + request.template(), + request.tags()); } public Configuration fromRequest(String id, Configuration request) { @@ -151,7 +164,8 @@ public Configuration fromRequest(String id, Configuration request) { request.collectorId(), request.name(), request.color(), - request.template()); + request.template(), + request.tags()); } public Configuration renderConfigurationForCollector(Sidecar sidecar, Configuration configuration) throws RenderTemplateException { @@ -165,13 +179,15 @@ public Configuration renderConfigurationForCollector(Sidecar sidecar, Configurat String pathDelim = sidecar.nodeDetails().operatingSystem().equalsIgnoreCase("windows") ? "\\" : "/"; context.put("spoolDir", sidecar.nodeDetails().collectorConfigurationDirectory() + pathDelim + configuration.id()); } + context.put("tags", sidecar.nodeDetails().tags().stream().collect(Collectors.toMap(t -> t, t -> true))); return Configuration.create( configuration.id(), configuration.collectorId(), configuration.name(), configuration.color(), - renderTemplate(configuration.id(), context) + renderTemplate(configuration.id(), context), + configuration.tags() ); } @@ -182,6 +198,7 @@ public String renderPreview(String template) throws RenderTemplateException { context.put("sidecarVersion", ""); context.put("operatingSystem", ""); context.put("spoolDir", ""); + context.put("tags", Map.of()); String previewName = UUID.randomUUID().toString(); stringTemplateLoader.putTemplate(previewName, template); diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagCacheInvalidation.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagCacheInvalidation.java index 9d6123dd1dbe..5d249ff8a464 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagCacheInvalidation.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagCacheInvalidation.java @@ -23,12 +23,15 @@ @AutoValue public abstract class EtagCacheInvalidation { - @JsonProperty("etag") - public abstract String etag(); + @JsonProperty("cache_context") + public abstract EtagService.CacheContext cacheContext(); + + @JsonProperty("cache_key") + public abstract String cacheKey(); @JsonCreator - public static EtagCacheInvalidation etag(@JsonProperty("etag") String etag) { - return new AutoValue_EtagCacheInvalidation(etag); + public static EtagCacheInvalidation create(@JsonProperty("cache_context") EtagService.CacheContext context, @JsonProperty("cache_key") String cacheKey) { + return new AutoValue_EtagCacheInvalidation(context, cacheKey); } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagService.java index 489df2c00bcf..a833c4761f52 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagService.java @@ -17,21 +17,29 @@ package org.graylog.plugins.sidecar.services; import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.joschi.jadconfig.util.Duration; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; +import com.google.common.hash.Hashing; import com.google.common.util.concurrent.AbstractIdleService; import org.graylog.plugins.sidecar.common.SidecarPluginConfiguration; import org.graylog2.events.ClusterEventBus; import org.graylog2.metrics.CacheStatsSet; +import org.graylog2.shared.bindings.providers.ObjectMapperProvider; import org.graylog2.shared.metrics.MetricUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Singleton; +import javax.ws.rs.core.EntityTag; +import java.nio.charset.StandardCharsets; import static com.codahale.metrics.MetricRegistry.name; @@ -39,21 +47,48 @@ public class EtagService extends AbstractIdleService { private static final Logger LOG = LoggerFactory.getLogger(EtagService.class); - private final Cache cache; - private MetricRegistry metricRegistry; - private EventBus eventBus; - private ClusterEventBus clusterEventBus; + private final Cache collectorCache; + private final Cache configurationCache; + private final Cache registrationCache; + private final MetricRegistry metricRegistry; + private final EventBus eventBus; + private final ClusterEventBus clusterEventBus; + private final ObjectMapper objectMapper; + + @JsonAutoDetect + enum CacheContext { + @JsonProperty("collector") + COLLECTOR, + @JsonProperty("configuration") + CONFIGURATION, + @JsonProperty("registration") + REGISTRATION + } @Inject public EtagService(SidecarPluginConfiguration pluginConfiguration, MetricRegistry metricRegistry, EventBus eventBus, - ClusterEventBus clusterEventBus) { + ClusterEventBus clusterEventBus, + ObjectMapperProvider objectMapperProvider) { this.metricRegistry = metricRegistry; this.eventBus = eventBus; this.clusterEventBus = clusterEventBus; + this.objectMapper = objectMapperProvider.get(); Duration cacheTime = pluginConfiguration.getCacheTime(); - cache = CacheBuilder.newBuilder() + collectorCache = CacheBuilder.newBuilder() + .recordStats() + .expireAfterWrite(cacheTime.getQuantity(), cacheTime.getUnit()) + .maximumSize(pluginConfiguration.getCacheMaxSize()) + .build(); + + configurationCache = CacheBuilder.newBuilder() + .recordStats() + .expireAfterWrite(cacheTime.getQuantity(), cacheTime.getUnit()) + .maximumSize(pluginConfiguration.getCacheMaxSize()) + .build(); + + registrationCache = CacheBuilder.newBuilder() .recordStats() .expireAfterWrite(cacheTime.getQuantity(), cacheTime.getUnit()) .maximumSize(pluginConfiguration.getCacheMaxSize()) @@ -62,41 +97,84 @@ public EtagService(SidecarPluginConfiguration pluginConfiguration, @Subscribe public void handleEtagInvalidation(EtagCacheInvalidation event) { - if (event.etag().equals("")) { - LOG.trace("Invalidating all collector configuration etags"); + var cache = switch (event.cacheContext()) { + case COLLECTOR -> collectorCache; + case CONFIGURATION -> configurationCache; + case REGISTRATION -> registrationCache; + }; + + if (event.cacheKey().equals("")) { + LOG.trace("Invalidating {} cache for all keys", event.cacheContext()); cache.invalidateAll(); } else { - LOG.trace("Invalidating collector configuration etag {}", event.etag()); - cache.invalidate(event.etag()); + LOG.trace("Invalidating {} cache for cacheKey {}", event.cacheContext(), event.cacheKey()); + cache.invalidate(event.cacheKey()); } } - public boolean isPresent(String etag) { - return cache.getIfPresent(etag) != null; + public boolean collectorsAreCached(String etag) { + return collectorCache.getIfPresent(etag) != null; + } + + public boolean configurationsAreCached(String etag) { + return configurationCache.getIfPresent(etag) != null; + } + + public boolean registrationIsCached(String sidecarId, String etag) { + return etag.equals(registrationCache.getIfPresent(sidecarId)); + } + + public void registerCollector(String etag) { + collectorCache.put(etag, Boolean.TRUE); + } + + public void registerConfiguration(String etag) { + configurationCache.put(etag, Boolean.TRUE); + } + + public void addSidecarRegistration(String sidecarId, String etag) { + registrationCache.put(sidecarId, etag); + } + + + public void invalidateAllConfigurations() { + configurationCache.invalidateAll(); + clusterEventBus.post(EtagCacheInvalidation.create(CacheContext.CONFIGURATION, "")); + } + + public void invalidateAllCollectors() { + collectorCache.invalidateAll(); + clusterEventBus.post(EtagCacheInvalidation.create(CacheContext.COLLECTOR, "")); } - public void put(String etag) { - cache.put(etag, Boolean.TRUE); + public void invalidateAllRegistrations() { + registrationCache.invalidateAll(); + clusterEventBus.post(EtagCacheInvalidation.create(CacheContext.REGISTRATION, "")); } - public void invalidate(String etag) { - clusterEventBus.post(EtagCacheInvalidation.etag(etag)); + public void invalidateRegistration(String sidecarId) { + registrationCache.invalidate(sidecarId); + clusterEventBus.post(EtagCacheInvalidation.create(CacheContext.REGISTRATION, sidecarId)); } - public void invalidateAll() { - cache.invalidateAll(); - clusterEventBus.post(EtagCacheInvalidation.etag("")); + public EntityTag buildEntityTagForResponse(Object o) throws JsonProcessingException { + final String json = objectMapper.writeValueAsString(o); + return new EntityTag(Hashing.murmur3_128().hashString(json, StandardCharsets.UTF_8).toString()); } @Override protected void startUp() throws Exception { eventBus.register(this); - MetricUtils.safelyRegisterAll(metricRegistry, new CacheStatsSet(name(ConfigurationService.class, "etag-cache"), cache)); + MetricUtils.safelyRegisterAll(metricRegistry, new CacheStatsSet(name(ConfigurationService.class, "etag-cache"), configurationCache)); + MetricUtils.safelyRegisterAll(metricRegistry, new CacheStatsSet(name(CollectorService.class, "etag-cache"), collectorCache)); + MetricUtils.safelyRegisterAll(metricRegistry, new CacheStatsSet(name(SidecarService.class, "etag-cache"), registrationCache)); } @Override protected void shutDown() throws Exception { eventBus.unregister(this); metricRegistry.removeMatching((name, metric) -> name.startsWith(name(ConfigurationService.class, "etag-cache"))); + metricRegistry.removeMatching((name, metric) -> name.startsWith(name(CollectorService.class, "etag-cache"))); + metricRegistry.removeMatching((name, metric) -> name.startsWith(name(SidecarService.class, "etag-cache"))); } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java index 865ceadce079..8675cea6994a 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java @@ -16,8 +16,10 @@ */ package org.graylog.plugins.sidecar.services; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.mongodb.BasicDBObject; +import org.apache.commons.collections4.CollectionUtils; import org.graylog.plugins.sidecar.rest.models.Collector; import org.graylog.plugins.sidecar.rest.models.CollectorStatus; import org.graylog.plugins.sidecar.rest.models.CollectorStatusList; @@ -42,6 +44,8 @@ import javax.inject.Inject; import javax.validation.ConstraintViolation; import javax.validation.Validator; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Set; import java.util.function.Predicate; @@ -75,23 +79,49 @@ public long count() { @Override public Sidecar save(Sidecar sidecar) { - if (sidecar != null) { - final Set> violations = validator.validate(sidecar); - if (violations.isEmpty()) { - return db.findAndModify( - DBQuery.is(Sidecar.FIELD_NODE_ID, sidecar.nodeId()), - new BasicDBObject(), - new BasicDBObject(), - false, - sidecar, - true, - true); - } else { - throw new IllegalArgumentException("Specified object failed validation: " + violations); - } - } else { - throw new IllegalArgumentException("Specified object is not of correct implementation type (" + sidecar.getClass() + ")!"); + Preconditions.checkNotNull(sidecar, "sidecar was null"); + + final Set> violations = validator.validate(sidecar); + if (!violations.isEmpty()) { + throw new IllegalArgumentException("Specified object failed validation: " + violations); } + + return db.findAndModify( + DBQuery.is(Sidecar.FIELD_NODE_ID, sidecar.nodeId()), + new BasicDBObject(), + new BasicDBObject(), + false, + sidecar, + true, + true); + } + + // Create new assignments based on tags and existing manual assignments' + public Sidecar updateTaggedConfigurationAssignments(Sidecar sidecar) { + final Set sidecarTags = sidecar.nodeDetails().tags(); + + // find all configurations that match the tags + // TODO only find configs that match the right collector os + final List taggedConfigs = configurationService.findByTags(sidecarTags); + + final List tagAssigned = taggedConfigs.stream().map(c -> { + // fill in ConfigurationAssignment.assignedFromTags() + // If we only support one tag on a configuration, this can be simplified + final Set matchedTags = c.tags().stream().filter(sidecarTags::contains).collect(Collectors.toSet()); + return ConfigurationAssignment.create(c.collectorId(), c.id(), matchedTags); + }).toList(); + + final List manuallyAssigned = sidecar.assignments().stream().filter(a -> { + // also overwrite manually assigned configs that would now be assigned through tags + if (tagAssigned.stream().anyMatch(tagAssignment -> tagAssignment.configurationId().equals(a.configurationId()))) { + return false; + } + return a.assignedFromTags().isEmpty(); + }).toList(); + + // return a sidecar with updated assignments + final Collection union = CollectionUtils.union(manuallyAssigned, tagAssigned); + return sidecar.toBuilder().assignments(new ArrayList<>(union)).build(); } public List all() { @@ -203,7 +233,7 @@ public Sidecar fromRequest(String nodeId, RegistrationRequest request, String co collectorVersion); } - public Sidecar assignConfiguration(String collectorNodeId, List assignments) throws NotFoundException{ + public Sidecar applyManualAssignments(String collectorNodeId, List assignments) throws NotFoundException{ Sidecar sidecar = findByNodeId(collectorNodeId); if (sidecar == null) { throw new NotFoundException("Couldn't find collector with ID " + collectorNodeId); @@ -222,8 +252,16 @@ public Sidecar assignConfiguration(String collectorNodeId, List taggedAssignments = sidecar.assignments().stream().filter(a -> !a.assignedFromTags().isEmpty()).toList(); + final List configIdsAssignedThroughTags = taggedAssignments.stream().map(ConfigurationAssignment::configurationId).toList(); + + final List filteredAssignments = assignments.stream().filter(a -> !configIdsAssignedThroughTags.contains(a.configurationId())).toList(); + final Collection union = CollectionUtils.union(filteredAssignments, taggedAssignments); + Sidecar toSave = sidecar.toBuilder() - .assignments(assignments) + .assignments(new ArrayList<>(union)) .build(); return save(toSave); } diff --git a/graylog2-server/src/main/java/org/graylog2/contentpacks/facades/SidecarCollectorConfigurationFacade.java b/graylog2-server/src/main/java/org/graylog2/contentpacks/facades/SidecarCollectorConfigurationFacade.java index 91dd72e35350..e8d0c24eb3e7 100644 --- a/graylog2-server/src/main/java/org/graylog2/contentpacks/facades/SidecarCollectorConfigurationFacade.java +++ b/graylog2-server/src/main/java/org/graylog2/contentpacks/facades/SidecarCollectorConfigurationFacade.java @@ -91,11 +91,12 @@ public NativeEntity createNativeEntity(Entity entity, private NativeEntity decode(EntityV1 entity, Map parameters) { final SidecarCollectorConfigurationEntity configurationEntity = objectMapper.convertValue(entity.data(), SidecarCollectorConfigurationEntity.class); - final Configuration configuration = Configuration.create( + final Configuration configuration = Configuration.createWithoutId( configurationEntity.collectorId().asString(parameters), configurationEntity.title().asString(parameters), configurationEntity.color().asString(parameters), - configurationEntity.template().asString(parameters)); + configurationEntity.template().asString(parameters), + Set.of()); final Configuration savedConfiguration = configurationService.save(configuration); return NativeEntity.create(entity.id(), savedConfiguration.id(), TYPE_V1, configuration.name(), savedConfiguration); diff --git a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/ConfigurationServiceTest.java b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/ConfigurationServiceTest.java index b02c69dd3974..d2c3fed73837 100644 --- a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/ConfigurationServiceTest.java +++ b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/ConfigurationServiceTest.java @@ -33,6 +33,8 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.Set; + import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.when; @@ -56,7 +58,7 @@ public class ConfigurationServiceTest { private Configuration buildTestConfig(String template) { - return Configuration.create(FILEBEAT_CONF_ID, "collId", "filebeat", "#ffffff", template); + return Configuration.create(FILEBEAT_CONF_ID, "collId", "filebeat", "#ffffff", template, Set.of()); } @Before diff --git a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java index 592a6c2581fe..270250ac6e1f 100644 --- a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java +++ b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java @@ -36,6 +36,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import javax.validation.Validator; import java.util.List; @@ -50,6 +52,9 @@ @UseModules({ObjectMapperModule.class, ValidatorModule.class, TestPasswordSecretModule.class}) public class SidecarServiceImplTest { private static final String collectionName = "sidecars"; + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock private CollectorService collectorService; diff --git a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java index a832eb4b7b6f..d189da40dbe4 100644 --- a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java +++ b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/rest/SidecarResourceTest.java @@ -26,6 +26,7 @@ import org.graylog.plugins.sidecar.rest.requests.RegistrationRequest; import org.graylog.plugins.sidecar.rest.resources.SidecarResource; 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.plugin.cluster.ClusterConfigService; @@ -38,6 +39,7 @@ import org.mockito.runners.MockitoJUnitRunner; import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.EntityTag; import javax.ws.rs.core.Response; import java.util.List; @@ -45,6 +47,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -66,6 +69,9 @@ public class SidecarResourceTest extends RestResourceBaseTest { @Mock private ClusterConfigService clusterConfigService; + @Mock + private EtagService etagService; + @Mock private SidecarConfiguration sidecarConfiguration; @@ -74,11 +80,13 @@ public void setUp() throws Exception { this.sidecars = getDummyCollectorList(); when(clusterConfigService.getOrDefault(SidecarConfiguration.class, SidecarConfiguration.defaultConfiguration())).thenReturn(sidecarConfiguration); when(sidecarConfiguration.sidecarUpdateInterval()).thenReturn(Period.seconds(30)); + when(etagService.buildEntityTagForResponse(any())).thenReturn(new EntityTag("hash browns")); this.resource = new SidecarResource( sidecarService, actionService, clusterConfigService, - statusMapper); + statusMapper, + etagService); } @Test(expected = NotFoundException.class) @@ -118,20 +126,25 @@ private List getDummyCollectorList() { @Test public void testRegister() throws Exception { + final NodeDetails nodeDetails = NodeDetails.create( + "DummyOS 1.0", + null, + null, + null, + null, + null, + null + ); final RegistrationRequest input = RegistrationRequest.create( "nodeName", - NodeDetails.create( - "DummyOS 1.0", - null, - null, - null, - null, - null, - null - ) + nodeDetails + ); + when(sidecarService.fromRequest(any(), any(RegistrationRequest.class), anyString())).thenReturn( + Sidecar.create("nodeId", "name", nodeDetails, "0.0.1") ); + when(sidecarService.updateTaggedConfigurationAssignments(any(Sidecar.class))).thenAnswer(invocation -> invocation.getArgument(0)); - final Response response = this.resource.register("sidecarId", input, "0.0.1"); + final Response response = this.resource.register("sidecarId", input, null, "0.0.1"); assertThat(response).isSuccess(); } @@ -152,7 +165,7 @@ public void testRegisterInvalidCollectorId() throws Exception { ) ); - final Response response = this.resource.register("", invalid, "0.0.1"); + final Response response = this.resource.register("", invalid, null, "0.0.1"); assertThat(response).isError(); assertThat(response).isStatus(Response.Status.BAD_REQUEST); @@ -174,7 +187,7 @@ public void testRegisterInvalidNodeId() throws Exception { ) ); - final Response response = this.resource.register("sidecarId", invalid, "0.0.1"); + final Response response = this.resource.register("sidecarId", invalid, null, "0.0.1"); assertThat(response).isError(); assertThat(response).isStatus(Response.Status.BAD_REQUEST); @@ -188,7 +201,7 @@ public void testRegisterMissingNodeDetails() throws Exception { null ); - final Response response = this.resource.register("sidecarId", invalid, "0.0.1"); + final Response response = this.resource.register("sidecarId", invalid, null, "0.0.1"); assertThat(response).isError(); assertThat(response).isStatus(Response.Status.BAD_REQUEST); @@ -210,7 +223,7 @@ public void testRegisterMissingOperatingSystem() throws Exception { ) ); - final Response response = this.resource.register("sidecarId", invalid, "0.0.1"); + final Response response = this.resource.register("sidecarId", invalid, null, "0.0.1"); assertThat(response).isError(); assertThat(response).isStatus(Response.Status.BAD_REQUEST); diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx index b0d9f2be683f..55fab55a4a4a 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx @@ -65,6 +65,7 @@ const ConfigurationForm = createReactClass({ color: configuration.color, collector_id: configuration.collector_id, template: configuration.template || '', + tags: configuration.tags }, }; }, From 686677ad54cf251d759466d6554cdc52400af8ce Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Mon, 19 Sep 2022 16:09:13 +0200 Subject: [PATCH 21/82] renamed btn Assign Configurations --- .../sidecars/administration/CollectorsAdministrationActions.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx index dbf5da6c139f..7a8c9be88352 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx @@ -43,7 +43,7 @@ const CollectorsAdministrationActions = (props) => { bsSize="small" disabled={selectedLogCollectorsNames.length !== 1} onClick={() => setShowConfigurationModal(true)}> - Edit Configurations + Assign Configurations Date: Mon, 19 Sep 2022 16:16:43 +0200 Subject: [PATCH 22/82] fix wrong postion of ColorLabel --- .../sidecars/administration/CollectorsAdministration.jsx | 5 +---- .../src/components/sidecars/common/ColorLabel.tsx | 8 +++++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx index 4ae8a93ed51d..7067200cb54f 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx @@ -281,11 +281,8 @@ const CollectorsAdministration = createReactClass({ {(configAssignments.length > 0) && this.setState({ selected: [sidecarCollectorId], showConfigurationModal: true })} />} - {configAssignments.map((configuration) => )} + {configAssignments.map((configuration) => )} - {/* */} ); diff --git a/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx b/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx index 77408fdb1240..147181929425 100644 --- a/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx +++ b/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx @@ -33,6 +33,7 @@ interface ColorLabelProps { color: string, size?: Size, text?: string | React.ReactNode, + style?: React.CSSProperties, theme: DefaultTheme } @@ -41,18 +42,17 @@ const ColorLabelWrap = styled.span(({ size, theme }: ColorLabelWrapProps) => { const fontSize = size === 'small' ? small : body; return css` - display: flex; vertical-align: middle; font-size: ${size === 'xsmall' ? tiny : fontSize}; `; }); -const ColorLabel = ({ color, size, text, theme }: ColorLabelProps) => { +const ColorLabel = ({ color, size, text, theme, style }: ColorLabelProps) => { const borderColor = theme.utils.colorLevel(color, 5); const textColor = theme.utils.contrastingColor(color); return ( - +
, size: 'normal', + style: undefined, }; export default withTheme(ColorLabel); From 32b21046530ae8ada5e05117fe67eeb05455d05e Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Mon, 19 Sep 2022 16:22:53 +0200 Subject: [PATCH 23/82] disable eslint errors --- .../sidecars/administration/CollectorConfigurationModal.tsx | 3 ++- .../sidecars/administration/CollectorsAdministration.jsx | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 420baa7061ae..5ad4e6fb5558 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -15,13 +15,13 @@ * along with this program. If not, see * . */ -import { naturalSortIgnoreCase } from 'util/SortUtils'; import React from 'react'; import PropTypes from 'prop-types'; import lodash from 'lodash'; import styled from 'styled-components'; +import { naturalSortIgnoreCase } from 'util/SortUtils'; import Routes from 'routing/Routes'; import { Table, BootstrapModalConfirm, BootstrapModalWrapper, Button, Modal } from 'components/bootstrap'; import { SearchForm, Icon } from 'components/common'; @@ -263,6 +263,7 @@ const CollectorConfigurationModal = (props) => { }; return ; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.show]); return ( diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx index 7067200cb54f..5142ebd68416 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx @@ -54,6 +54,7 @@ const DisabledCollector = styled.div(({ theme }) => css` export const PAGE_SIZES = [10, 25, 50, 100]; const CollectorsAdministration = createReactClass({ + // eslint-disable-next-line react/no-unused-class-component-methods propTypes: { sidecarCollectorPairs: PropTypes.array.isRequired, collectors: PropTypes.array.isRequired, @@ -122,6 +123,7 @@ const CollectorsAdministration = createReactClass({ return selected.filter((sidecarCollectorId) => filteredSidecarCollectorIds.includes(sidecarCollectorId)); }, + // eslint-disable-next-line react/sort-comp handleConfigurationChange(selectedConfigurations, doneCallback) { const { selected, enabledCollectors } = this.state; const { onConfigurationChange } = this.props; @@ -147,6 +149,7 @@ const CollectorsAdministration = createReactClass({ onProcessAction(action, selectedCollectors, doneCallback); }, + // eslint-disable-next-line react/no-unstable-nested-components formatHeader(selectedSidecarCollectorPairs) { const { collectors, configurations } = this.props; const { selected, enabledCollectors } = this.state; @@ -214,6 +217,7 @@ const CollectorsAdministration = createReactClass({ this.setState({ selected: newSelection }); }, + // eslint-disable-next-line react/no-unstable-nested-components formatSidecarNoCollectors(sidecar) { return ( @@ -238,6 +242,7 @@ const CollectorsAdministration = createReactClass({ ); }, + // eslint-disable-next-line react/no-unstable-nested-components formatCollector(sidecar, collector, configurations) { const sidecarCollectorId = this.sidecarCollectorId(sidecar, collector); const configAssignmentIDs = sidecar.assignments.filter((assignment) => assignment.collector_id === collector.id).map((assignment) => assignment.configuration_id); @@ -288,6 +293,7 @@ const CollectorsAdministration = createReactClass({ ); }, + // eslint-disable-next-line react/no-unstable-nested-components formatSidecar(sidecar, collectors, configurations) { if (collectors.length === 0) { return this.formatSidecarNoCollectors(sidecar); From 9c3cef93cd22c666ed20386cb386024a1ffb3696 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Wed, 21 Sep 2022 13:06:44 +0200 Subject: [PATCH 24/82] added ModalSubTitle styling --- .../administration/CollectorConfigurationModal.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 5ad4e6fb5558..4cdd740431d8 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -93,6 +93,12 @@ const ModalTitle = styled(Modal.Title)` line-height: 1.1; `; +const ModalSubTitle = styled.div` + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; + const CollectorConfigurationModal = (props) => { const [nextAssignedConfigurations, setNextAssignedConfigurations] = React.useState([]); @@ -226,7 +232,9 @@ const CollectorConfigurationModal = (props) => { Edit {selectedLogCollectors[0]?.name} Configurations -
sidecars: {selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name).join(', ')}
+ + sidecars: {selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name).join(', ')} +
From e906bca5f5f2c146dcc0492c932ea3e8a399cfe7 Mon Sep 17 00:00:00 2001 From: Marco Pfatschbacher Date: Wed, 21 Sep 2022 17:32:07 +0200 Subject: [PATCH 25/82] Sidecar tags tests (#13502) * Only assign tagged configs that match the OS * rename test * add tests for SidecarService.updateTaggedConfigurationAssignments() * add tests for SidecarService.applyManualAssignments() --- .../sidecar/services/SidecarService.java | 13 +- .../collectors/SidecarServiceImplTest.java | 169 -------- .../collectors/SidecarServiceTest.java | 381 ++++++++++++++++++ .../collectors/collectorsSingleDataset.json | 14 - .../plugins/sidecar/collectors/sidecars.json | 16 + 5 files changed, 405 insertions(+), 188 deletions(-) delete mode 100644 graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java create mode 100644 graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceTest.java delete mode 100644 graylog2-server/src/test/resources/org/graylog/plugins/sidecar/collectors/collectorsSingleDataset.json create mode 100644 graylog2-server/src/test/resources/org/graylog/plugins/sidecar/collectors/sidecars.json diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java index 8675cea6994a..4c886ea10007 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java @@ -101,10 +101,13 @@ public Sidecar updateTaggedConfigurationAssignments(Sidecar sidecar) { final Set sidecarTags = sidecar.nodeDetails().tags(); // find all configurations that match the tags - // TODO only find configs that match the right collector os final List taggedConfigs = configurationService.findByTags(sidecarTags); + final Set matchingOsCollectorIds = collectorService.all().stream() + .filter(c -> c.nodeOperatingSystem().equalsIgnoreCase(sidecar.nodeDetails().operatingSystem())) + .map(Collector::id).collect(Collectors.toSet()); - final List tagAssigned = taggedConfigs.stream().map(c -> { + final List tagAssigned = taggedConfigs.stream() + .filter(c -> matchingOsCollectorIds.contains(c.collectorId())).map(c -> { // fill in ConfigurationAssignment.assignedFromTags() // If we only support one tag on a configuration, this can be simplified final Set matchedTags = c.tags().stream().filter(sidecarTags::contains).collect(Collectors.toSet()); @@ -233,10 +236,10 @@ public Sidecar fromRequest(String nodeId, RegistrationRequest request, String co collectorVersion); } - public Sidecar applyManualAssignments(String collectorNodeId, List assignments) throws NotFoundException{ - Sidecar sidecar = findByNodeId(collectorNodeId); + public Sidecar applyManualAssignments(String sidecarNodeId, List assignments) throws NotFoundException{ + Sidecar sidecar = findByNodeId(sidecarNodeId); if (sidecar == null) { - throw new NotFoundException("Couldn't find collector with ID " + collectorNodeId); + throw new NotFoundException("Couldn't find sidecar with nodeId " + sidecarNodeId); } for (ConfigurationAssignment assignment : assignments) { Collector collector = collectorService.find(assignment.collectorId()); diff --git a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java deleted file mode 100644 index 270250ac6e1f..000000000000 --- a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceImplTest.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * 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.collectors; - -import com.mongodb.client.MongoCollection; -import org.bson.Document; -import org.graylog.plugins.sidecar.rest.models.NodeDetails; -import org.graylog.plugins.sidecar.rest.models.Sidecar; -import org.graylog.plugins.sidecar.services.CollectorService; -import org.graylog.plugins.sidecar.services.ConfigurationService; -import org.graylog.plugins.sidecar.services.SidecarService; -import org.graylog.testing.inject.TestPasswordSecretModule; -import org.graylog.testing.mongodb.MongoDBFixtures; -import org.graylog.testing.mongodb.MongoDBInstance; -import org.graylog2.bindings.providers.MongoJackObjectMapperProvider; -import org.graylog2.shared.bindings.ObjectMapperModule; -import org.graylog2.shared.bindings.ValidatorModule; -import org.jukito.JukitoRunner; -import org.jukito.UseModules; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -import javax.validation.Validator; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -@RunWith(JukitoRunner.class) -@UseModules({ObjectMapperModule.class, ValidatorModule.class, TestPasswordSecretModule.class}) -public class SidecarServiceImplTest { - private static final String collectionName = "sidecars"; - - @Rule - public final MockitoRule mockitoRule = MockitoJUnit.rule(); - @Mock - private CollectorService collectorService; - - @Mock private ConfigurationService configurationService; - - @Rule - public final MongoDBInstance mongodb = MongoDBInstance.createForClass(); - - private SidecarService sidecarService; - - @Before - public void setUp(MongoJackObjectMapperProvider mapperProvider, - Validator validator) throws Exception { - this.sidecarService = new SidecarService(collectorService, configurationService, mongodb.mongoConnection(), mapperProvider, validator); - } - - @Test - public void testCountEmptyCollection() throws Exception { - final long result = this.sidecarService.count(); - - assertEquals(0, result); - } - - @Test - @MongoDBFixtures("collectorsMultipleDocuments.json") - public void testCountNonEmptyCollection() throws Exception { - final long result = this.sidecarService.count(); - - assertEquals(3, result); - } - - @Test - public void testSaveFirstRecord() throws Exception { - String nodeId = "nodeId"; - String nodeName = "nodeName"; - String version = "0.0.1"; - String os = "DummyOS 1.0"; - final Sidecar sidecar = Sidecar.create( - nodeId, - nodeName, - NodeDetails.create( - os, - null, - null, - null, - null, - null, - null), - version - ); - - final Sidecar result = this.sidecarService.save(sidecar); - MongoCollection collection = mongodb.mongoConnection().getMongoDatabase().getCollection(collectionName); - Document document = collection.find().first(); - Document nodeDetails = document.get("node_details", Document.class); - - assertNotNull(result); - assertEquals(nodeId, document.get("node_id")); - assertEquals(nodeName, document.get("node_name")); - assertEquals(version, document.get("sidecar_version")); - assertEquals(os, nodeDetails.get("operating_system")); - } - - @Test - @MongoDBFixtures("collectorsMultipleDocuments.json") - public void testAll() throws Exception { - final List sidecars = this.sidecarService.all(); - - assertNotNull(sidecars); - assertEquals(3, sidecars.size()); - } - - @Test - public void testAllEmptyCollection() throws Exception { - final List sidecars = this.sidecarService.all(); - - assertNotNull(sidecars); - assertEquals(0, sidecars.size()); - } - - @Test - @MongoDBFixtures("collectorsMultipleDocuments.json") - public void testFindById() throws Exception { - final String collector1id = "uniqueid1"; - - final Sidecar sidecar = this.sidecarService.findByNodeId(collector1id); - - assertNotNull(sidecar); - assertEquals(collector1id, sidecar.nodeId()); - } - - @Test - @MongoDBFixtures("collectorsMultipleDocuments.json") - public void testFindByIdNonexisting() throws Exception { - final String collector1id = "nonexisting"; - - final Sidecar sidecar = this.sidecarService.findByNodeId(collector1id); - - assertNull(sidecar); - } - - @Test - @MongoDBFixtures("collectorsMultipleDocuments.json") - public void testDestroy() throws Exception { - final Sidecar sidecar = mock(Sidecar.class); - when(sidecar.id()).thenReturn("581b3bff8e4dc4270055dfcb"); - - final int result = this.sidecarService.delete(sidecar.id()); - assertEquals(1, result); - assertEquals(2, mongodb.mongoConnection().getMongoDatabase().getCollection(collectionName).countDocuments()); - } -} diff --git a/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceTest.java b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceTest.java new file mode 100644 index 000000000000..5240e1753ba5 --- /dev/null +++ b/graylog2-server/src/test/java/org/graylog/plugins/sidecar/collectors/SidecarServiceTest.java @@ -0,0 +1,381 @@ +/* + * 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.collectors; + +import com.mongodb.client.MongoCollection; +import org.bson.Document; +import org.graylog.plugins.sidecar.rest.models.Collector; +import org.graylog.plugins.sidecar.rest.models.Configuration; +import org.graylog.plugins.sidecar.rest.models.NodeDetails; +import org.graylog.plugins.sidecar.rest.models.Sidecar; +import org.graylog.plugins.sidecar.rest.requests.ConfigurationAssignment; +import org.graylog.plugins.sidecar.services.CollectorService; +import org.graylog.plugins.sidecar.services.ConfigurationService; +import org.graylog.plugins.sidecar.services.SidecarService; +import org.graylog.testing.inject.TestPasswordSecretModule; +import org.graylog.testing.mongodb.MongoDBFixtures; +import org.graylog.testing.mongodb.MongoDBInstance; +import org.graylog2.bindings.providers.MongoJackObjectMapperProvider; +import org.graylog2.database.NotFoundException; +import org.graylog2.shared.bindings.ObjectMapperModule; +import org.graylog2.shared.bindings.ValidatorModule; +import org.jukito.JukitoRunner; +import org.jukito.UseModules; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import javax.validation.Validator; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(JukitoRunner.class) +@UseModules({ObjectMapperModule.class, ValidatorModule.class, TestPasswordSecretModule.class}) +public class SidecarServiceTest { + private static final String collectionName = "sidecars"; + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + @Mock + private CollectorService collectorService; + + @Mock private ConfigurationService configurationService; + + @Rule + public final MongoDBInstance mongodb = MongoDBInstance.createForClass(); + + private SidecarService sidecarService; + + @Before + public void setUp(MongoJackObjectMapperProvider mapperProvider, + Validator validator) throws Exception { + this.sidecarService = new SidecarService(collectorService, configurationService, mongodb.mongoConnection(), mapperProvider, validator); + } + + @Test + public void testCountEmptyCollection() throws Exception { + final long result = this.sidecarService.count(); + + assertEquals(0, result); + } + + @Test + @MongoDBFixtures("collectorsMultipleDocuments.json") + public void testCountNonEmptyCollection() throws Exception { + final long result = this.sidecarService.count(); + + assertEquals(3, result); + } + + @Test + public void testSaveFirstRecord() throws Exception { + String nodeId = "nodeId"; + String nodeName = "nodeName"; + String version = "0.0.1"; + String os = "DummyOS 1.0"; + final Sidecar sidecar = Sidecar.create( + nodeId, + nodeName, + NodeDetails.create( + os, + null, + null, + null, + null, + null, + null), + version + ); + + final Sidecar result = this.sidecarService.save(sidecar); + MongoCollection collection = mongodb.mongoConnection().getMongoDatabase().getCollection(collectionName); + Document document = collection.find().first(); + Document nodeDetails = document.get("node_details", Document.class); + + assertNotNull(result); + assertEquals(nodeId, document.get("node_id")); + assertEquals(nodeName, document.get("node_name")); + assertEquals(version, document.get("sidecar_version")); + assertEquals(os, nodeDetails.get("operating_system")); + } + + @Test + @MongoDBFixtures("collectorsMultipleDocuments.json") + public void testAll() throws Exception { + final List sidecars = this.sidecarService.all(); + + assertNotNull(sidecars); + assertEquals(3, sidecars.size()); + } + + @Test + public void testAllEmptyCollection() throws Exception { + final List sidecars = this.sidecarService.all(); + + assertNotNull(sidecars); + assertEquals(0, sidecars.size()); + } + + @Test + @MongoDBFixtures("collectorsMultipleDocuments.json") + public void testFindById() throws Exception { + final String collector1id = "uniqueid1"; + + final Sidecar sidecar = this.sidecarService.findByNodeId(collector1id); + + assertNotNull(sidecar); + assertEquals(collector1id, sidecar.nodeId()); + } + + @Test + @MongoDBFixtures("collectorsMultipleDocuments.json") + public void testFindByIdNonexisting() throws Exception { + final String collector1id = "nonexisting"; + + final Sidecar sidecar = this.sidecarService.findByNodeId(collector1id); + + assertNull(sidecar); + } + + @Test + @MongoDBFixtures("collectorsMultipleDocuments.json") + public void testDestroy() throws Exception { + final Sidecar sidecar = mock(Sidecar.class); + when(sidecar.id()).thenReturn("581b3bff8e4dc4270055dfcb"); + + final int result = this.sidecarService.delete(sidecar.id()); + assertEquals(1, result); + assertEquals(2, mongodb.mongoConnection().getMongoDatabase().getCollection(collectionName).countDocuments()); + } + + @Test + public void simpleTagAssignment() { + final Configuration configuration = getConfiguration(); + when(configurationService.findByTags(anySet())).thenReturn(List.of(configuration)); + final Collector collector = getCollector(); + + when(collectorService.all()).thenReturn(List.of(collector)); + Sidecar sidecar = getTestSidecar(); + sidecar = sidecar.toBuilder().nodeDetails(getNodeDetails("linux", Set.of("tag1"))).build(); + + sidecar = sidecarService.updateTaggedConfigurationAssignments(sidecar); + + assertThat(sidecar.assignments()).hasSize(1); + assertThat(sidecar.assignments().get(0).assignedFromTags()).isEqualTo(Set.of("tag1")); + } + + @Test + public void mergeWithManualAssignments() { + final Configuration configuration = getConfiguration(); + when(configurationService.findByTags(anySet())).thenReturn(List.of(configuration)); + final Collector collector = getCollector(); + + when(collectorService.all()).thenReturn(List.of(collector)); + Sidecar sidecar = getTestSidecar(); + sidecar = sidecar.toBuilder().nodeDetails(getNodeDetails("linux", Set.of("tag1"))) + .assignments(List.of(ConfigurationAssignment.create("some collector", "some config", null))) + .build(); + + sidecar = sidecarService.updateTaggedConfigurationAssignments(sidecar); + + assertThat(sidecar.assignments()).hasSize(2); + assertThat(sidecar.assignments()).satisfies(assignments -> { + assertThat(assignments.stream().filter(a -> a.assignedFromTags().equals(Set.of())).findAny()).isPresent(); + assertThat(assignments.stream().filter(a -> a.assignedFromTags().equals(Set.of("tag1"))).findAny()).isPresent(); + }); + } + + @Test + public void updateExistingAssignment() { + final Configuration configuration = getConfiguration(); + when(configurationService.findByTags(anySet())).thenReturn(List.of(configuration)); + final Collector collector = getCollector(); + + when(collectorService.all()).thenReturn(List.of(collector)); + Sidecar sidecar = getTestSidecar(); + final ConfigurationAssignment existingAssignment = ConfigurationAssignment.create(collector.id(), configuration.id(), Set.of("tag1")); + sidecar = sidecar.toBuilder().nodeDetails(getNodeDetails("linux", Set.of("tag1"))) + .assignments(List.of(existingAssignment)) + .build(); + + sidecar = sidecarService.updateTaggedConfigurationAssignments(sidecar); + + assertThat(sidecar.assignments()).hasSize(1); + assertThat(sidecar.assignments()).first().isEqualTo(existingAssignment); + } + + @Test + public void updateExistingAssignments() { + final Configuration configuration = getConfiguration(); + when(configurationService.findByTags(anySet())).thenReturn(List.of(configuration)); + final Collector collector = getCollector(); + + when(collectorService.all()).thenReturn(List.of(collector)); + Sidecar sidecar = getTestSidecar(); + final ConfigurationAssignment existingTag3Assignment = ConfigurationAssignment.create(collector.id(), configuration.id(), Set.of("tag3")); + final ConfigurationAssignment existingManualAssignment = ConfigurationAssignment.create("some-collector", "some-config", null); + sidecar = sidecar.toBuilder().nodeDetails(getNodeDetails("linux", Set.of("tag1"))) + .assignments(List.of(existingTag3Assignment, existingManualAssignment)) + .build(); + + sidecar = sidecarService.updateTaggedConfigurationAssignments(sidecar); + + assertThat(sidecar.assignments()).hasSize(2); + assertThat(sidecar.assignments()).satisfies(assignments -> { + assertThat(assignments.stream().filter(a -> a.assignedFromTags().equals(Set.of())).findAny()).isPresent(); + assertThat(assignments.stream().filter(a -> a.assignedFromTags().equals(Set.of("tag3"))).findAny()).isEmpty(); + assertThat(assignments.stream().filter(a -> a.assignedFromTags().equals(Set.of("tag1"))).findAny()).isPresent(); + }); + } + + @Test + public void ignoresTagsWithWrongOS() { + final Configuration configuration = getConfiguration(); + when(configurationService.findByTags(anySet())).thenReturn(List.of(configuration)); + final Collector collector = getCollector(); + + when(collectorService.all()).thenReturn(List.of(collector)); + Sidecar sidecar = getTestSidecar(); + sidecar = sidecar.toBuilder().nodeDetails(getNodeDetails("windows", Set.of("tag1"))).build(); + + sidecar = sidecarService.updateTaggedConfigurationAssignments(sidecar); + + // The tagged config is linux only + assertThat(sidecar.assignments()).hasSize(0); + } + + @Test + public void replacesManualAssignmentsWithTaggedOnes() { + final Configuration configuration = getConfiguration(); + when(configurationService.findByTags(anySet())).thenReturn(List.of(configuration)); + final Collector collector = getCollector(); + + when(collectorService.all()).thenReturn(List.of(collector)); + Sidecar sidecar = getTestSidecar(); + final ConfigurationAssignment manualAssignment = ConfigurationAssignment.create(collector.id(), configuration.id(), null); + sidecar = sidecar.toBuilder().nodeDetails(getNodeDetails("linux", Set.of("tag1"))) + .assignments(List.of(manualAssignment)) + .build(); + + sidecar = sidecarService.updateTaggedConfigurationAssignments(sidecar); + + assertThat(sidecar.assignments()).hasSize(1); + assertThat(sidecar.assignments()).satisfies(assignments -> { + assertThat(assignments.stream().filter(a -> a.assignedFromTags().equals(Set.of("tag1"))).findAny()).isPresent(); + }); + } + + @Test + @MongoDBFixtures("sidecars.json") + public void applyManualAssignment() throws NotFoundException { + Sidecar sidecar = sidecarService.findByNodeId("node-id"); + + final Configuration configuration = getConfiguration(); + when(configurationService.find(anyString())).thenReturn(configuration); + final Collector collector = getCollector(); + when(collectorService.find(anyString())).thenReturn(collector); + + assertThat(sidecar.assignments()).isEmpty(); + final ConfigurationAssignment manualAssignment = ConfigurationAssignment.create(collector.id(), configuration.id(), null); + sidecar = sidecarService.applyManualAssignments(sidecar.nodeId(), List.of(manualAssignment)); + + assertThat(sidecar.assignments()).hasSize(1); + } + + @Test + @MongoDBFixtures("sidecars.json") + public void applyManualAssignmentKeepTagged() throws NotFoundException { + Sidecar sidecar = sidecarService.findByNodeId("node-id"); + final ConfigurationAssignment taggedAssignment = ConfigurationAssignment.create("some-collector", "some-config", Set.of("tag")); + sidecar = sidecarService.save(sidecar.toBuilder().assignments(List.of(taggedAssignment)).build()); + + final Configuration configuration = getConfiguration(); + when(configurationService.find(anyString())).thenReturn(configuration); + final Collector collector = getCollector(); + when(collectorService.find(anyString())).thenReturn(collector); + + assertThat(sidecar.assignments()).hasSize(1); + final ConfigurationAssignment manualAssignment = ConfigurationAssignment.create(collector.id(), configuration.id(), null); + sidecar = sidecarService.applyManualAssignments(sidecar.nodeId(), List.of(manualAssignment)); + + assertThat(sidecar.assignments()).hasSize(2); + } + + @Test + @MongoDBFixtures("sidecars.json") + public void ignoreModificationOfTaggedAssignments() throws NotFoundException { + Sidecar sidecar = sidecarService.findByNodeId("node-id"); + + final Configuration configuration = getConfiguration(); + when(configurationService.find(anyString())).thenReturn(configuration); + final Collector collector = getCollector(); + when(collectorService.find(anyString())).thenReturn(collector); + final ConfigurationAssignment taggedAssignment = ConfigurationAssignment.create(collector.id(), configuration.id(), Set.of("tag")); + sidecar = sidecarService.save(sidecar.toBuilder().assignments(List.of(taggedAssignment)).build()); + + + assertThat(sidecar.assignments()).hasSize(1); + final ConfigurationAssignment manualAssignment = ConfigurationAssignment.create(collector.id(), configuration.id(), null); + sidecar = sidecarService.applyManualAssignments(sidecar.nodeId(), List.of(manualAssignment)); + + assertThat(sidecar.assignments()).hasSize(1); + // Tagged assignment is kept intact + assertThat(sidecar.assignments().get(0).assignedFromTags()).isEqualTo(Set.of("tag")); + } + + private static Configuration getConfiguration() { + return Configuration.create("config-id", "collector-id", "config-name", "color", "template", Set.of("tag1")); + } + + private static Collector getCollector() { + return Collector.create("collector-id", "collector-name", "service", "linux", + "/path", "param", "valid param", ""); + } + + private Sidecar getTestSidecar() { + return Sidecar.create( + "node-id", + "node-name", + getNodeDetails("linux", null), + "1.3.0" + ); + } + + private NodeDetails getNodeDetails(String os, Set tags) { + return NodeDetails.create( + os, + null, + null, + null, + null, + tags, + null); + } +} diff --git a/graylog2-server/src/test/resources/org/graylog/plugins/sidecar/collectors/collectorsSingleDataset.json b/graylog2-server/src/test/resources/org/graylog/plugins/sidecar/collectors/collectorsSingleDataset.json deleted file mode 100644 index 6b906729885d..000000000000 --- a/graylog2-server/src/test/resources/org/graylog/plugins/sidecar/collectors/collectorsSingleDataset.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "sidecars": [ - { - "_id": "581b3bff8e4dc4270055dfcb", - "node_id": "nodeId", - "node_name": "nodeName", - "sidecar_version": "0.0.1", - "node_details": { - "operating_system": "DummyOS 1.0" - }, - "last_seen": "" - } - ] -} diff --git a/graylog2-server/src/test/resources/org/graylog/plugins/sidecar/collectors/sidecars.json b/graylog2-server/src/test/resources/org/graylog/plugins/sidecar/collectors/sidecars.json new file mode 100644 index 000000000000..d47fc9369ac3 --- /dev/null +++ b/graylog2-server/src/test/resources/org/graylog/plugins/sidecar/collectors/sidecars.json @@ -0,0 +1,16 @@ +{ + "sidecars": [ + { + "_id": { + "$oid": "581b3bff8e4dc4270055dfcb" + }, + "node_id": "node-id", + "node_name": "node-name", + "sidecar_version": "1.3.0", + "node_details": { + "operating_system": "linux" + }, + "last_seen": "2015-04-01T11:50:20.195Z" + } + ] +} From 835a9357b3a0eb4ab49c5586034f18d1c4180330 Mon Sep 17 00:00:00 2001 From: Marco Pfatschbacher Date: Wed, 21 Sep 2022 17:57:21 +0200 Subject: [PATCH 26/82] Fix cache invalidation for collector actions (#13504) * Fix cache invalidation for collector actions The registration cache was using the sidecar "_id", while the ActionService was invalidating the sidecar "node_id". Change the entire cache to using the "node_id" * Rename parameter from sidecarId to sidecarNodeId Co-authored-by: Othello Maurer --- .../sidecar/rest/requests/BulkActionRequest.java | 6 +++--- .../rest/resources/AdministrationResource.java | 7 +------ .../sidecar/rest/resources/SidecarResource.java | 6 +++--- .../plugins/sidecar/services/ActionService.java | 10 ++++++++-- .../plugins/sidecar/services/EtagService.java | 14 +++++++------- 5 files changed, 22 insertions(+), 21 deletions(-) diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/requests/BulkActionRequest.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/requests/BulkActionRequest.java index 37c3f6f42cfb..594c3218cc51 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/requests/BulkActionRequest.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/requests/BulkActionRequest.java @@ -33,8 +33,8 @@ public abstract class BulkActionRequest { public abstract List collectorIds(); @JsonCreator - public static BulkActionRequest create(@JsonProperty("sidecar_id") String collectorId, + public static BulkActionRequest create(@JsonProperty("sidecar_id") String sidecarId, @JsonProperty("collector_ids") List collectorIds) { - return new AutoValue_BulkActionRequest(collectorId, collectorIds); + return new AutoValue_BulkActionRequest(sidecarId, collectorIds); } -} \ No newline at end of file +} diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/AdministrationResource.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/AdministrationResource.java index 951aa770faa7..32b30f0d7d25 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/AdministrationResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/AdministrationResource.java @@ -44,7 +44,6 @@ import org.graylog.plugins.sidecar.services.ActionService; import org.graylog.plugins.sidecar.services.CollectorService; import org.graylog.plugins.sidecar.services.ConfigurationService; -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; @@ -91,7 +90,6 @@ public class AdministrationResource extends RestResource implements PluginRestRe private final AdministrationFiltersFactory administrationFiltersFactory; private final ActiveSidecarFilter activeSidecarFilter; private final SidecarStatusMapper sidecarStatusMapper; - private final EtagService etagService; @Inject public AdministrationResource(SidecarService sidecarService, @@ -100,8 +98,7 @@ public AdministrationResource(SidecarService sidecarService, ActionService actionService, AdministrationFiltersFactory administrationFiltersFactory, ClusterConfigService clusterConfigService, - SidecarStatusMapper sidecarStatusMapper, - EtagService etagService) { + SidecarStatusMapper sidecarStatusMapper) { final SidecarConfiguration sidecarConfiguration = clusterConfigService.getOrDefault(SidecarConfiguration.class, SidecarConfiguration.defaultConfiguration()); this.sidecarService = sidecarService; this.configurationService = configurationService; @@ -109,7 +106,6 @@ public AdministrationResource(SidecarService sidecarService, this.actionService = actionService; this.administrationFiltersFactory = administrationFiltersFactory; this.sidecarStatusMapper = sidecarStatusMapper; - this.etagService = etagService; this.activeSidecarFilter = new ActiveSidecarFilter(sidecarConfiguration.sidecarInactiveThreshold()); this.searchQueryParser = new SearchQueryParser(Sidecar.FIELD_NODE_NAME, SidecarResource.SEARCH_FIELD_MAPPING); } @@ -170,7 +166,6 @@ public Response setAction(@ApiParam(name = "JSON body", required = true) .collect(Collectors.toList()); final CollectorActions collectorActions = actionService.fromRequest(bulkActionRequest.sidecarId(), actions); actionService.saveAction(collectorActions); - etagService.invalidateRegistration(bulkActionRequest.sidecarId()); } return Response.accepted().build(); diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/SidecarResource.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/SidecarResource.java index 6cfbbc693991..b91e2081dbc3 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/SidecarResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/SidecarResource.java @@ -223,7 +223,7 @@ public Response register(@ApiParam(name = "sidecarId", value = "The id this Side // If the sidecar has the recent registration, return with HTTP 304 if (ifNoneMatch != null) { EntityTag etag = new EntityTag(ifNoneMatch.replaceAll("\"", "")); - if (etagService.registrationIsCached(sidecar.id(), etag.toString())) { + if (etagService.registrationIsCached(sidecar.nodeId(), etag.toString())) { sidecarService.save(sidecar); return Response.notModified().tag(etag).build(); } @@ -247,7 +247,7 @@ public Response register(@ApiParam(name = "sidecarId", value = "The id this Side sidecar.assignments()); // add new etag to cache EntityTag registrationEtag = etagService.buildEntityTagForResponse(sidecarRegistrationResponse); - etagService.addSidecarRegistration(sidecar.id(), registrationEtag.toString()); + etagService.addSidecarRegistration(sidecar.nodeId(), registrationEtag.toString()); return Response.accepted(sidecarRegistrationResponse).tag(registrationEtag).build(); } @@ -273,7 +273,7 @@ public Response assignConfiguration(@ApiParam(name = "JSON body", required = tru try { Sidecar sidecar = sidecarService.applyManualAssignments(nodeId, nodeRelations); sidecarService.save(sidecar); - etagService.invalidateRegistration(sidecar.id()); + etagService.invalidateRegistration(sidecar.nodeId()); } catch (org.graylog2.database.NotFoundException e) { throw new NotFoundException(e.getMessage()); } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ActionService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ActionService.java index 91fd257118a9..9167adb44edd 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ActionService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/ActionService.java @@ -35,9 +35,13 @@ public class ActionService { private static final String COLLECTION_NAME = "sidecar_collector_actions"; private final JacksonDBCollection dbCollection; + private final EtagService etagService; + @Inject public ActionService(MongoConnection mongoConnection, - MongoJackObjectMapperProvider mapper){ + MongoJackObjectMapperProvider mapper, + EtagService etagService){ + this.etagService = etagService; dbCollection = JacksonDBCollection.wrap( mongoConnection.getDatabase().getCollection(COLLECTION_NAME), CollectorActions.class, @@ -70,7 +74,7 @@ public CollectorActions fromRequest(String sidecarId, List acti } public CollectorActions saveAction(CollectorActions collectorActions) { - return dbCollection.findAndModify( + final CollectorActions actions = dbCollection.findAndModify( DBQuery.is("sidecar_id", collectorActions.sidecarId()), new BasicDBObject(), new BasicDBObject(), @@ -78,6 +82,8 @@ public CollectorActions saveAction(CollectorActions collectorActions) { collectorActions, true, true); + etagService.invalidateRegistration(collectorActions.sidecarId()); + return actions; } public CollectorActions findActionBySidecar(String sidecarId, boolean remove) { diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagService.java index a833c4761f52..8b8b4ecfc360 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/EtagService.java @@ -120,8 +120,8 @@ public boolean configurationsAreCached(String etag) { return configurationCache.getIfPresent(etag) != null; } - public boolean registrationIsCached(String sidecarId, String etag) { - return etag.equals(registrationCache.getIfPresent(sidecarId)); + public boolean registrationIsCached(String sidecarNodeId, String etag) { + return etag.equals(registrationCache.getIfPresent(sidecarNodeId)); } public void registerCollector(String etag) { @@ -132,8 +132,8 @@ public void registerConfiguration(String etag) { configurationCache.put(etag, Boolean.TRUE); } - public void addSidecarRegistration(String sidecarId, String etag) { - registrationCache.put(sidecarId, etag); + public void addSidecarRegistration(String sidecarNodeId, String etag) { + registrationCache.put(sidecarNodeId, etag); } @@ -152,9 +152,9 @@ public void invalidateAllRegistrations() { clusterEventBus.post(EtagCacheInvalidation.create(CacheContext.REGISTRATION, "")); } - public void invalidateRegistration(String sidecarId) { - registrationCache.invalidate(sidecarId); - clusterEventBus.post(EtagCacheInvalidation.create(CacheContext.REGISTRATION, sidecarId)); + public void invalidateRegistration(String sidecarNodeId) { + registrationCache.invalidate(sidecarNodeId); + clusterEventBus.post(EtagCacheInvalidation.create(CacheContext.REGISTRATION, sidecarNodeId)); } public EntityTag buildEntityTagForResponse(Object o) throws JsonProcessingException { From 1e474944503bb3f75f4f9878327b5e82b8c5547c Mon Sep 17 00:00:00 2001 From: Othello Maurer Date: Thu, 22 Sep 2022 15:37:01 +0200 Subject: [PATCH 27/82] Invalidate registration cache only for affected sidecars (#13514) * Invalidate registration cache only for affected sidecars * Use node-id for cache invalidation The cache key has been changed since this PR has been started. * Consider OS for cache invalidation Also use symmetric difference of tags to further reduce unnecessary invalidations * Invalidate less when creating new configs --- .../rest/resources/ConfigurationResource.java | 25 ++++++++++++++++--- .../sidecar/services/SidecarService.java | 8 ++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationResource.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationResource.java index 808bfe08e0f0..312937e1d101 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationResource.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/rest/resources/ConfigurationResource.java @@ -19,6 +19,7 @@ import com.codahale.metrics.annotation.Timed; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -26,6 +27,7 @@ import org.apache.shiro.authz.annotation.RequiresPermissions; import org.graylog.plugins.sidecar.audit.SidecarAuditEventTypes; import org.graylog.plugins.sidecar.permissions.SidecarRestPermissions; +import org.graylog.plugins.sidecar.rest.models.Collector; import org.graylog.plugins.sidecar.rest.models.CollectorUpload; import org.graylog.plugins.sidecar.rest.models.Configuration; import org.graylog.plugins.sidecar.rest.models.ConfigurationSummary; @@ -36,6 +38,7 @@ import org.graylog.plugins.sidecar.rest.responses.ConfigurationListResponse; import org.graylog.plugins.sidecar.rest.responses.ConfigurationPreviewRenderResponse; import org.graylog.plugins.sidecar.rest.responses.ConfigurationSidecarsResponse; +import org.graylog.plugins.sidecar.services.CollectorService; import org.graylog.plugins.sidecar.services.ConfigurationService; import org.graylog.plugins.sidecar.services.EtagService; import org.graylog.plugins.sidecar.services.ImportService; @@ -77,6 +80,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -98,6 +102,7 @@ public class ConfigurationResource extends RestResource implements PluginRestRes private final SidecarService sidecarService; private final EtagService etagService; private final ImportService importService; + private final CollectorService collectorService; private final SearchQueryParser searchQueryParser; private static final ImmutableMap SEARCH_FIELD_MAPPING = ImmutableMap.builder() .put("id", SearchQueryField.create(Configuration.FIELD_ID)) @@ -109,12 +114,14 @@ public class ConfigurationResource extends RestResource implements PluginRestRes public ConfigurationResource(ConfigurationService configurationService, SidecarService sidecarService, EtagService etagService, - ImportService importService) { + ImportService importService, + CollectorService collectorService) { this.configurationService = configurationService; this.sidecarService = sidecarService; this.etagService = etagService; this.importService = importService; - this.searchQueryParser = new SearchQueryParser(Configuration.FIELD_NAME, SEARCH_FIELD_MAPPING);; + this.collectorService = collectorService; + this.searchQueryParser = new SearchQueryParser(Configuration.FIELD_NAME, SEARCH_FIELD_MAPPING); } @GET @@ -282,7 +289,11 @@ public Response createConfiguration(@ApiParam(name = "JSON body", required = tru final Configuration config = configurationService.save(configuration); if (!config.tags().isEmpty()) { - etagService.invalidateAllRegistrations(); + final String os = Optional.ofNullable(collectorService.find(request.collectorId())) + .map(Collector::nodeOperatingSystem).orElse(""); + sidecarService.findByTagsAndOS(config.tags(), os) + .map(Sidecar::nodeId) + .forEach(etagService::invalidateRegistration); } return Response.ok().entity(config).build(); @@ -333,8 +344,14 @@ public Response updateConfiguration(@ApiParam(name = "id", required = true) return Response.status(Response.Status.BAD_REQUEST).entity(validationResult).build(); } etagService.invalidateAllConfigurations(); + if (! previousConfiguration.tags().equals(updatedConfiguration.tags())) { - etagService.invalidateAllRegistrations(); + final Set tags = Sets.symmetricDifference(previousConfiguration.tags(), updatedConfiguration.tags()); + final String os = Optional.ofNullable(collectorService.find(request.collectorId())) + .map(Collector::nodeOperatingSystem).orElse(""); + sidecarService.findByTagsAndOS(tags, os) + .map(Sidecar::nodeId) + .forEach(etagService::invalidateRegistration); } return Response.ok().entity(configurationService.save(updatedConfiguration)).build(); diff --git a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java index 4c886ea10007..6871c2aa2cea 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/sidecar/services/SidecarService.java @@ -49,6 +49,7 @@ import java.util.List; import java.util.Set; import java.util.function.Predicate; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -274,4 +275,11 @@ public List toSummaryList(List sidecars, Predicate collector.toSummary(isActiveFunction)) .collect(Collectors.toList()); } + + public Stream findByTagsAndOS(Collection tags, String os) { + return streamQuery(DBQuery.and( + DBQuery.in("node_details.tags", tags), + DBQuery.regex("node_details.operating_system", Pattern.compile("^" + Pattern.quote(os) + "$", Pattern.CASE_INSENSITIVE)) + )); + } } From c8abf7b73a7781a426560c3f8ac976f7128b35ef Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Thu, 22 Sep 2022 18:24:14 +0200 Subject: [PATCH 28/82] added logic to CollectorConfigurationModal to support multiple sidecars --- .../CollectorConfigurationModal.tsx | 91 +++++++++++++++---- 1 file changed, 73 insertions(+), 18 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 4cdd740431d8..96c0a50ad581 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -54,14 +54,23 @@ const ConfigurationSummary = styled.div` word-break: break-all; `; +const SecondaryText = styled.div` + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + color: #aaaaaa; + margin-top: -6px; + margin-bottom: -2px; +`; + const TableRow = styled.tr` cursor: pointer; border-bottom: 1px solid lightgray; - height: 32px; + height: 49px; `; const StickyTableRowFooter = styled.tr` - height: 34px; + height: 32px; position: sticky; bottom: 0; `; @@ -104,12 +113,23 @@ const CollectorConfigurationModal = (props) => { const modalConfirm = React.useRef(null); - const getAssignedConfigurations = (selectedSidecarCollectorPairs, configurations) => { + const getAssignedConfigurations = () => { + const { selectedSidecarCollectorPairs, configurations } = props; const assignments = selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar).reduce((accumulator, sidecar) => accumulator.concat(sidecar.assignments), []); return assignments.map((assignment) => configurations.find((configuration) => configuration.id === assignment.configuration_id)); }; + const getFullyAndPartiallyAssignments = (_assignedConfigurations) => { + const { selectedSidecarCollectorPairs } = props; + const occurrences = lodash.countBy(_assignedConfigurations); + + return [ + lodash.uniq(_assignedConfigurations.filter((a) => occurrences[a] === selectedSidecarCollectorPairs.length)) as any[], + lodash.uniq(_assignedConfigurations.filter((a) => occurrences[a] < selectedSidecarCollectorPairs.length)) as any[], + ]; + }; + const onSave = (configurationNames: string[]) => { setNextAssignedConfigurations(configurationNames); modalConfirm.current.open(); @@ -137,6 +157,12 @@ const CollectorConfigurationModal = (props) => { return props.collectors.find((b) => b.id === configuration.collector_id); }; + const getSidecars = (name: string) => { + const configuration = getConfiguration(name); + + return props.selectedSidecarCollectorPairs.filter(({ sidecar }) => sidecar.assignments.map((s) => s.configuration_id).includes(configuration.id)).map((a) => a.sidecar.node_name); + }; + const renderConfigurationSummary = (_previousAssignedConfigurations, _nextAssignedConfigurations, _selectedSidecarCollectorPairs) => { const toAdd = lodash.difference(_nextAssignedConfigurations, _previousAssignedConfigurations); const toRemove = lodash.difference(_previousAssignedConfigurations, _nextAssignedConfigurations); @@ -180,41 +206,66 @@ const CollectorConfigurationModal = (props) => { // Do not allow configuration changes when more than one log collector type is selected const selectedLogCollectors = lodash.uniq(selectedSidecarCollectorPairs.map(({ collector }) => collector)) as any[]; - const assignedConfigurations = getAssignedConfigurations(selectedSidecarCollectorPairs, configurations) + const assignedConfigurations = getAssignedConfigurations() .filter((configuration) => selectedLogCollectors[0]?.id === configuration.collector_id) .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) .map((config) => config.name); + const [fullyAssignedConfigurations, partiallyAssignedConfigurations] = getFullyAndPartiallyAssignments(assignedConfigurations); + const nonAssignedConfigurations = configurations.filter((c) => !assignedConfigurations.includes(c.name)); const MemoizedModalForm = React.useMemo(() => { // eslint-disable-next-line react/no-unstable-nested-components const ModalForm = () => { const [searchQuery, setSearchQuery] = React.useState(''); - const [selectedConfigurations, setSelectedConfigurations] = React.useState(assignedConfigurations); + const [selectedConfigurations, setSelectedConfigurations] = React.useState(fullyAssignedConfigurations); + const [partiallySelectedConfigurations, setPartiallySelectedConfigurations] = React.useState(partiallyAssignedConfigurations); + + const onReset = () => { + setSelectedConfigurations(fullyAssignedConfigurations); + setPartiallySelectedConfigurations(partiallyAssignedConfigurations); + }; - const isSelected = (name: string) => selectedConfigurations.includes(name); + const isNotDirty = lodash.isEqual(selectedConfigurations, fullyAssignedConfigurations) && lodash.isEqual(partiallySelectedConfigurations, partiallyAssignedConfigurations); - const isNotDirty = lodash.isEqual(selectedConfigurations, assignedConfigurations); + const allSidecarNames = selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name); const options = nonAssignedConfigurations .filter((configuration) => (selectedLogCollectors[0]?.id === configuration.collector_id)) .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) .map((c) => c.name); - const filteredOptions = [...assignedConfigurations, ...options].filter((configuration) => configuration.match(new RegExp(searchQuery, 'i'))); + const filteredOptions = [...fullyAssignedConfigurations, ...partiallyAssignedConfigurations, ...options].filter((configuration) => configuration.match(new RegExp(searchQuery, 'i'))); - const rows = filteredOptions.map((option) => { - const selected = isSelected(option); - const config = getConfiguration(option); - const collector = getCollector(option); + const rows = filteredOptions.map((configName) => { + const config = getConfiguration(configName); + const collector = getCollector(configName); + const sidecars = getSidecars(configName); + const selected = selectedConfigurations.includes(configName); + const partiallySelected = !selected && partiallySelectedConfigurations.includes(configName); + const secondaryText = (selected && allSidecarNames.join(', ')) || (partiallySelected && sidecars.join(', ')) || ''; return ( - setSelectedConfigurations(selected ? selectedConfigurations.filter((c) => c !== option) : [...selectedConfigurations, option])}> - {selected && } + { + if (partiallySelected) { + setPartiallySelectedConfigurations(partiallySelectedConfigurations.filter((c) => c !== configName)); + } else { + setSelectedConfigurations(selected ? selectedConfigurations.filter((c) => c !== configName) : [...selectedConfigurations, configName]); + } + }}> + + {selected && } + {partiallySelected && } + - {option} + + {configName} + + {secondaryText} + + {collector @@ -222,7 +273,7 @@ const CollectorConfigurationModal = (props) => { : Unknown collector} - {selected && } + {(selected || partiallySelected) && } ); }); @@ -233,7 +284,10 @@ const CollectorConfigurationModal = (props) => { Edit {selectedLogCollectors[0]?.name} Configurations - sidecars: {selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name).join(', ')} + + {`${allSidecarNames.length} sidecar${allSidecarNames.length > 1 ? 's' : ''}: `} + {allSidecarNames.join(', ')} + @@ -264,6 +318,7 @@ const CollectorConfigurationModal = (props) => { + From 5e40ca219cdc26cfb88b779eb1816becea9862e7 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Fri, 23 Sep 2022 11:09:18 +0200 Subject: [PATCH 29/82] onReset resets the search form too --- .../sidecars/administration/CollectorConfigurationModal.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 96c0a50ad581..bad011363631 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -225,6 +225,7 @@ const CollectorConfigurationModal = (props) => { const onReset = () => { setSelectedConfigurations(fullyAssignedConfigurations); setPartiallySelectedConfigurations(partiallyAssignedConfigurations); + setSearchQuery(''); }; const isNotDirty = lodash.isEqual(selectedConfigurations, fullyAssignedConfigurations) && lodash.isEqual(partiallySelectedConfigurations, partiallyAssignedConfigurations); @@ -292,7 +293,7 @@ const CollectorConfigurationModal = (props) => { - setSearchQuery(q)} topMargin={0} queryWidth="100%" /> + setSearchQuery(q)} topMargin={0} queryWidth="100%" /> From 40c5af32b02c6fc5e7fe2320a5e1a52b4b0a17e1 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Fri, 23 Sep 2022 17:27:16 +0200 Subject: [PATCH 30/82] updated confirmConfigurationChange fnct to support multiple sidecars --- .../CollectorConfigurationModal.tsx | 56 +++++++++++-------- .../CollectorsAdministration.jsx | 7 +-- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index bad011363631..8fed6a3c828f 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -110,18 +110,25 @@ const ModalSubTitle = styled.div` const CollectorConfigurationModal = (props) => { const [nextAssignedConfigurations, setNextAssignedConfigurations] = React.useState([]); + const [nextPartiallyAssignedConfigurations, setNextPartiallyAssignedConfigurations] = React.useState([]); const modalConfirm = React.useRef(null); - const getAssignedConfigurations = () => { - const { selectedSidecarCollectorPairs, configurations } = props; - const assignments = selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar).reduce((accumulator, sidecar) => accumulator.concat(sidecar.assignments), []); + const { configurations, selectedSidecarCollectorPairs, onConfigurationSelectionChange, onCancel, collectors } = props; - return assignments.map((assignment) => configurations.find((configuration) => configuration.id === assignment.configuration_id)); + // Do not allow configuration changes when more than one log collector type is selected + const selectedLogCollectors = lodash.uniq(selectedSidecarCollectorPairs.map(({ collector }) => collector)) as any[]; + + const getAssignedConfigurations = (_selectedSidecarCollectorPairs, _selectedLogCollectors) => { + const assignments = _selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar).reduce((accumulator, sidecar) => accumulator.concat(sidecar.assignments), []); + + return assignments.map((assignment) => configurations.find((configuration) => configuration.id === assignment.configuration_id)) + .filter((configuration) => _selectedLogCollectors[0]?.id === configuration.collector_id) + .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) + .map((config) => config.name); }; const getFullyAndPartiallyAssignments = (_assignedConfigurations) => { - const { selectedSidecarCollectorPairs } = props; const occurrences = lodash.countBy(_assignedConfigurations); return [ @@ -130,16 +137,29 @@ const CollectorConfigurationModal = (props) => { ]; }; - const onSave = (configurationNames: string[]) => { - setNextAssignedConfigurations(configurationNames); + const onSave = (fullyAssignedConfigs: string[], partiallyAssignedConfigs: string[]) => { + setNextAssignedConfigurations(fullyAssignedConfigs); + setNextPartiallyAssignedConfigurations(partiallyAssignedConfigs); modalConfirm.current.open(); }; const confirmConfigurationChange = (doneCallback) => { - const { onConfigurationSelectionChange, configurations, onCancel } = props; const assignedConfigurationsToSave = configurations.filter((c) => nextAssignedConfigurations.includes(c.name)); - onConfigurationSelectionChange(assignedConfigurationsToSave, doneCallback); + // eslint-disable-next-line no-restricted-syntax + for (const sidecarCollectorPair of selectedSidecarCollectorPairs) { + let configs = assignedConfigurationsToSave; + + if (nextPartiallyAssignedConfigurations.length) { + const assignments = getAssignedConfigurations([sidecarCollectorPair], selectedLogCollectors); + const assignmentsToKeep = lodash.intersection(assignments, nextPartiallyAssignedConfigurations); + const assignedConfigurationsToKeep = configurations.filter((c) => assignmentsToKeep.includes(c.name)); + configs = [...assignedConfigurationsToSave, ...assignedConfigurationsToKeep]; + } + + onConfigurationSelectionChange([sidecarCollectorPair], configs, doneCallback); + } + onCancel(); }; @@ -148,19 +168,19 @@ const CollectorConfigurationModal = (props) => { }; const getConfiguration = (name: string) => { - return props.configurations.find((c) => c.name === name); + return configurations.find((c) => c.name === name); }; const getCollector = (name: string) => { const configuration = getConfiguration(name); - return props.collectors.find((b) => b.id === configuration.collector_id); + return collectors.find((b) => b.id === configuration.collector_id); }; const getSidecars = (name: string) => { const configuration = getConfiguration(name); - return props.selectedSidecarCollectorPairs.filter(({ sidecar }) => sidecar.assignments.map((s) => s.configuration_id).includes(configuration.id)).map((a) => a.sidecar.node_name); + return selectedSidecarCollectorPairs.filter(({ sidecar }) => sidecar.assignments.map((s) => s.configuration_id).includes(configuration.id)).map((a) => a.sidecar.node_name); }; const renderConfigurationSummary = (_previousAssignedConfigurations, _nextAssignedConfigurations, _selectedSidecarCollectorPairs) => { @@ -201,15 +221,7 @@ const CollectorConfigurationModal = (props) => { ); }; - const { configurations, selectedSidecarCollectorPairs } = props; - - // Do not allow configuration changes when more than one log collector type is selected - const selectedLogCollectors = lodash.uniq(selectedSidecarCollectorPairs.map(({ collector }) => collector)) as any[]; - - const assignedConfigurations = getAssignedConfigurations() - .filter((configuration) => selectedLogCollectors[0]?.id === configuration.collector_id) - .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) - .map((config) => config.name); + const assignedConfigurations = getAssignedConfigurations(selectedSidecarCollectorPairs, selectedLogCollectors); const [fullyAssignedConfigurations, partiallyAssignedConfigurations] = getFullyAndPartiallyAssignments(assignedConfigurations); @@ -320,7 +332,7 @@ const CollectorConfigurationModal = (props) => { - + ); diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx index 5142ebd68416..73559a06b5d5 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx @@ -124,13 +124,8 @@ const CollectorsAdministration = createReactClass({ }, // eslint-disable-next-line react/sort-comp - handleConfigurationChange(selectedConfigurations, doneCallback) { - const { selected, enabledCollectors } = this.state; + handleConfigurationChange(selectedSidecars, selectedConfigurations, doneCallback) { const { onConfigurationChange } = this.props; - - const selectedSidecars = enabledCollectors - .filter(({ sidecar, collector }) => selected.includes(this.sidecarCollectorId(sidecar, collector))); - onConfigurationChange(selectedSidecars, selectedConfigurations, doneCallback); }, From 9b5d57b6f25f0e2143c156c63df0bfeaacf3c5fa Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Fri, 23 Sep 2022 17:37:13 +0200 Subject: [PATCH 31/82] simplify Configuration summary when multiple sidecars are selected --- .../sidecars/administration/CollectorConfigurationModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 8fed6a3c828f..27e0e0f31576 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -213,8 +213,8 @@ const CollectorConfigurationModal = (props) => { onConfirm={confirmConfigurationChange} onCancel={cancelConfigurationChange}> - {toAddSummary} - {toRemoveSummary} + {(_selectedSidecarCollectorPairs.length === 1) && toAddSummary} + {(_selectedSidecarCollectorPairs.length === 1) && toRemoveSummary}

Are you sure you want to proceed with this action for {formattedSummary}?

From 08101d10f3582cf1132bdaa464e3455972472895 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Mon, 26 Sep 2022 11:59:40 +0200 Subject: [PATCH 32/82] finalized confirmation summary --- .../CollectorConfigurationModal.tsx | 29 +++---------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 27e0e0f31576..5e25d8f01b3a 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -184,28 +184,9 @@ const CollectorConfigurationModal = (props) => { }; const renderConfigurationSummary = (_previousAssignedConfigurations, _nextAssignedConfigurations, _selectedSidecarCollectorPairs) => { - const toAdd = lodash.difference(_nextAssignedConfigurations, _previousAssignedConfigurations); - const toRemove = lodash.difference(_previousAssignedConfigurations, _nextAssignedConfigurations); - const exampleSidecarCollectorPair = _selectedSidecarCollectorPairs[0]; - const collectorIndicator = exampleSidecarCollectorPair ? ( - - - - ) : null; - - let toAddSummary; - let toRemoveSummary; - - if (toRemove.length > 0) { - toAddSummary =

You are going to remove the configuration {toRemove.join(', ')} for collector {collectorIndicator}

; - } - - if (toAdd.length > 0) { - toRemoveSummary =

You are going to apply the configuration {toAdd.join(', ')} for collector {collectorIndicator}

; - } - - const formattedSummary = _selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name).join(', '); + const sidecarsSummary = _selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name).join(', '); + const numberOfSidecarsSummary = `${_selectedSidecarCollectorPairs.length} sidecars`; + const summary = _selectedSidecarCollectorPairs.length <= 5 ? sidecarsSummary : numberOfSidecarsSummary; return ( { onConfirm={confirmConfigurationChange} onCancel={cancelConfigurationChange}> - {(_selectedSidecarCollectorPairs.length === 1) && toAddSummary} - {(_selectedSidecarCollectorPairs.length === 1) && toRemoveSummary} -

Are you sure you want to proceed with this action for {formattedSummary}?

+

Are you sure you want to proceed with this action for {summary}?

); From 9c31f0427af024f5c1968b05888cc17e879f1f38 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Mon, 26 Sep 2022 15:07:25 +0200 Subject: [PATCH 33/82] fixed exit dialog by ESC key bug --- .../sidecars/administration/CollectorConfigurationModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 5e25d8f01b3a..2671285ab361 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -271,7 +271,7 @@ const CollectorConfigurationModal = (props) => { }); return ( - + Edit {selectedLogCollectors[0]?.name} Configurations From 266a86266cfedeb1be20b744ac3984d96cd3bd01 Mon Sep 17 00:00:00 2001 From: Marco Pfatschbacher Date: Mon, 26 Sep 2022 15:06:29 +0200 Subject: [PATCH 34/82] Add description for tag variables --- .../sidecars/configuration-forms/TemplatesHelper.jsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx index 9063e7c07cfe..1032620152f6 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx @@ -54,6 +54,12 @@ class TemplatesHelper extends React.Component { {TemplatesHelper._buildVariableName('spoolDir')} A directory that is unique per configuration and can be used to store collector data. + + {TemplatesHelper._buildVariableName('tags.')} + A map of tags that are set for the sidecar. This can be used to render conditional configuration snippets. e.g.:
+ <#if sidecar.tags.webserver??>
  - /var/log/apache/*.log
</#if>
+ + From 83d1f3afc773e752b3f1710779e87505dafc3f86 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Thu, 29 Sep 2022 13:08:40 +0200 Subject: [PATCH 35/82] added Configuration Tags field in Collector Configuration page --- .../configuration-forms/ConfigurationForm.jsx | 22 +++++++- .../ConfigurationTagsSelect.jsx | 53 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.jsx diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx index 55fab55a4a4a..a447ea903040 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx @@ -30,6 +30,7 @@ import { CollectorsActions, CollectorsStore } from 'stores/sidecars/CollectorsSt import SourceViewModal from './SourceViewModal'; import ImportsViewModal from './ImportsViewModal'; +import ConfigurationTagsSelect from './ConfigurationTagsSelect'; const ConfigurationForm = createReactClass({ displayName: 'ConfigurationForm', @@ -65,7 +66,7 @@ const ConfigurationForm = createReactClass({ color: configuration.color, collector_id: configuration.collector_id, template: configuration.template || '', - tags: configuration.tags + tags: configuration.tags || [] }, }; }, @@ -153,6 +154,12 @@ const ConfigurationForm = createReactClass({ this._formDataUpdate('name')(nextName); }, + _onTagsChange(nextTags) { + const nextTagsArray = nextTags.split(','); + + this._formDataUpdate('tags')(nextTagsArray); + }, + _getCollectorDefaultTemplate(collectorId) { const storedTemplate = this.defaultTemplates[collectorId]; @@ -324,6 +331,19 @@ const ConfigurationForm = createReactClass({ Choose a color to use for this configuration. + + Configuration Tags +
+ ({ name: tag }))} + tags={formData.tags} + onChange={this._onTagsChange} + className="form-control" /> +
+ Choose tags to use for this configuration. +
+ Collector {this._renderCollectorTypeField(formData.collector_id, collectors, configurationSidecars)} diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.jsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.jsx new file mode 100644 index 000000000000..b0b674ffcf92 --- /dev/null +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.jsx @@ -0,0 +1,53 @@ +/* + * 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 + * . + */ +import PropTypes from 'prop-types'; +import React from 'react'; + +import MultiSelect from 'components/common/MultiSelect'; + +class ConfigurationTagsSelect extends React.Component { + static propTypes = { + tags: PropTypes.arrayOf(PropTypes.string), + availableTags: PropTypes.array.isRequired, + onChange: PropTypes.func.isRequired, + }; + + static defaultProps = { + tags: [], + }; + + getValue = () => { + return this.refs.select.getValue().split(','); + }; + + render() { + const tagsValue = this.props.tags.join(','); + const tagsOptions = this.props.availableTags.map((tag) => { + return { value: tag.name, label: tag.name }; + }); + return ( + + ); + } +} + +export default ConfigurationTagsSelect; From 253ab0469ad6dd4e9d3d541d1581353a433c4da0 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Thu, 29 Sep 2022 18:56:03 +0200 Subject: [PATCH 36/82] prototype: implement auto assigned tags in config selection modal --- .../CollectorConfigurationModal.tsx | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 2671285ab361..fc440ddfb2b0 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -230,7 +230,7 @@ const CollectorConfigurationModal = (props) => { const filteredOptions = [...fullyAssignedConfigurations, ...partiallyAssignedConfigurations, ...options].filter((configuration) => configuration.match(new RegExp(searchQuery, 'i'))); - const rows = filteredOptions.map((configName) => { + const rows = filteredOptions.map((configName, i) => { const config = getConfiguration(configName); const collector = getCollector(configName); const sidecars = getSidecars(configName); @@ -238,13 +238,19 @@ const CollectorConfigurationModal = (props) => { const partiallySelected = !selected && partiallySelectedConfigurations.includes(configName); const secondaryText = (selected && allSidecarNames.join(', ')) || (partiallySelected && sidecars.join(', ')) || ''; + const autoAssignedTags = ['tag1', 'tag2']; + const isAssignedFromTags = autoAssignedTags.length > 0 && i === 0; + return ( { - if (partiallySelected) { - setPartiallySelectedConfigurations(partiallySelectedConfigurations.filter((c) => c !== configName)); - } else { - setSelectedConfigurations(selected ? selectedConfigurations.filter((c) => c !== configName) : [...selectedConfigurations, configName]); + if (!isAssignedFromTags) { + if (partiallySelected) { + setPartiallySelectedConfigurations(partiallySelectedConfigurations.filter((c) => c !== configName)); + } else { + setSelectedConfigurations(selected ? selectedConfigurations.filter((c) => c !== configName) : [...selectedConfigurations, configName]); + } } }}> @@ -258,6 +264,9 @@ const CollectorConfigurationModal = (props) => { {secondaryText} + + {isAssignedFromTags && } + {collector @@ -290,7 +299,7 @@ const CollectorConfigurationModal = (props) => { {(rows.length === 0) ? ( - + No configurations available for the selected log collector. @@ -298,7 +307,7 @@ const CollectorConfigurationModal = (props) => { rows )} - +  Add a new configuration From 17125979664813d5ad993a48410ed585841dbfc4 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Fri, 30 Sep 2022 13:27:39 +0200 Subject: [PATCH 37/82] finalized tags UI implementation --- .../CollectorConfigurationModal.tsx | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index fc440ddfb2b0..6679ec51e31d 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -167,20 +167,28 @@ const CollectorConfigurationModal = (props) => { setNextAssignedConfigurations([]); }; - const getConfiguration = (name: string) => { - return configurations.find((c) => c.name === name); + const getConfiguration = (configName: string) => { + return configurations.find((c) => c.name === configName); }; - const getCollector = (name: string) => { - const configuration = getConfiguration(name); + const getCollector = (configName: string) => { + const configuration = getConfiguration(configName); return collectors.find((b) => b.id === configuration.collector_id); }; - const getSidecars = (name: string) => { - const configuration = getConfiguration(name); + const getSidecars = (configName: string) => { + const configuration = getConfiguration(configName); - return selectedSidecarCollectorPairs.filter(({ sidecar }) => sidecar.assignments.map((s) => s.configuration_id).includes(configuration.id)).map((a) => a.sidecar.node_name); + return selectedSidecarCollectorPairs.filter(({ sidecar }) => sidecar.assignments.map((s) => s.configuration_id).includes(configuration.id)).map((a) => a.sidecar); + }; + + const getAssignedFromTags = (configId: string, collectorId: string, sidecars) => { + if (sidecars.length === 1) { + return sidecars[0].assignments.find((a) => (a.collector_id === collectorId) && (a.configuration_id === configId)).assigned_from_tags; + } + + return []; }; const renderConfigurationSummary = (_previousAssignedConfigurations, _nextAssignedConfigurations, _selectedSidecarCollectorPairs) => { @@ -230,16 +238,16 @@ const CollectorConfigurationModal = (props) => { const filteredOptions = [...fullyAssignedConfigurations, ...partiallyAssignedConfigurations, ...options].filter((configuration) => configuration.match(new RegExp(searchQuery, 'i'))); - const rows = filteredOptions.map((configName, i) => { + const rows = filteredOptions.map((configName) => { const config = getConfiguration(configName); const collector = getCollector(configName); const sidecars = getSidecars(configName); + const autoAssignedTags = getAssignedFromTags(config.id, collector.id, sidecars); + const selected = selectedConfigurations.includes(configName); const partiallySelected = !selected && partiallySelectedConfigurations.includes(configName); - const secondaryText = (selected && allSidecarNames.join(', ')) || (partiallySelected && sidecars.join(', ')) || ''; - - const autoAssignedTags = ['tag1', 'tag2']; - const isAssignedFromTags = autoAssignedTags.length > 0 && i === 0; + const secondaryText = (selected && allSidecarNames.join(', ')) || (partiallySelected && sidecars.node_name.join(', ')) || ''; + const isAssignedFromTags = autoAssignedTags.length > 0; return ( Date: Fri, 30 Sep 2022 14:02:41 +0200 Subject: [PATCH 38/82] fix linter issues --- .../configuration-forms/ConfigurationForm.jsx | 14 ++++++++++---- .../ConfigurationTagsSelect.jsx | 8 ++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx index a447ea903040..277f6f9eae6a 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx @@ -16,6 +16,7 @@ */ import PropTypes from 'prop-types'; import React from 'react'; +// eslint-disable-next-line no-restricted-imports import createReactClass from 'create-react-class'; import Reflux from 'reflux'; import lodash from 'lodash'; @@ -33,8 +34,9 @@ import ImportsViewModal from './ImportsViewModal'; import ConfigurationTagsSelect from './ConfigurationTagsSelect'; const ConfigurationForm = createReactClass({ + // eslint-disable-next-line react/no-unused-class-component-methods displayName: 'ConfigurationForm', - + // eslint-disable-next-line react/no-unused-class-component-methods propTypes: { action: PropTypes.oneOf(['create', 'edit']), configuration: PropTypes.object, @@ -66,7 +68,7 @@ const ConfigurationForm = createReactClass({ color: configuration.color, collector_id: configuration.collector_id, template: configuration.template || '', - tags: configuration.tags || [] + tags: configuration.tags || [], }, }; }, @@ -135,6 +137,7 @@ const ConfigurationForm = createReactClass({ }; }, + // eslint-disable-next-line react/no-unused-class-component-methods replaceConfigurationVariableName(oldname, newname) { const { formData } = this.state; @@ -164,7 +167,9 @@ const ConfigurationForm = createReactClass({ const storedTemplate = this.defaultTemplates[collectorId]; if (storedTemplate !== undefined) { - return new Promise((resolve) => resolve(storedTemplate)); + return new Promise((resolve) => { + resolve(storedTemplate); + }); } return CollectorsActions.getCollector(collectorId).then((collector) => { @@ -248,6 +253,7 @@ const ConfigurationForm = createReactClass({ return options; }, + // eslint-disable-next-line react/no-unstable-nested-components _formatValidationMessage(fieldName, defaultText) { const { validation_errors: validationErrors } = this.state; @@ -268,6 +274,7 @@ const ConfigurationForm = createReactClass({ return null; }, + // eslint-disable-next-line react/no-unstable-nested-components _renderCollectorTypeField(collectorId, collectors, configurationSidecars) { const isConfigurationInUse = configurationSidecars.sidecar_ids && configurationSidecars.sidecar_ids.length > 0; @@ -335,7 +342,6 @@ const ConfigurationForm = createReactClass({ Configuration Tags
({ name: tag }))} tags={formData.tags} onChange={this._onTagsChange} diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.jsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.jsx index b0b674ffcf92..3950a8cd3065 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.jsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.jsx @@ -30,18 +30,14 @@ class ConfigurationTagsSelect extends React.Component { tags: [], }; - getValue = () => { - return this.refs.select.getValue().split(','); - }; - render() { const tagsValue = this.props.tags.join(','); const tagsOptions = this.props.availableTags.map((tag) => { return { value: tag.name, label: tag.name }; }); + return ( - Date: Fri, 30 Sep 2022 14:47:41 +0200 Subject: [PATCH 39/82] fix sidecars.node_name type error --- .../sidecars/administration/CollectorConfigurationModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 6679ec51e31d..e37dfa3ee228 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -246,7 +246,7 @@ const CollectorConfigurationModal = (props) => { const selected = selectedConfigurations.includes(configName); const partiallySelected = !selected && partiallySelectedConfigurations.includes(configName); - const secondaryText = (selected && allSidecarNames.join(', ')) || (partiallySelected && sidecars.node_name.join(', ')) || ''; + const secondaryText = (selected && allSidecarNames.join(', ')) || (partiallySelected && sidecars.map((s) => s.node_name).join(', ')) || ''; const isAssignedFromTags = autoAssignedTags.length > 0; return ( From ebf4b3babaebd5b7f274f2c53bba626d21f0e12c Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Fri, 30 Sep 2022 14:56:57 +0200 Subject: [PATCH 40/82] handled case assigned_from_tags with multiple sidecarse --- .../sidecars/administration/CollectorConfigurationModal.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index e37dfa3ee228..39e437cc5c30 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -184,11 +184,9 @@ const CollectorConfigurationModal = (props) => { }; const getAssignedFromTags = (configId: string, collectorId: string, sidecars) => { - if (sidecars.length === 1) { - return sidecars[0].assignments.find((a) => (a.collector_id === collectorId) && (a.configuration_id === configId)).assigned_from_tags; - } + const assigned_from_tags = sidecars.reduce((accumulator, sidecar) => accumulator.concat(sidecar.assignments.find((a) => (a.collector_id === collectorId) && (a.configuration_id === configId)).assigned_from_tags), []); - return []; + return lodash.uniq(assigned_from_tags); }; const renderConfigurationSummary = (_previousAssignedConfigurations, _nextAssignedConfigurations, _selectedSidecarCollectorPairs) => { From d3c62cb4482b027cae3db7945ed28942da1f1716 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Fri, 30 Sep 2022 15:07:19 +0200 Subject: [PATCH 41/82] hide close icon btn when isAssignedFromTags --- .../sidecars/administration/CollectorConfigurationModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 39e437cc5c30..764a729e13ca 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -280,7 +280,7 @@ const CollectorConfigurationModal = (props) => { : Unknown collector} - {(selected || partiallySelected) && } + {(selected || partiallySelected) && !isAssignedFromTags && } ); }); From 4772f8d9f7287304a98fbd008245f80f59692317 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Fri, 30 Sep 2022 15:13:44 +0200 Subject: [PATCH 42/82] fixed TemplatesHelper linter issues --- .../sidecars/configuration-forms/TemplatesHelper.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx index 1032620152f6..d9af3f57e4e6 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx @@ -56,8 +56,8 @@ class TemplatesHelper extends React.Component { {TemplatesHelper._buildVariableName('tags.')} - A map of tags that are set for the sidecar. This can be used to render conditional configuration snippets. e.g.:
- <#if sidecar.tags.webserver??>
  - /var/log/apache/*.log
</#if>
+ A map of tags that are set for the sidecar. This can be used to render conditional configuration snippets. e.g.:
+ <#if sidecar.tags.webserver??>
  - /var/log/apache/*.log
</#if>
From b9bd8bb960848fe6aa5bcf99f87109116e5683d7 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Fri, 30 Sep 2022 16:18:48 +0200 Subject: [PATCH 43/82] cleanup --- .../CollectorConfigurationModal.tsx | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 764a729e13ca..b35e16cdde31 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -146,8 +146,7 @@ const CollectorConfigurationModal = (props) => { const confirmConfigurationChange = (doneCallback) => { const assignedConfigurationsToSave = configurations.filter((c) => nextAssignedConfigurations.includes(c.name)); - // eslint-disable-next-line no-restricted-syntax - for (const sidecarCollectorPair of selectedSidecarCollectorPairs) { + selectedSidecarCollectorPairs.forEach((sidecarCollectorPair) => { let configs = assignedConfigurationsToSave; if (nextPartiallyAssignedConfigurations.length) { @@ -158,7 +157,7 @@ const CollectorConfigurationModal = (props) => { } onConfigurationSelectionChange([sidecarCollectorPair], configs, doneCallback); - } + }); onCancel(); }; @@ -210,11 +209,13 @@ const CollectorConfigurationModal = (props) => { const [fullyAssignedConfigurations, partiallyAssignedConfigurations] = getFullyAndPartiallyAssignments(assignedConfigurations); - const nonAssignedConfigurations = configurations.filter((c) => !assignedConfigurations.includes(c.name)); + const nonAssignedConfigurations = configurations.filter((c) => !assignedConfigurations.includes(c.name) && (selectedLogCollectors[0]?.id === c.collector_id)) + .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) + .map((c) => c.name); - const MemoizedModalForm = React.useMemo(() => { + const MemoizedConfigurationModal = React.useMemo(() => { // eslint-disable-next-line react/no-unstable-nested-components - const ModalForm = () => { + const ConfigurationModal = () => { const [searchQuery, setSearchQuery] = React.useState(''); const [selectedConfigurations, setSelectedConfigurations] = React.useState(fullyAssignedConfigurations); const [partiallySelectedConfigurations, setPartiallySelectedConfigurations] = React.useState(partiallyAssignedConfigurations); @@ -229,12 +230,7 @@ const CollectorConfigurationModal = (props) => { const allSidecarNames = selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name); - const options = nonAssignedConfigurations - .filter((configuration) => (selectedLogCollectors[0]?.id === configuration.collector_id)) - .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) - .map((c) => c.name); - - const filteredOptions = [...fullyAssignedConfigurations, ...partiallyAssignedConfigurations, ...options].filter((configuration) => configuration.match(new RegExp(searchQuery, 'i'))); + const filteredOptions = [...fullyAssignedConfigurations, ...partiallyAssignedConfigurations, ...nonAssignedConfigurations].filter((configuration) => configuration.match(new RegExp(searchQuery, 'i'))); const rows = filteredOptions.map((configName) => { const config = getConfiguration(configName); @@ -332,13 +328,13 @@ const CollectorConfigurationModal = (props) => { ); }; - return ; + return ; // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.show]); return ( <> - {MemoizedModalForm} + {MemoizedConfigurationModal} {renderConfigurationSummary(assignedConfigurations, nextAssignedConfigurations, selectedSidecarCollectorPairs)} ); From 4ca057e6abf4781ae1c26718ec02a48ebf74e51f Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Wed, 5 Oct 2022 18:48:41 +0200 Subject: [PATCH 44/82] split CollectorConfigurationModal into 2 components to reduce complexity --- .../CollectorConfigurationModal.tsx | 331 ++++++------------ .../CollectorConfigurationModalContainer.tsx | 184 ++++++++++ .../CollectorConfigurationSelector.jsx | 175 --------- .../CollectorsAdministration.jsx | 14 +- .../CollectorsAdministrationActions.jsx | 14 +- 5 files changed, 300 insertions(+), 418 deletions(-) create mode 100644 graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx delete mode 100644 graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationSelector.jsx diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index b35e16cdde31..55b6a3a236cf 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -15,15 +15,13 @@ * along with this program. If not, see * . */ - import React from 'react'; import PropTypes from 'prop-types'; import lodash from 'lodash'; import styled from 'styled-components'; -import { naturalSortIgnoreCase } from 'util/SortUtils'; import Routes from 'routing/Routes'; -import { Table, BootstrapModalConfirm, BootstrapModalWrapper, Button, Modal } from 'components/bootstrap'; +import { Table, BootstrapModalWrapper, Button, Modal } from 'components/bootstrap'; import { SearchForm, Icon } from 'components/common'; import CollectorIndicator from 'components/sidecars/common/CollectorIndicator'; import ColorLabel from 'components/sidecars/common/ColorLabel'; @@ -50,10 +48,6 @@ const AddNewConfiguration = styled.div` align-items: center; `; -const ConfigurationSummary = styled.div` - word-break: break-all; -`; - const SecondaryText = styled.div` overflow: hidden; white-space: nowrap; @@ -108,245 +102,124 @@ const ModalSubTitle = styled.div` text-overflow: ellipsis; `; -const CollectorConfigurationModal = (props) => { - const [nextAssignedConfigurations, setNextAssignedConfigurations] = React.useState([]); - const [nextPartiallyAssignedConfigurations, setNextPartiallyAssignedConfigurations] = React.useState([]); - - const modalConfirm = React.useRef(null); - - const { configurations, selectedSidecarCollectorPairs, onConfigurationSelectionChange, onCancel, collectors } = props; - - // Do not allow configuration changes when more than one log collector type is selected - const selectedLogCollectors = lodash.uniq(selectedSidecarCollectorPairs.map(({ collector }) => collector)) as any[]; - - const getAssignedConfigurations = (_selectedSidecarCollectorPairs, _selectedLogCollectors) => { - const assignments = _selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar).reduce((accumulator, sidecar) => accumulator.concat(sidecar.assignments), []); - - return assignments.map((assignment) => configurations.find((configuration) => configuration.id === assignment.configuration_id)) - .filter((configuration) => _selectedLogCollectors[0]?.id === configuration.collector_id) - .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) - .map((config) => config.name); - }; - - const getFullyAndPartiallyAssignments = (_assignedConfigurations) => { - const occurrences = lodash.countBy(_assignedConfigurations); - - return [ - lodash.uniq(_assignedConfigurations.filter((a) => occurrences[a] === selectedSidecarCollectorPairs.length)) as any[], - lodash.uniq(_assignedConfigurations.filter((a) => occurrences[a] < selectedSidecarCollectorPairs.length)) as any[], - ]; - }; - - const onSave = (fullyAssignedConfigs: string[], partiallyAssignedConfigs: string[]) => { - setNextAssignedConfigurations(fullyAssignedConfigs); - setNextPartiallyAssignedConfigurations(partiallyAssignedConfigs); - modalConfirm.current.open(); - }; - - const confirmConfigurationChange = (doneCallback) => { - const assignedConfigurationsToSave = configurations.filter((c) => nextAssignedConfigurations.includes(c.name)); - - selectedSidecarCollectorPairs.forEach((sidecarCollectorPair) => { - let configs = assignedConfigurationsToSave; - - if (nextPartiallyAssignedConfigurations.length) { - const assignments = getAssignedConfigurations([sidecarCollectorPair], selectedLogCollectors); - const assignmentsToKeep = lodash.intersection(assignments, nextPartiallyAssignedConfigurations); - const assignedConfigurationsToKeep = configurations.filter((c) => assignmentsToKeep.includes(c.name)); - configs = [...assignedConfigurationsToSave, ...assignedConfigurationsToKeep]; - } - - onConfigurationSelectionChange([sidecarCollectorPair], configs, doneCallback); - }); - - onCancel(); - }; - - const cancelConfigurationChange = () => { - setNextAssignedConfigurations([]); - }; - - const getConfiguration = (configName: string) => { - return configurations.find((c) => c.name === configName); - }; +const CollectorConfigurationModal = ({ show, onCancel, onSave, selectedCollectorName, selectedSidecarNames, initialAssignedConfigs, initialPartiallyAssignedConfigs, unassignedConfigs, getRowData }) => { + const [searchQuery, setSearchQuery] = React.useState(''); + const [selectedConfigurations, setSelectedConfigurations] = React.useState(initialAssignedConfigs); + const [partiallySelectedConfigurations, setPartiallySelectedConfigurations] = React.useState(initialPartiallyAssignedConfigs); - const getCollector = (configName: string) => { - const configuration = getConfiguration(configName); - - return collectors.find((b) => b.id === configuration.collector_id); + const onReset = () => { + setSelectedConfigurations(initialAssignedConfigs); + setPartiallySelectedConfigurations(initialPartiallyAssignedConfigs); + setSearchQuery(''); }; - const getSidecars = (configName: string) => { - const configuration = getConfiguration(configName); + const isNotDirty = lodash.isEqual(selectedConfigurations, initialAssignedConfigs) && lodash.isEqual(partiallySelectedConfigurations, initialPartiallyAssignedConfigs); - return selectedSidecarCollectorPairs.filter(({ sidecar }) => sidecar.assignments.map((s) => s.configuration_id).includes(configuration.id)).map((a) => a.sidecar); - }; + const filteredOptions = [...initialAssignedConfigs, ...initialPartiallyAssignedConfigs, ...unassignedConfigs].filter((configuration) => configuration.match(new RegExp(searchQuery, 'i'))); - const getAssignedFromTags = (configId: string, collectorId: string, sidecars) => { - const assigned_from_tags = sidecars.reduce((accumulator, sidecar) => accumulator.concat(sidecar.assignments.find((a) => (a.collector_id === collectorId) && (a.configuration_id === configId)).assigned_from_tags), []); + const rows = filteredOptions.map((configName) => { + const { configuration, collector, sidecars, autoAssignedTags } = getRowData(configName); - return lodash.uniq(assigned_from_tags); - }; - - const renderConfigurationSummary = (_previousAssignedConfigurations, _nextAssignedConfigurations, _selectedSidecarCollectorPairs) => { - const sidecarsSummary = _selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name).join(', '); - const numberOfSidecarsSummary = `${_selectedSidecarCollectorPairs.length} sidecars`; - const summary = _selectedSidecarCollectorPairs.length <= 5 ? sidecarsSummary : numberOfSidecarsSummary; + const selected = selectedConfigurations.includes(configName); + const partiallySelected = !selected && partiallySelectedConfigurations.includes(configName); + const secondaryText = (selected && selectedSidecarNames.join(', ')) || (partiallySelected && sidecars.map((s) => s.node_name).join(', ')) || ''; + const isAssignedFromTags = autoAssignedTags.length > 0; return ( - - -

Are you sure you want to proceed with this action for {summary}?

-
-
+ { + if (!isAssignedFromTags) { + if (partiallySelected) { + setPartiallySelectedConfigurations(partiallySelectedConfigurations.filter((c) => c !== configName)); + } else { + setSelectedConfigurations(selected ? selectedConfigurations.filter((c) => c !== configName) : [...selectedConfigurations, configName]); + } + } + }}> + + {selected && } + {partiallySelected && } + + + + {configName} + + {secondaryText} + + + + {isAssignedFromTags && } + + + + {collector + ? + : Unknown collector} + + + {(selected || partiallySelected) && !isAssignedFromTags && } + ); - }; - - const assignedConfigurations = getAssignedConfigurations(selectedSidecarCollectorPairs, selectedLogCollectors); - - const [fullyAssignedConfigurations, partiallyAssignedConfigurations] = getFullyAndPartiallyAssignments(assignedConfigurations); - - const nonAssignedConfigurations = configurations.filter((c) => !assignedConfigurations.includes(c.name) && (selectedLogCollectors[0]?.id === c.collector_id)) - .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) - .map((c) => c.name); - - const MemoizedConfigurationModal = React.useMemo(() => { - // eslint-disable-next-line react/no-unstable-nested-components - const ConfigurationModal = () => { - const [searchQuery, setSearchQuery] = React.useState(''); - const [selectedConfigurations, setSelectedConfigurations] = React.useState(fullyAssignedConfigurations); - const [partiallySelectedConfigurations, setPartiallySelectedConfigurations] = React.useState(partiallyAssignedConfigurations); - - const onReset = () => { - setSelectedConfigurations(fullyAssignedConfigurations); - setPartiallySelectedConfigurations(partiallyAssignedConfigurations); - setSearchQuery(''); - }; - - const isNotDirty = lodash.isEqual(selectedConfigurations, fullyAssignedConfigurations) && lodash.isEqual(partiallySelectedConfigurations, partiallyAssignedConfigurations); - - const allSidecarNames = selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name); - - const filteredOptions = [...fullyAssignedConfigurations, ...partiallyAssignedConfigurations, ...nonAssignedConfigurations].filter((configuration) => configuration.match(new RegExp(searchQuery, 'i'))); - - const rows = filteredOptions.map((configName) => { - const config = getConfiguration(configName); - const collector = getCollector(configName); - const sidecars = getSidecars(configName); - const autoAssignedTags = getAssignedFromTags(config.id, collector.id, sidecars); - - const selected = selectedConfigurations.includes(configName); - const partiallySelected = !selected && partiallySelectedConfigurations.includes(configName); - const secondaryText = (selected && allSidecarNames.join(', ')) || (partiallySelected && sidecars.map((s) => s.node_name).join(', ')) || ''; - const isAssignedFromTags = autoAssignedTags.length > 0; - - return ( - { - if (!isAssignedFromTags) { - if (partiallySelected) { - setPartiallySelectedConfigurations(partiallySelectedConfigurations.filter((c) => c !== configName)); - } else { - setSelectedConfigurations(selected ? selectedConfigurations.filter((c) => c !== configName) : [...selectedConfigurations, configName]); - } - } - }}> - - {selected && } - {partiallySelected && } - - - - {configName} - - {secondaryText} - - - - {isAssignedFromTags && } - - - - {collector - ? - : Unknown collector} - - - {(selected || partiallySelected) && !isAssignedFromTags && } - - ); - }); - - return ( - - - - Edit {selectedLogCollectors[0]?.name} Configurations - - - {`${allSidecarNames.length} sidecar${allSidecarNames.length > 1 ? 's' : ''}: `} - {allSidecarNames.join(', ')} - - - - - - setSearchQuery(q)} topMargin={0} queryWidth="100%" /> - - - - {(rows.length === 0) ? ( - - - No configurations available for the selected log collector. - - - ) : ( - rows - )} - - - -  Add a new configuration - - - - - - - - - - - - - - ); - }; - - return ; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.show]); + }); return ( - <> - {MemoizedConfigurationModal} - {renderConfigurationSummary(assignedConfigurations, nextAssignedConfigurations, selectedSidecarCollectorPairs)} - + + + + Edit {selectedCollectorName} Configurations + + + {`${selectedSidecarNames.length} sidecar${selectedSidecarNames.length > 1 ? 's' : ''}: `} + {selectedSidecarNames.join(', ')} + + + + + + setSearchQuery(q)} topMargin={0} queryWidth="100%" /> + + + + {(rows.length === 0) ? ( + + + No configurations available for the selected log collector. + + + ) : ( + rows + )} + + + +  Add a new configuration + + + + + + + + + + + + + ); }; CollectorConfigurationModal.propTypes = { - collectors: PropTypes.array.isRequired, - configurations: PropTypes.array.isRequired, - selectedSidecarCollectorPairs: PropTypes.array.isRequired, - onConfigurationSelectionChange: PropTypes.func.isRequired, show: PropTypes.bool.isRequired, + selectedCollectorName: PropTypes.string.isRequired, + selectedSidecarNames: PropTypes.array.isRequired, + initialAssignedConfigs: PropTypes.array.isRequired, + initialPartiallyAssignedConfigs: PropTypes.array.isRequired, + unassignedConfigs: PropTypes.array.isRequired, onCancel: PropTypes.func.isRequired, + onSave: PropTypes.func.isRequired, + getRowData: PropTypes.func.isRequired, }; export default CollectorConfigurationModal; diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx new file mode 100644 index 000000000000..1371c9c078ae --- /dev/null +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx @@ -0,0 +1,184 @@ +/* eslint-disable react/prop-types */ +/* + * 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 + * . + */ +import React from 'react'; +import PropTypes from 'prop-types'; +import lodash from 'lodash'; +import styled from 'styled-components'; + +import { naturalSortIgnoreCase } from 'util/SortUtils'; +import { BootstrapModalConfirm } from 'components/bootstrap'; + +import CollectorConfigurationModal from './CollectorConfigurationModal'; + +const ConfigurationSummary = styled.div` + word-break: break-all; +`; + +const CollectorConfigurationModalContainer = (props) => { + const [nextAssignedConfigurations, setNextAssignedConfigurations] = React.useState([]); + const [nextPartiallyAssignedConfigurations, setNextPartiallyAssignedConfigurations] = React.useState([]); + const modalConfirm = React.useRef(null); + + const getSelectedLogCollector = () => { + return (lodash.uniq(props.selectedSidecarCollectorPairs.map(({ collector }) => collector)))[0]; + }; + + const getAssignedConfigurations = (_selectedSidecarCollectorPairs, selectedCollector) => { + const assignments = _selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar).reduce((accumulator, sidecar) => accumulator.concat(sidecar.assignments), []); + + return assignments.map((assignment) => props.configurations.find((configuration) => configuration.id === assignment.configuration_id)) + .filter((configuration) => selectedCollector?.id === configuration.collector_id) + .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) + .map((config) => config.name); + }; + + const getUnassignedConfigurations = (assignedConfigurations: string[], selectedCollector) => { + return props.configurations.filter((c) => !assignedConfigurations.includes(c.name) && (selectedCollector?.id === c.collector_id)) + .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) + .map((c) => c.name); + }; + + const getFullyAndPartiallyAssignments = (_assignedConfigurations: string[]) => { + const occurrences = lodash.countBy(_assignedConfigurations); + + return [ + lodash.uniq(_assignedConfigurations.filter((a) => occurrences[a] === props.selectedSidecarCollectorPairs.length)), + lodash.uniq(_assignedConfigurations.filter((a) => occurrences[a] < props.selectedSidecarCollectorPairs.length)), + ]; + }; + + const onSave = (fullyAssignedConfigs: string[], partiallyAssignedConfigs: string[]) => { + setNextAssignedConfigurations(fullyAssignedConfigs); + setNextPartiallyAssignedConfigurations(partiallyAssignedConfigs); + modalConfirm.current.open(); + }; + + const confirmConfigurationChange = (doneCallback: () => void) => { + const assignedConfigurationsToSave = props.configurations.filter((c) => nextAssignedConfigurations.includes(c.name)); + + props.selectedSidecarCollectorPairs.forEach((sidecarCollectorPair) => { + let configs = assignedConfigurationsToSave; + + if (nextPartiallyAssignedConfigurations.length) { + const selectedLogCollector = getSelectedLogCollector(); + const assignments = getAssignedConfigurations([sidecarCollectorPair], selectedLogCollector); + const assignmentsToKeep = lodash.intersection(assignments, nextPartiallyAssignedConfigurations); + const assignedConfigurationsToKeep = props.configurations.filter((c) => assignmentsToKeep.includes(c.name)); + configs = [...assignedConfigurationsToSave, ...assignedConfigurationsToKeep]; + } + + props.onConfigurationSelectionChange([sidecarCollectorPair], configs, doneCallback); + }); + + props.onCancel(); + }; + + const cancelConfigurationChange = () => { + setNextAssignedConfigurations([]); + }; + + const getConfiguration = (configName: string) => { + return props.configurations.find((c) => c.name === configName); + }; + + const getCollector = (configName: string) => { + const configuration = getConfiguration(configName); + + return props.collectors.find((b) => b.id === configuration.collector_id); + }; + + const getSidecars = (configName: string) => { + const configuration = getConfiguration(configName); + + return props.selectedSidecarCollectorPairs.filter(({ sidecar }) => sidecar.assignments.map((s) => s.configuration_id).includes(configuration.id)).map((a) => a.sidecar); + }; + + const getAssignedFromTags = (configId: string, collectorId: string, sidecars) => { + const assigned_from_tags = sidecars.reduce((accumulator, sidecar) => accumulator.concat(sidecar.assignments.find((a) => (a.collector_id === collectorId) && (a.configuration_id === configId)).assigned_from_tags), []); + + return lodash.uniq(assigned_from_tags); + }; + + const getRowData = (configName: string) => { + const configuration = getConfiguration(configName); + const collector = getCollector(configName); + const sidecars = getSidecars(configName); + const autoAssignedTags = getAssignedFromTags(configuration.id, collector.id, sidecars); + + return { configuration, collector, sidecars, autoAssignedTags }; + }; + + const renderConfigurationSummary = () => { + const sidecarsSummary = props.selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name).join(', '); + const numberOfSidecarsSummary = `${props.selectedSidecarCollectorPairs.length} sidecars`; + const summary = props.selectedSidecarCollectorPairs.length <= 5 ? sidecarsSummary : numberOfSidecarsSummary; + + return ( + + +

Are you sure you want to proceed with this action for {summary}?

+
+
+ ); + }; + + const MemoizedConfigurationModal = React.useMemo(() => { + const renderConfigurationModal = () => { + const selectedCollector = getSelectedLogCollector(); + const assignedConfigurations = getAssignedConfigurations(props.selectedSidecarCollectorPairs, selectedCollector); + const unassignedConfigurations = getUnassignedConfigurations(assignedConfigurations, selectedCollector); + const [initialAssignedConfigs, initialPartiallyAssignedConfigs] = getFullyAndPartiallyAssignments(assignedConfigurations); + + return ( + sidecar.node_name)} + initialAssignedConfigs={initialAssignedConfigs} + initialPartiallyAssignedConfigs={initialPartiallyAssignedConfigs} + unassignedConfigs={unassignedConfigurations} + onCancel={props.onCancel} + onSave={onSave} + getRowData={getRowData} /> + ); + }; + + return renderConfigurationModal; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.show]); + + return ( + <> + + {renderConfigurationSummary()} + + ); +}; + +CollectorConfigurationModalContainer.propTypes = { + collectors: PropTypes.array.isRequired, + configurations: PropTypes.array.isRequired, + selectedSidecarCollectorPairs: PropTypes.array.isRequired, + onConfigurationSelectionChange: PropTypes.func.isRequired, + show: PropTypes.bool.isRequired, + onCancel: PropTypes.func.isRequired, +}; + +export default CollectorConfigurationModalContainer; diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationSelector.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationSelector.jsx deleted file mode 100644 index 0337b7883f5c..000000000000 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationSelector.jsx +++ /dev/null @@ -1,175 +0,0 @@ -/* - * 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 - * . - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import lodash from 'lodash'; - -import { Button, BootstrapModalConfirm } from 'components/bootstrap'; -import { SelectPopover } from 'components/common'; -import { naturalSortIgnoreCase } from 'util/SortUtils'; -import CollectorIndicator from 'components/sidecars/common/CollectorIndicator'; -import ColorLabel from 'components/sidecars/common/ColorLabel'; - -const getAssignedConfigurations = (selectedSidecarCollectorPairs, configurations) => { - const assignments = selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar).reduce((accumulator, sidecar) => accumulator.concat(sidecar.assignments), []); - - return assignments.map((assignment) => configurations.find((configuration) => configuration.id === assignment.configuration_id)); -}; - -class CollectorConfigurationSelector extends React.Component { - static propTypes = { - collectors: PropTypes.array.isRequired, - configurations: PropTypes.array.isRequired, - selectedSidecarCollectorPairs: PropTypes.array.isRequired, - onConfigurationSelectionChange: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - - this.state = { - nextAssignedConfigurations: [], - }; - } - - handleConfigurationSelect = (configurationNames, hideCallback) => { - const { configurations } = this.props; - - hideCallback(); - const nextAssignedConfigurations = configurations.filter((c) => configurationNames.includes(c.name)); - - this.setState({ nextAssignedConfigurations }, this.modal.open); - }; - - confirmConfigurationChange = (doneCallback) => { - const { onConfigurationSelectionChange } = this.props; - const { nextAssignedConfigurations } = this.state; - - onConfigurationSelectionChange(nextAssignedConfigurations, doneCallback); - }; - - cancelConfigurationChange = () => { - this.setState({ nextAssignedConfigurations: [] }); - }; - - configurationFormatter = (configurationName) => { - const { configurations, collectors } = this.props; - const configuration = configurations.find((c) => c.name === configurationName); - const collector = collectors.find((b) => b.id === configuration.collector_id); - - return ( - - {configuration.name}  - - {collector - ? ( - - ) - : Unknown collector} - - - ); - }; - - renderConfigurationSummary = (nextAssignedConfigurations, selectedSidecarCollectorPairs) => { - const exampleSidecarCollectorPair = selectedSidecarCollectorPairs[0]; - const collectorIndicator = ( - - - - ); - - let actionSummary; - - if (nextAssignedConfigurations.length === 0) { - actionSummary = You are going to remove the configuration for collector {collectorIndicator} from:; - } else { - actionSummary = You are going to apply the {nextAssignedConfigurations[0].name} configuration for collector {collectorIndicator} to:; - } - - const formattedSummary = selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name).join(', '); - - return ( - { this.modal = c; }} - title="Configuration summary" - onConfirm={this.confirmConfigurationChange} - onCancel={this.cancelConfigurationChange}> -
-

{actionSummary}

-

{formattedSummary}

-

Are you sure you want to proceed with this action?

-
-
- ); - }; - - render() { - const { nextAssignedConfigurations } = this.state; - const { configurations, selectedSidecarCollectorPairs } = this.props; - - // Do not allow configuration changes when more than one log collector type is selected - const selectedLogCollectors = lodash.uniq(selectedSidecarCollectorPairs.map(({ collector }) => collector)); - - if (selectedLogCollectors.length > 1) { - return ( - Configure } - items={[`Cannot change configurations of ${selectedLogCollectors.map((collector) => collector.name).join(', ')} collectors simultaneously`]} - displayDataFilter={false} - disabled /> - ); - } - - const configurationNames = configurations - .filter((configuration) => selectedLogCollectors[0].id === configuration.collector_id) - .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) - .map((c) => c.name); - - if (configurationNames.length === 0) { - return ( - Configure } - items={['No configurations available for the selected log collector']} - displayDataFilter={false} - disabled /> - ); - } - - const assignedConfigurations = getAssignedConfigurations(selectedSidecarCollectorPairs, configurations) - .filter((configuration) => selectedLogCollectors[0].id === configuration.collector_id); - - return ( - - Configure } - items={configurationNames} - itemFormatter={this.configurationFormatter} - onItemSelect={this.handleConfigurationSelect} - selectedItems={assignedConfigurations.map((config) => config.name)} - filterPlaceholder="Filter by configuration" /> - {this.renderConfigurationSummary(nextAssignedConfigurations, selectedSidecarCollectorPairs)} - - ); - } -} - -export default CollectorConfigurationSelector; diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx index 73559a06b5d5..4c5b83c7b52e 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx @@ -34,7 +34,7 @@ import commonStyle from 'components/sidecars/common/CommonSidecarStyles.css'; import CollectorsAdministrationActions from './CollectorsAdministrationActions'; import CollectorsAdministrationFilters from './CollectorsAdministrationFilters'; -import CollectorConfigurationModal from './CollectorConfigurationModal'; +import CollectorConfigurationModalContainer from './CollectorConfigurationModalContainer'; import FiltersSummary from './FiltersSummary'; import style from './CollectorsAdministration.css'; @@ -377,12 +377,12 @@ const CollectorsAdministration = createReactClass({ - this.setState({ selected: [], showConfigurationModal: false })} /> + this.setState({ selected: [], showConfigurationModal: false })} />
); }, diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx index 7a8c9be88352..865df4f87148 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx @@ -22,7 +22,7 @@ import styled from 'styled-components'; import { ButtonToolbar, Button } from 'components/bootstrap'; import { Icon } from 'components/common'; -import CollectorConfigurationModal from './CollectorConfigurationModal'; +import CollectorConfigurationModalContainer from './CollectorConfigurationModalContainer'; import CollectorProcessControl from './CollectorProcessControl'; const ConfigurationButton = styled(Button)` @@ -45,12 +45,12 @@ const CollectorsAdministrationActions = (props) => { onClick={() => setShowConfigurationModal(true)}> Assign Configurations - + ); From f85a049012beff28e593535f27eda8e2634a26b4 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Fri, 7 Oct 2022 14:04:18 +0200 Subject: [PATCH 45/82] small css changes --- .../sidecars/administration/CollectorConfigurationModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 55b6a3a236cf..1a710a667758 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -53,7 +53,7 @@ const SecondaryText = styled.div` white-space: nowrap; text-overflow: ellipsis; color: #aaaaaa; - margin-top: -6px; + margin-top: -4px; margin-bottom: -2px; `; @@ -66,7 +66,7 @@ const TableRow = styled.tr` const StickyTableRowFooter = styled.tr` height: 32px; position: sticky; - bottom: 0; + bottom: -1px; `; const IconTableCell = styled.td` From ed6a40c9fba6ffb7352ce5d13363f8a13c92a8e4 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Mon, 10 Oct 2022 10:06:32 +0200 Subject: [PATCH 46/82] added tests for CollectorConfigurationModal --- .../CollectorConfigurationModal.test.tsx | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.test.tsx diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.test.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.test.tsx new file mode 100644 index 000000000000..989ccb7c99d2 --- /dev/null +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.test.tsx @@ -0,0 +1,108 @@ +/* + * 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 + * . + */ +import * as React from 'react'; +import { render, screen } from 'wrappedTestingLibrary'; + +import CollectorConfigurationModal from './CollectorConfigurationModal'; + +describe('CollectorConfigurationModal', () => { + const renderModal = ( + show: boolean = false, + collectorName: string = '', + sidecarNames : string[] = [], + assignedConfigs: string[] = [], + ) => ( + {}} + onSave={() => {}} + getRowData={() => ({ + configuration: { id: 'id', name: 'name', color: 'black' }, + collector: { id: 'id', name: 'name', node_operating_system: 'mac' }, + sidecars: [], + autoAssignedTags: [], + })} /> + ); + + it('Should only open modal when show is true', () => { + render( + renderModal( + false, + 'collector1', + ), + ); + + const modalTitle = screen.queryByText(/collector1/i); + + expect(modalTitle).toBe(null); + }); + + it('Should display in the title the collector name and the selected sidecar names', () => { + render( + renderModal( + true, + 'collector1', + ['sidecar1', 'sidecar2'], + ), + ); + + const modalTitle = screen.queryByText(/collector1/i); + const modalSubTitle = screen.queryByText(/sidecar1, sidecar2/i); + + expect(modalTitle).not.toBe(null); + expect(modalSubTitle).not.toBe(null); + }); + + it('Should display empty list message and a possibility to create a new config', () => { + render( + renderModal( + true, + 'collector1', + ['sidecar1', 'sidecar2'], + ), + ); + + const emptyListMasg = screen.queryByText(/No configurations available for the selected log collector./i); + const addNewConfig = screen.queryByText(/Add a new configuration/i); + + expect(emptyListMasg).not.toBe(null); + expect(addNewConfig).not.toBe(null); + }); + + it('Should display assigned config names', () => { + render( + renderModal( + true, + 'collector1', + ['sidecar1', 'sidecar2'], + ['config1', 'config2', 'config3'], + ), + ); + + const config1 = screen.queryByText(/config1/i); + const config2 = screen.queryByText(/config2/i); + const config3 = screen.queryByText(/config3/i); + + expect(config1).not.toBe(null); + expect(config2).not.toBe(null); + expect(config3).not.toBe(null); + }); +}); From 6bd072466340213cc587e5b1395cc2582e2fcb07 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Mon, 10 Oct 2022 17:34:18 +0200 Subject: [PATCH 47/82] fixed too long logic lines --- .../CollectorConfigurationModal.tsx | 13 ++++++++++-- .../CollectorConfigurationModalContainer.tsx | 6 +++++- .../CollectorsAdministration.jsx | 21 +++++++++++++++++-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 1a710a667758..17db64ffcb04 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/prop-types */ /* * Copyright (C) 2020 Graylog, Inc. * @@ -102,7 +101,17 @@ const ModalSubTitle = styled.div` text-overflow: ellipsis; `; -const CollectorConfigurationModal = ({ show, onCancel, onSave, selectedCollectorName, selectedSidecarNames, initialAssignedConfigs, initialPartiallyAssignedConfigs, unassignedConfigs, getRowData }) => { +const CollectorConfigurationModal = ({ + show, + onCancel, + onSave, + selectedCollectorName, + selectedSidecarNames, + initialAssignedConfigs, + initialPartiallyAssignedConfigs, + unassignedConfigs, + getRowData, +}) => { const [searchQuery, setSearchQuery] = React.useState(''); const [selectedConfigurations, setSelectedConfigurations] = React.useState(initialAssignedConfigs); const [partiallySelectedConfigurations, setPartiallySelectedConfigurations] = React.useState(initialPartiallyAssignedConfigs); diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx index 1371c9c078ae..8a94f5cbd07f 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx @@ -109,7 +109,11 @@ const CollectorConfigurationModalContainer = (props) => { }; const getAssignedFromTags = (configId: string, collectorId: string, sidecars) => { - const assigned_from_tags = sidecars.reduce((accumulator, sidecar) => accumulator.concat(sidecar.assignments.find((a) => (a.collector_id === collectorId) && (a.configuration_id === configId)).assigned_from_tags), []); + const assigned_from_tags = sidecars.reduce((accumulator, sidecar) => { + return accumulator.concat( + sidecar.assignments.find((a) => (a.collector_id === collectorId) && (a.configuration_id === configId)).assigned_from_tags, + ); + }, []); return lodash.uniq(assigned_from_tags); }; diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx index 4c5b83c7b52e..066c1328f98f 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx @@ -280,8 +280,25 @@ const CollectorsAdministration = createReactClass({ - {(configAssignments.length > 0) && this.setState({ selected: [sidecarCollectorId], showConfigurationModal: true })} />} - {configAssignments.map((configuration) => )} + {(configAssignments.length > 0) && + this.setState({ selected: [sidecarCollectorId], showConfigurationModal: true })} + /> + } + {configAssignments.map((configuration) => + + + + )} From ce5c410f82a6c95317397f496bf060fe09d50e73 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Mon, 10 Oct 2022 18:15:06 +0200 Subject: [PATCH 48/82] avoid single charachter variables --- .../CollectorConfigurationModal.tsx | 6 ++-- .../CollectorConfigurationModalContainer.tsx | 36 ++++++++++--------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 17db64ffcb04..ee345a525ff8 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -131,7 +131,7 @@ const CollectorConfigurationModal = ({ const selected = selectedConfigurations.includes(configName); const partiallySelected = !selected && partiallySelectedConfigurations.includes(configName); - const secondaryText = (selected && selectedSidecarNames.join(', ')) || (partiallySelected && sidecars.map((s) => s.node_name).join(', ')) || ''; + const secondaryText = (selected && selectedSidecarNames.join(', ')) || (partiallySelected && sidecars.map((sidecar) => sidecar.node_name).join(', ')) || ''; const isAssignedFromTags = autoAssignedTags.length > 0; return ( @@ -140,9 +140,9 @@ const CollectorConfigurationModal = ({ onClick={() => { if (!isAssignedFromTags) { if (partiallySelected) { - setPartiallySelectedConfigurations(partiallySelectedConfigurations.filter((c) => c !== configName)); + setPartiallySelectedConfigurations(partiallySelectedConfigurations.filter((name) => name !== configName)); } else { - setSelectedConfigurations(selected ? selectedConfigurations.filter((c) => c !== configName) : [...selectedConfigurations, configName]); + setSelectedConfigurations(selected ? selectedConfigurations.filter((name) => name !== configName) : [...selectedConfigurations, configName]); } } }}> diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx index 8a94f5cbd07f..4ccfd860885e 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/prop-types */ /* * Copyright (C) 2020 Graylog, Inc. * @@ -38,27 +37,32 @@ const CollectorConfigurationModalContainer = (props) => { return (lodash.uniq(props.selectedSidecarCollectorPairs.map(({ collector }) => collector)))[0]; }; + const sortConfigurationNames = (configs) => { + return configs.sort((config1, config2) => naturalSortIgnoreCase(config1.name, config2.name)) + .map((config) => config.name); + }; + const getAssignedConfigurations = (_selectedSidecarCollectorPairs, selectedCollector) => { const assignments = _selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar).reduce((accumulator, sidecar) => accumulator.concat(sidecar.assignments), []); - return assignments.map((assignment) => props.configurations.find((configuration) => configuration.id === assignment.configuration_id)) - .filter((configuration) => selectedCollector?.id === configuration.collector_id) - .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) - .map((config) => config.name); + const filteredAssignments = assignments.map((assignment) => props.configurations.find((configuration) => configuration.id === assignment.configuration_id)) + .filter((configuration) => selectedCollector?.id === configuration.collector_id); + + return sortConfigurationNames(filteredAssignments); }; const getUnassignedConfigurations = (assignedConfigurations: string[], selectedCollector) => { - return props.configurations.filter((c) => !assignedConfigurations.includes(c.name) && (selectedCollector?.id === c.collector_id)) - .sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)) - .map((c) => c.name); + const filteredConfigs = props.configurations.filter((config) => !assignedConfigurations.includes(config.name) && (selectedCollector?.id === config.collector_id)); + + return sortConfigurationNames(filteredConfigs); }; const getFullyAndPartiallyAssignments = (_assignedConfigurations: string[]) => { const occurrences = lodash.countBy(_assignedConfigurations); return [ - lodash.uniq(_assignedConfigurations.filter((a) => occurrences[a] === props.selectedSidecarCollectorPairs.length)), - lodash.uniq(_assignedConfigurations.filter((a) => occurrences[a] < props.selectedSidecarCollectorPairs.length)), + lodash.uniq(_assignedConfigurations.filter((config) => occurrences[config] === props.selectedSidecarCollectorPairs.length)), + lodash.uniq(_assignedConfigurations.filter((config) => occurrences[config] < props.selectedSidecarCollectorPairs.length)), ]; }; @@ -69,7 +73,7 @@ const CollectorConfigurationModalContainer = (props) => { }; const confirmConfigurationChange = (doneCallback: () => void) => { - const assignedConfigurationsToSave = props.configurations.filter((c) => nextAssignedConfigurations.includes(c.name)); + const assignedConfigurationsToSave = props.configurations.filter((config) => nextAssignedConfigurations.includes(config.name)); props.selectedSidecarCollectorPairs.forEach((sidecarCollectorPair) => { let configs = assignedConfigurationsToSave; @@ -78,7 +82,7 @@ const CollectorConfigurationModalContainer = (props) => { const selectedLogCollector = getSelectedLogCollector(); const assignments = getAssignedConfigurations([sidecarCollectorPair], selectedLogCollector); const assignmentsToKeep = lodash.intersection(assignments, nextPartiallyAssignedConfigurations); - const assignedConfigurationsToKeep = props.configurations.filter((c) => assignmentsToKeep.includes(c.name)); + const assignedConfigurationsToKeep = props.configurations.filter((config) => assignmentsToKeep.includes(config.name)); configs = [...assignedConfigurationsToSave, ...assignedConfigurationsToKeep]; } @@ -93,25 +97,25 @@ const CollectorConfigurationModalContainer = (props) => { }; const getConfiguration = (configName: string) => { - return props.configurations.find((c) => c.name === configName); + return props.configurations.find((config) => config.name === configName); }; const getCollector = (configName: string) => { const configuration = getConfiguration(configName); - return props.collectors.find((b) => b.id === configuration.collector_id); + return props.collectors.find((collector) => collector.id === configuration.collector_id); }; const getSidecars = (configName: string) => { const configuration = getConfiguration(configName); - return props.selectedSidecarCollectorPairs.filter(({ sidecar }) => sidecar.assignments.map((s) => s.configuration_id).includes(configuration.id)).map((a) => a.sidecar); + return props.selectedSidecarCollectorPairs.filter(({ sidecar }) => sidecar.assignments.map((assignment) => assignment.configuration_id).includes(configuration.id)).map((assignment) => assignment.sidecar); }; const getAssignedFromTags = (configId: string, collectorId: string, sidecars) => { const assigned_from_tags = sidecars.reduce((accumulator, sidecar) => { return accumulator.concat( - sidecar.assignments.find((a) => (a.collector_id === collectorId) && (a.configuration_id === configId)).assigned_from_tags, + sidecar.assignments.find((assignment) => (assignment.collector_id === collectorId) && (assignment.configuration_id === configId)).assigned_from_tags, ); }, []); From 0bc91b070f22ea929b3eea3840ca2ce2d0b5d862 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Mon, 10 Oct 2022 18:17:34 +0200 Subject: [PATCH 49/82] fixed Missing semicolons --- .../sidecars/administration/CollectorConfigurationModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index ee345a525ff8..7c4d97383102 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -29,11 +29,11 @@ import { Link } from 'components/common/router'; const ConfigurationContainer = styled.div` overflow: auto; height: 360px; - margin-top: 8px + margin-top: 8px; `; const ConfigurationTable = styled(Table)` - margin-bottom: 0 + margin-bottom: 0; `; const NoConfigurationMessage = styled.div` From 1e60ad0f595ef116e6a60cfb13b7ffefaf1ba8e5 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Mon, 10 Oct 2022 18:21:25 +0200 Subject: [PATCH 50/82] removed React. in hooks --- .../administration/CollectorConfigurationModal.tsx | 8 ++++---- .../CollectorConfigurationModalContainer.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 7c4d97383102..7731107a30d8 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -14,7 +14,7 @@ * along with this program. If not, see * . */ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import lodash from 'lodash'; import styled from 'styled-components'; @@ -112,9 +112,9 @@ const CollectorConfigurationModal = ({ unassignedConfigs, getRowData, }) => { - const [searchQuery, setSearchQuery] = React.useState(''); - const [selectedConfigurations, setSelectedConfigurations] = React.useState(initialAssignedConfigs); - const [partiallySelectedConfigurations, setPartiallySelectedConfigurations] = React.useState(initialPartiallyAssignedConfigs); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedConfigurations, setSelectedConfigurations] = useState(initialAssignedConfigs); + const [partiallySelectedConfigurations, setPartiallySelectedConfigurations] = useState(initialPartiallyAssignedConfigs); const onReset = () => { setSelectedConfigurations(initialAssignedConfigs); diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx index 4ccfd860885e..af050b4409ba 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx @@ -14,7 +14,7 @@ * along with this program. If not, see * . */ -import React from 'react'; +import React, { useState, useRef } from 'react'; import PropTypes from 'prop-types'; import lodash from 'lodash'; import styled from 'styled-components'; @@ -29,9 +29,9 @@ const ConfigurationSummary = styled.div` `; const CollectorConfigurationModalContainer = (props) => { - const [nextAssignedConfigurations, setNextAssignedConfigurations] = React.useState([]); - const [nextPartiallyAssignedConfigurations, setNextPartiallyAssignedConfigurations] = React.useState([]); - const modalConfirm = React.useRef(null); + const [nextAssignedConfigurations, setNextAssignedConfigurations] = useState([]); + const [nextPartiallyAssignedConfigurations, setNextPartiallyAssignedConfigurations] = useState([]); + const modalConfirm = useRef(null); const getSelectedLogCollector = () => { return (lodash.uniq(props.selectedSidecarCollectorPairs.map(({ collector }) => collector)))[0]; From 4d3a741a8873ddb07dd19bbce6bdd6ff6f03afc6 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Tue, 11 Oct 2022 10:13:15 +0200 Subject: [PATCH 51/82] RegExp instance set into a variable --- .../sidecars/administration/CollectorConfigurationModal.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 7731107a30d8..13520fe5ccb3 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -124,7 +124,9 @@ const CollectorConfigurationModal = ({ const isNotDirty = lodash.isEqual(selectedConfigurations, initialAssignedConfigs) && lodash.isEqual(partiallySelectedConfigurations, initialPartiallyAssignedConfigs); - const filteredOptions = [...initialAssignedConfigs, ...initialPartiallyAssignedConfigs, ...unassignedConfigs].filter((configuration) => configuration.match(new RegExp(searchQuery, 'i'))); + const searchQueryRegexPattern = new RegExp(searchQuery, 'i'); + + const filteredOptions = [...initialAssignedConfigs, ...initialPartiallyAssignedConfigs, ...unassignedConfigs].filter((configuration) => configuration.match(searchQueryRegexPattern)); const rows = filteredOptions.map((configName) => { const { configuration, collector, sidecars, autoAssignedTags } = getRowData(configName); From 7d1afcac4228170f0e1fe71373f5af1c2506d102 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Tue, 11 Oct 2022 10:26:22 +0200 Subject: [PATCH 52/82] destruct props --- .../CollectorConfigurationModalContainer.tsx | 51 +++++++++++-------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx index af050b4409ba..7ce1ab9cf5c8 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx @@ -28,13 +28,20 @@ const ConfigurationSummary = styled.div` word-break: break-all; `; -const CollectorConfigurationModalContainer = (props) => { +const CollectorConfigurationModalContainer = ({ + collectors, + configurations, + selectedSidecarCollectorPairs, + onConfigurationSelectionChange, + show, + onCancel, +}) => { const [nextAssignedConfigurations, setNextAssignedConfigurations] = useState([]); const [nextPartiallyAssignedConfigurations, setNextPartiallyAssignedConfigurations] = useState([]); const modalConfirm = useRef(null); const getSelectedLogCollector = () => { - return (lodash.uniq(props.selectedSidecarCollectorPairs.map(({ collector }) => collector)))[0]; + return (lodash.uniq(selectedSidecarCollectorPairs.map(({ collector }) => collector)))[0]; }; const sortConfigurationNames = (configs) => { @@ -45,14 +52,14 @@ const CollectorConfigurationModalContainer = (props) => { const getAssignedConfigurations = (_selectedSidecarCollectorPairs, selectedCollector) => { const assignments = _selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar).reduce((accumulator, sidecar) => accumulator.concat(sidecar.assignments), []); - const filteredAssignments = assignments.map((assignment) => props.configurations.find((configuration) => configuration.id === assignment.configuration_id)) + const filteredAssignments = assignments.map((assignment) => configurations.find((configuration) => configuration.id === assignment.configuration_id)) .filter((configuration) => selectedCollector?.id === configuration.collector_id); return sortConfigurationNames(filteredAssignments); }; const getUnassignedConfigurations = (assignedConfigurations: string[], selectedCollector) => { - const filteredConfigs = props.configurations.filter((config) => !assignedConfigurations.includes(config.name) && (selectedCollector?.id === config.collector_id)); + const filteredConfigs = configurations.filter((config) => !assignedConfigurations.includes(config.name) && (selectedCollector?.id === config.collector_id)); return sortConfigurationNames(filteredConfigs); }; @@ -61,8 +68,8 @@ const CollectorConfigurationModalContainer = (props) => { const occurrences = lodash.countBy(_assignedConfigurations); return [ - lodash.uniq(_assignedConfigurations.filter((config) => occurrences[config] === props.selectedSidecarCollectorPairs.length)), - lodash.uniq(_assignedConfigurations.filter((config) => occurrences[config] < props.selectedSidecarCollectorPairs.length)), + lodash.uniq(_assignedConfigurations.filter((config) => occurrences[config] === selectedSidecarCollectorPairs.length)), + lodash.uniq(_assignedConfigurations.filter((config) => occurrences[config] < selectedSidecarCollectorPairs.length)), ]; }; @@ -73,23 +80,23 @@ const CollectorConfigurationModalContainer = (props) => { }; const confirmConfigurationChange = (doneCallback: () => void) => { - const assignedConfigurationsToSave = props.configurations.filter((config) => nextAssignedConfigurations.includes(config.name)); + const assignedConfigurationsToSave = configurations.filter((config) => nextAssignedConfigurations.includes(config.name)); - props.selectedSidecarCollectorPairs.forEach((sidecarCollectorPair) => { + selectedSidecarCollectorPairs.forEach((sidecarCollectorPair) => { let configs = assignedConfigurationsToSave; if (nextPartiallyAssignedConfigurations.length) { const selectedLogCollector = getSelectedLogCollector(); const assignments = getAssignedConfigurations([sidecarCollectorPair], selectedLogCollector); const assignmentsToKeep = lodash.intersection(assignments, nextPartiallyAssignedConfigurations); - const assignedConfigurationsToKeep = props.configurations.filter((config) => assignmentsToKeep.includes(config.name)); + const assignedConfigurationsToKeep = configurations.filter((config) => assignmentsToKeep.includes(config.name)); configs = [...assignedConfigurationsToSave, ...assignedConfigurationsToKeep]; } - props.onConfigurationSelectionChange([sidecarCollectorPair], configs, doneCallback); + onConfigurationSelectionChange([sidecarCollectorPair], configs, doneCallback); }); - props.onCancel(); + onCancel(); }; const cancelConfigurationChange = () => { @@ -97,19 +104,19 @@ const CollectorConfigurationModalContainer = (props) => { }; const getConfiguration = (configName: string) => { - return props.configurations.find((config) => config.name === configName); + return configurations.find((config) => config.name === configName); }; const getCollector = (configName: string) => { const configuration = getConfiguration(configName); - return props.collectors.find((collector) => collector.id === configuration.collector_id); + return collectors.find((collector) => collector.id === configuration.collector_id); }; const getSidecars = (configName: string) => { const configuration = getConfiguration(configName); - return props.selectedSidecarCollectorPairs.filter(({ sidecar }) => sidecar.assignments.map((assignment) => assignment.configuration_id).includes(configuration.id)).map((assignment) => assignment.sidecar); + return selectedSidecarCollectorPairs.filter(({ sidecar }) => sidecar.assignments.map((assignment) => assignment.configuration_id).includes(configuration.id)).map((assignment) => assignment.sidecar); }; const getAssignedFromTags = (configId: string, collectorId: string, sidecars) => { @@ -132,9 +139,9 @@ const CollectorConfigurationModalContainer = (props) => { }; const renderConfigurationSummary = () => { - const sidecarsSummary = props.selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name).join(', '); - const numberOfSidecarsSummary = `${props.selectedSidecarCollectorPairs.length} sidecars`; - const summary = props.selectedSidecarCollectorPairs.length <= 5 ? sidecarsSummary : numberOfSidecarsSummary; + const sidecarsSummary = selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name).join(', '); + const numberOfSidecarsSummary = `${selectedSidecarCollectorPairs.length} sidecars`; + const summary = selectedSidecarCollectorPairs.length <= 5 ? sidecarsSummary : numberOfSidecarsSummary; return ( { const MemoizedConfigurationModal = React.useMemo(() => { const renderConfigurationModal = () => { const selectedCollector = getSelectedLogCollector(); - const assignedConfigurations = getAssignedConfigurations(props.selectedSidecarCollectorPairs, selectedCollector); + const assignedConfigurations = getAssignedConfigurations(selectedSidecarCollectorPairs, selectedCollector); const unassignedConfigurations = getUnassignedConfigurations(assignedConfigurations, selectedCollector); const [initialAssignedConfigs, initialPartiallyAssignedConfigs] = getFullyAndPartiallyAssignments(assignedConfigurations); return ( - sidecar.node_name)} + selectedSidecarNames={selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name)} initialAssignedConfigs={initialAssignedConfigs} initialPartiallyAssignedConfigs={initialPartiallyAssignedConfigs} unassignedConfigs={unassignedConfigurations} - onCancel={props.onCancel} + onCancel={onCancel} onSave={onSave} getRowData={getRowData} /> ); @@ -170,7 +177,7 @@ const CollectorConfigurationModalContainer = (props) => { return renderConfigurationModal; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.show]); + }, [show]); return ( <> From 4411fcb000893d827a8d9010d4bf3c993b69c3e4 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Tue, 11 Oct 2022 15:16:27 +0200 Subject: [PATCH 53/82] used styled components --- .../administration/CollectorConfigurationModal.tsx | 11 ++++++----- .../administration/CollectorsAdministration.jsx | 9 ++++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 13520fe5ccb3..d4b405681d31 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -17,7 +17,7 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import lodash from 'lodash'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import Routes from 'routing/Routes'; import { Table, BootstrapModalWrapper, Button, Modal } from 'components/bootstrap'; @@ -56,11 +56,12 @@ const SecondaryText = styled.div` margin-bottom: -2px; `; -const TableRow = styled.tr` - cursor: pointer; +const TableRow = styled.tr(({ disabled = false }: {disabled?: boolean}) => css` + cursor: ${disabled ? 'auto' : 'pointer'}; + background-color: ${disabled ? '#E8E8E8' : 'initial'}; border-bottom: 1px solid lightgray; height: 49px; -`; +`); const StickyTableRowFooter = styled.tr` height: 32px; @@ -138,7 +139,7 @@ const CollectorConfigurationModal = ({ return ( { if (!isAssignedFromTags) { if (partiallySelected) { diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx index 066c1328f98f..69f38be6e4c4 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx @@ -51,6 +51,10 @@ const DisabledCollector = styled.div(({ theme }) => css` color: ${theme.colors.variant.light.default}; `); +const StyledColorLabel = styled(ColorLabel)` + display: flex; +`; + export const PAGE_SIZES = [10, 25, 50, 100]; const CollectorsAdministration = createReactClass({ @@ -292,10 +296,9 @@ const CollectorsAdministration = createReactClass({ key={configuration.id} to={Routes.SYSTEM.SIDECARS.EDIT_CONFIGURATION(configuration.id)} > - )} From ba7e6e1a8a2ca86674419cc34bbb57962b58bf44 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Tue, 11 Oct 2022 16:47:21 +0200 Subject: [PATCH 54/82] css styled component changes --- .../administration/CollectorConfigurationModal.tsx | 2 +- .../sidecars/administration/CollectorsAdministration.jsx | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index d4b405681d31..8bd49f95aa57 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -58,7 +58,7 @@ const SecondaryText = styled.div` const TableRow = styled.tr(({ disabled = false }: {disabled?: boolean}) => css` cursor: ${disabled ? 'auto' : 'pointer'}; - background-color: ${disabled ? '#E8E8E8' : 'initial'}; + background-color: ${disabled ? '#E8E8E8 !important' : 'initial'}; border-bottom: 1px solid lightgray; height: 49px; `); diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx index 69f38be6e4c4..066c1328f98f 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx @@ -51,10 +51,6 @@ const DisabledCollector = styled.div(({ theme }) => css` color: ${theme.colors.variant.light.default}; `); -const StyledColorLabel = styled(ColorLabel)` - display: flex; -`; - export const PAGE_SIZES = [10, 25, 50, 100]; const CollectorsAdministration = createReactClass({ @@ -296,9 +292,10 @@ const CollectorsAdministration = createReactClass({ key={configuration.id} to={Routes.SYSTEM.SIDECARS.EDIT_CONFIGURATION(configuration.id)} > - )} From cdc095389d0e712404580207ae3c978dc8e4dd5b Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Tue, 11 Oct 2022 16:51:16 +0200 Subject: [PATCH 55/82] destruct CollectorsAdministrationActions props --- .../administration/CollectorsAdministrationActions.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx index 865df4f87148..c91def33bb5a 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx @@ -29,11 +29,16 @@ const ConfigurationButton = styled(Button)` margin-right: 6px `; -const CollectorsAdministrationActions = (props) => { +const CollectorsAdministrationActions = ({ + collectors, + configurations, + selectedSidecarCollectorPairs, + onConfigurationSelectionChange, + onProcessAction +}) => { const [showConfigurationModal, setShowConfigurationModal] = React.useState(false); const onCancelConfigurationModal = React.useCallback(() => setShowConfigurationModal(false), []); - const { collectors, configurations, selectedSidecarCollectorPairs, onConfigurationSelectionChange, onProcessAction } = props; const selectedLogCollectorsNames = lodash.uniq(selectedSidecarCollectorPairs.map(({ collector }) => collector.name)); return ( From 4beb760037c8ba91068e08d98f2c791bc0c5e52b Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Tue, 11 Oct 2022 17:14:24 +0200 Subject: [PATCH 56/82] convert CollectorProcessControl to typescript and using React functional component --- .../CollectorProcessControl.jsx | 152 ------------------ .../CollectorProcessControl.tsx | 137 ++++++++++++++++ 2 files changed, 137 insertions(+), 152 deletions(-) delete mode 100644 graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.jsx create mode 100644 graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.tsx diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.jsx deleted file mode 100644 index e60f97989b59..000000000000 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.jsx +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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 - * . - */ -import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import lodash from 'lodash'; - -import { Button, Panel, BootstrapModalConfirm } from 'components/bootstrap'; -import { Pluralize, SelectPopover } from 'components/common'; - -const PROCESS_ACTIONS = ['start', 'restart', 'stop']; - -const CollectorProcessControl = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - selectedSidecarCollectorPairs: PropTypes.array.isRequired, - onProcessAction: PropTypes.func.isRequired, - }, - - getInitialState() { - return { - selectedAction: undefined, - isConfigurationWarningHidden: false, - }; - }, - - // eslint-disable-next-line react/sort-comp - resetSelectedAction() { - this.setState({ selectedAction: undefined }); - }, - - handleProcessActionSelect(processAction, hideCallback) { - hideCallback(); - this.setState({ selectedAction: processAction ? processAction[0] : undefined }, this.modal.open); - }, - - confirmProcessAction(doneCallback) { - const { onProcessAction, selectedSidecarCollectorPairs } = this.props; - const { selectedAction } = this.state; - - const callback = () => { - doneCallback(); - this.resetSelectedAction(); - }; - - onProcessAction(selectedAction, selectedSidecarCollectorPairs, callback); - }, - - cancelProcessAction() { - this.resetSelectedAction(); - }, - - hideConfigurationWarning() { - this.setState({ isConfigurationWarningHidden: true }); - }, - - renderSummaryContent(selectedAction, selectedSidecars) { - return ( - <> -

- You are going to {selectedAction} log collectors in  - : -

-

{selectedSidecars.join(', ')}

-

Are you sure you want to proceed with this action?

- - ); - }, - - renderConfigurationWarning(selectedAction) { - return ( - -

- At least one selected Collector is not configured yet. To start a new Collector, assign a - Configuration to it and the Sidecar will start the process for you. -

-

- {lodash.capitalize(selectedAction)}ing a Collector without Configuration will have no effect. -

- -
- ); - }, - - renderProcessActionSummary(selectedSidecarCollectorPairs, selectedAction) { - const { isConfigurationWarningHidden } = this.state; - const selectedSidecars = lodash.uniq(selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name)); - - // Check if all selected collectors have assigned configurations - const allHaveConfigurationsAssigned = selectedSidecarCollectorPairs.every(({ collector, sidecar }) => { - return sidecar.assignments.some(({ collector_id }) => collector_id === collector.id); - }); - - const shouldShowConfigurationWarning = !isConfigurationWarningHidden && !allHaveConfigurationsAssigned; - - return ( - { this.modal = c; }} - title="Process action summary" - confirmButtonDisabled={shouldShowConfigurationWarning} - onConfirm={this.confirmProcessAction} - onCancel={this.cancelProcessAction}> -
- {shouldShowConfigurationWarning - ? this.renderConfigurationWarning(selectedAction) - : this.renderSummaryContent(selectedAction, selectedSidecars)} -
-
- ); - }, - - render() { - const { selectedSidecarCollectorPairs } = this.props; - const { selectedAction } = this.state; - - const actionFormatter = (action) => lodash.capitalize(action); - - return ( - - Process - )} - items={PROCESS_ACTIONS} - itemFormatter={actionFormatter} - selectedItems={selectedAction ? [selectedAction] : []} - displayDataFilter={false} - onItemSelect={this.handleProcessActionSelect} /> - {this.renderProcessActionSummary(selectedSidecarCollectorPairs, selectedAction)} - - ); - }, -}); - -export default CollectorProcessControl; diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.tsx new file mode 100644 index 000000000000..f64006d5db91 --- /dev/null +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.tsx @@ -0,0 +1,137 @@ +/* + * 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 + * . + */ +import React, { useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import lodash from 'lodash'; + +import { Button, Panel, BootstrapModalConfirm } from 'components/bootstrap'; +import { Pluralize, SelectPopover } from 'components/common'; + +const PROCESS_ACTIONS = ['start', 'restart', 'stop']; + +const CollectorProcessControl = ({ selectedSidecarCollectorPairs, onProcessAction }) => { + const [selectedAction, setSelectedAction] = useState(undefined); + const [isConfigurationWarningHidden, setIsConfigurationWarningHidden] = useState(false); + const modalRef = useRef(null); + + const resetSelectedAction = () => { + setSelectedAction(undefined); + }; + + const handleProcessActionSelect = (processAction, hideCallback) => { + hideCallback(); + setSelectedAction(processAction ? processAction[0] : undefined); + modalRef.current?.open(); + }; + + const confirmProcessAction = (doneCallback) => { + const callback = () => { + doneCallback(); + resetSelectedAction(); + }; + + onProcessAction(selectedAction, selectedSidecarCollectorPairs, callback); + }; + + const cancelProcessAction = () => { + resetSelectedAction(); + }; + + const hideConfigurationWarning = () => { + setIsConfigurationWarningHidden(true); + }; + + const renderSummaryContent = (selectedSidecars) => { + return ( + <> +

+ You are going to {selectedAction} log collectors in  + : +

+

{selectedSidecars.join(', ')}

+

Are you sure you want to proceed with this action?

+ + ); + }; + + const renderConfigurationWarning = () => { + return ( + +

+ At least one selected Collector is not configured yet. To start a new Collector, assign a + Configuration to it and the Sidecar will start the process for you. +

+

+ {lodash.capitalize(selectedAction)}ing a Collector without Configuration will have no effect. +

+ +
+ ); + }; + + const renderProcessActionSummary = () => { + const selectedSidecars = lodash.uniq(selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name)); + + // Check if all selected collectors have assigned configurations + const allHaveConfigurationsAssigned = selectedSidecarCollectorPairs.every(({ collector, sidecar }) => { + return sidecar.assignments.some(({ collector_id }) => collector_id === collector.id); + }); + + const shouldShowConfigurationWarning = !isConfigurationWarningHidden && !allHaveConfigurationsAssigned; + + return ( + +
+ {shouldShowConfigurationWarning + ? renderConfigurationWarning() + : renderSummaryContent(selectedSidecars)} +
+
+ ); + }; + + const actionFormatter = (action) => lodash.capitalize(action); + + return ( + + Process + )} + items={PROCESS_ACTIONS} + itemFormatter={actionFormatter} + selectedItems={selectedAction ? [selectedAction] : []} + displayDataFilter={false} + onItemSelect={handleProcessActionSelect} /> + {renderProcessActionSummary()} + + ); +}; + +CollectorProcessControl.propTypes = { + selectedSidecarCollectorPairs: PropTypes.array.isRequired, + onProcessAction: PropTypes.func.isRequired, +}; + +export default CollectorProcessControl; From 1b693c2e937acbdc9cf5443a29b81ea8ae5625d5 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Tue, 11 Oct 2022 17:48:39 +0200 Subject: [PATCH 57/82] removed React. in hooks --- .../administration/CollectorConfigurationModalContainer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx index 7ce1ab9cf5c8..2f9d6f329f4c 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx @@ -14,7 +14,7 @@ * along with this program. If not, see * . */ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useMemo } from 'react'; import PropTypes from 'prop-types'; import lodash from 'lodash'; import styled from 'styled-components'; @@ -155,7 +155,7 @@ const CollectorConfigurationModalContainer = ({ ); }; - const MemoizedConfigurationModal = React.useMemo(() => { + const MemoizedConfigurationModal = useMemo(() => { const renderConfigurationModal = () => { const selectedCollector = getSelectedLogCollector(); const assignedConfigurations = getAssignedConfigurations(selectedSidecarCollectorPairs, selectedCollector); From 7aa63fde7b95b9deab6b82742456e408ee78c318 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Wed, 12 Oct 2022 12:51:56 +0200 Subject: [PATCH 58/82] convert CollectorsAdministration to typescript and using React functional component --- .../CollectorsAdministration.jsx | 408 ------------------ .../CollectorsAdministration.tsx | 358 +++++++++++++++ 2 files changed, 358 insertions(+), 408 deletions(-) delete mode 100644 graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx create mode 100644 graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx deleted file mode 100644 index 066c1328f98f..000000000000 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.jsx +++ /dev/null @@ -1,408 +0,0 @@ -/* - * 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 - * . - */ -/* eslint-disable */ -import React from 'react'; -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import lodash from 'lodash'; -import styled, { css } from 'styled-components'; - -import { naturalSortIgnoreCase } from 'util/SortUtils'; -import { Link } from 'components/common/router'; -import { ControlledTableList, PaginatedList, IconButton } from 'components/common'; -import Routes from 'routing/Routes'; -import { Col, Row, Input } from 'components/bootstrap'; -import ColorLabel from 'components/sidecars/common/ColorLabel'; -import OperatingSystemIcon from 'components/sidecars/common/OperatingSystemIcon'; -import SidecarSearchForm from 'components/sidecars/common/SidecarSearchForm'; -import StatusIndicator from 'components/sidecars/common/StatusIndicator'; -import commonStyle from 'components/sidecars/common/CommonSidecarStyles.css'; - -import CollectorsAdministrationActions from './CollectorsAdministrationActions'; -import CollectorsAdministrationFilters from './CollectorsAdministrationFilters'; -import CollectorConfigurationModalContainer from './CollectorConfigurationModalContainer'; -import FiltersSummary from './FiltersSummary'; -import style from './CollectorsAdministration.css'; - -const HeaderComponentsWrapper = styled.div(({ theme }) => css` - float: right; - margin: 5px 0; - - .btn-link { - color: ${theme.colors.variant.darker.default}; - } -`); - -const DisabledCollector = styled.div(({ theme }) => css` - color: ${theme.colors.variant.light.default}; -`); - -export const PAGE_SIZES = [10, 25, 50, 100]; - -const CollectorsAdministration = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - sidecarCollectorPairs: PropTypes.array.isRequired, - collectors: PropTypes.array.isRequired, - configurations: PropTypes.array.isRequired, - pagination: PropTypes.object.isRequired, - query: PropTypes.string.isRequired, - filters: PropTypes.object.isRequired, - onPageChange: PropTypes.func.isRequired, - onFilter: PropTypes.func.isRequired, - onQueryChange: PropTypes.func.isRequired, - onConfigurationChange: PropTypes.func.isRequired, - onProcessAction: PropTypes.func.isRequired, - }, - - getInitialState() { - const { sidecarCollectorPairs } = this.props; - - return { - enabledCollectors: this.getEnabledCollectors(sidecarCollectorPairs), - selected: [], - showConfigurationModal: false, - }; - }, - - UNSAFE_componentWillReceiveProps(nextProps) { - const { sidecarCollectorPairs } = this.props; - - if (!lodash.isEqual(sidecarCollectorPairs, nextProps.sidecarCollectorPairs)) { - this.setState({ - enabledCollectors: this.getEnabledCollectors(nextProps.sidecarCollectorPairs), - selected: this.filterSelectedCollectors(nextProps.sidecarCollectorPairs), - }); - } - }, - - componentDidUpdate() { - const { enabledCollectors, selected } = this.state; - - this.setSelectAllCheckboxState(this.selectAllInput, enabledCollectors, selected); - }, - - // Filter out sidecars with no compatible collectors - getEnabledCollectors(collectors) { - return collectors.filter(({ collector }) => !lodash.isEmpty(collector)); - }, - - setSelectAllCheckboxState(selectAllInput, collectors, selected) { - const selectAllCheckbox = selectAllInput ? selectAllInput.getInputDOMNode() : undefined; - - if (!selectAllCheckbox) { - return; - } - - // Set the select all checkbox as indeterminate if some but not all items are selected. - selectAllCheckbox.indeterminate = selected.length > 0 && !this.isAllSelected(collectors, selected); - }, - - sidecarCollectorId(sidecar, collector) { - return `${sidecar.node_id}-${collector.name}`; - }, - - filterSelectedCollectors(collectors) { - const { selected } = this.state; - const filteredSidecarCollectorIds = collectors.map(({ collector, sidecar }) => this.sidecarCollectorId(sidecar, collector)); - - return selected.filter((sidecarCollectorId) => filteredSidecarCollectorIds.includes(sidecarCollectorId)); - }, - - // eslint-disable-next-line react/sort-comp - handleConfigurationChange(selectedSidecars, selectedConfigurations, doneCallback) { - const { onConfigurationChange } = this.props; - onConfigurationChange(selectedSidecars, selectedConfigurations, doneCallback); - }, - - handleProcessAction(action, selectedSidecarCollectorPairs, doneCallback) { - const { onProcessAction } = this.props; - const selectedCollectors = {}; - - selectedSidecarCollectorPairs.forEach(({ sidecar, collector }) => { - if (selectedCollectors[sidecar.node_id]) { - selectedCollectors[sidecar.node_id].push(collector.id); - } else { - selectedCollectors[sidecar.node_id] = [collector.id]; - } - }); - - onProcessAction(action, selectedCollectors, doneCallback); - }, - - // eslint-disable-next-line react/no-unstable-nested-components - formatHeader(selectedSidecarCollectorPairs) { - const { collectors, configurations } = this.props; - const { selected, enabledCollectors } = this.state; - const selectedItems = selected.length; - - let headerMenu; - - if (selectedItems === 0) { - const { filters, onFilter } = this.props; - - headerMenu = ( - - ); - } else { - headerMenu = ( - - ); - } - - return ( - - {headerMenu} - - { this.selectAllInput = c; }} - id="select-all-checkbox" - type="checkbox" - label={selectedItems === 0 ? 'Select all' : `${selectedItems} selected`} - disabled={enabledCollectors.length === 0} - checked={this.isAllSelected(enabledCollectors, selected)} - onChange={this.toggleSelectAll} - wrapperClassName="form-group-inline" /> - - ); - }, - - handleSidecarCollectorSelect(sidecarCollectorId) { - return (event) => { - const { selected } = this.state; - - const newSelection = (event.target.checked - ? lodash.union(selected, [sidecarCollectorId]) - : lodash.without(selected, sidecarCollectorId)); - - this.setState({ selected: newSelection }); - }; - }, - - isAllSelected(collectors, selected) { - return collectors.length > 0 && collectors.length === selected.length; - }, - - toggleSelectAll(event) { - const { enabledCollectors } = this.state; - const newSelection = (event.target.checked - ? enabledCollectors.map(({ sidecar, collector }) => this.sidecarCollectorId(sidecar, collector)) - : []); - - this.setState({ selected: newSelection }); - }, - - // eslint-disable-next-line react/no-unstable-nested-components - formatSidecarNoCollectors(sidecar) { - return ( - - - - -

- {sidecar.node_name} -  {sidecar.node_id} -

- -
- - - - No collectors compatible with {sidecar.node_details.operating_system} - - - -
-
- ); - }, - - // eslint-disable-next-line react/no-unstable-nested-components - formatCollector(sidecar, collector, configurations) { - const sidecarCollectorId = this.sidecarCollectorId(sidecar, collector); - const configAssignmentIDs = sidecar.assignments.filter((assignment) => assignment.collector_id === collector.id).map((assignment) => assignment.configuration_id); - const configAssignments = configurations.filter((config) => configAssignmentIDs.includes(config.id)).sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)); - const { selected } = this.state; - let collectorStatus = { status: null, message: null, id: null }; - - try { - const result = sidecar.node_details.status.collectors.find((c) => c.collector_id === collector.id); - - if (result) { - collectorStatus = { - status: result.status, - message: result.message, - id: result.collector_id, - }; - } - } catch (e) { - // Do nothing - } - - return ( - - - - - - - {(configAssignments.length > 0) && ( - - )} - - - - - {(configAssignments.length > 0) && - this.setState({ selected: [sidecarCollectorId], showConfigurationModal: true })} - /> - } - {configAssignments.map((configuration) => - - - - )} - - - - ); - }, - - // eslint-disable-next-line react/no-unstable-nested-components - formatSidecar(sidecar, collectors, configurations) { - if (collectors.length === 0) { - return this.formatSidecarNoCollectors(sidecar); - } - - return ( - -
- - -

- {sidecar.node_name} -  {sidecar.node_id} {!sidecar.active && — inactive} -

- -
- {collectors.map((collector) => this.formatCollector(sidecar, collector, configurations))} -
-
- ); - }, - - handleSearch(query, callback) { - const { onQueryChange } = this.props; - - onQueryChange(query, callback()); - }, - - handleReset() { - const { onQueryChange } = this.props; - - onQueryChange(); - }, - - handleResetFilters() { - const { onFilter } = this.props; - - onFilter(); - }, - - render() { - const { configurations, collectors, onPageChange, pagination, query, sidecarCollectorPairs, filters } = this.props; - const { selected, showConfigurationModal } = this.state; - - const selectedSidecarCollectorPairs = selected.map((selectedSidecarCollectorId) => { - return sidecarCollectorPairs.find(({ sidecar, collector }) => this.sidecarCollectorId(sidecar, collector) === selectedSidecarCollectorId); - }); - - let formattedCollectors; - - if (sidecarCollectorPairs.length === 0) { - formattedCollectors = ( - - {sidecarCollectorPairs.length === 0 ? 'There are no collectors to display' : 'Filters do not match any collectors'} - - ); - } else { - const sidecars = lodash.uniq(sidecarCollectorPairs.map(({ sidecar }) => sidecar)); - - formattedCollectors = sidecars.map((sidecarToMap) => { - const sidecarCollectors = sidecarCollectorPairs - .filter(({ sidecar }) => sidecar.node_id === sidecarToMap.node_id) - .map(({ collector }) => collector) - .filter((collector) => !lodash.isEmpty(collector)); - - return this.formatSidecar(sidecarToMap, sidecarCollectors, configurations); - }); - } - - return ( -
- - - - - - - {this.formatHeader(selectedSidecarCollectorPairs)} - {formattedCollectors} - - - - - this.setState({ selected: [], showConfigurationModal: false })} /> -
- ); - }, -}); - -export default CollectorsAdministration; diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx new file mode 100644 index 000000000000..a09f20ba6adc --- /dev/null +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx @@ -0,0 +1,358 @@ +/* + * 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 + * . + */ +import React, { useState, useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import lodash from 'lodash'; +import styled, { css } from 'styled-components'; + +import { naturalSortIgnoreCase } from 'util/SortUtils'; +import { Link } from 'components/common/router'; +import { ControlledTableList, PaginatedList, IconButton } from 'components/common'; +import Routes from 'routing/Routes'; +import { Col, Row, Input } from 'components/bootstrap'; +import ColorLabel from 'components/sidecars/common/ColorLabel'; +import OperatingSystemIcon from 'components/sidecars/common/OperatingSystemIcon'; +import SidecarSearchForm from 'components/sidecars/common/SidecarSearchForm'; +import StatusIndicator from 'components/sidecars/common/StatusIndicator'; +import commonStyle from 'components/sidecars/common/CommonSidecarStyles.css'; + +import CollectorsAdministrationActions from './CollectorsAdministrationActions'; +import CollectorsAdministrationFilters from './CollectorsAdministrationFilters'; +import CollectorConfigurationModalContainer from './CollectorConfigurationModalContainer'; +import FiltersSummary from './FiltersSummary'; +import style from './CollectorsAdministration.css'; + +const HeaderComponentsWrapper = styled.div(({ theme }) => css` + float: right; + margin: 5px 0; + + .btn-link { + color: ${theme.colors.variant.darker.default}; + } +`); + +const DisabledCollector = styled.div(({ theme }) => css` + color: ${theme.colors.variant.light.default}; +`); + +export const PAGE_SIZES = [10, 25, 50, 100]; + +const CollectorsAdministration = ({ + configurations, + collectors, + onPageChange, + pagination, + query, + sidecarCollectorPairs, + filters, + onFilter, + onQueryChange, + onConfigurationChange, + onProcessAction, +}) => { + const [showConfigurationModal, setShowConfigurationModal] = useState(false); + const [selected, setSelected] = useState([]); + const selectAllInput = useRef(null); + + // Filter out sidecars with no compatible collectors + const enabledCollectors = collectors.filter(({ collector }) => !lodash.isEmpty(collector)); + + const sidecarCollectorId = (sidecar, collector) => { + return `${sidecar.node_id}-${collector.name}`; + }; + + const isAllSelected = (_collectors, _selected) => { + return _collectors.length > 0 && _collectors.length === _selected.length; + }; + + useEffect(() => { + const selectAllCheckbox = selectAllInput ? selectAllInput.current.getInputDOMNode() : undefined; + + if (selectAllCheckbox) { + // Set the select all checkbox as indeterminate if some but not all items are selected. + selectAllCheckbox.indeterminate = selected.length > 0 && !isAllSelected(collectors, selected); + } + }, [selectAllInput, collectors, selected]); + + const handleConfigurationChange = (selectedSidecars, selectedConfigurations, doneCallback) => { + onConfigurationChange(selectedSidecars, selectedConfigurations, doneCallback); + }; + + const handleProcessAction = (action, selectedSidecarCollectorPairs, doneCallback) => { + const selectedCollectors = {}; + + selectedSidecarCollectorPairs.forEach(({ sidecar, collector }) => { + if (selectedCollectors[sidecar.node_id]) { + selectedCollectors[sidecar.node_id].push(collector.id); + } else { + selectedCollectors[sidecar.node_id] = [collector.id]; + } + }); + + onProcessAction(action, selectedCollectors, doneCallback); + }; + + const toggleSelectAll = (event) => { + const newSelection = (event.target.checked + ? enabledCollectors.map(({ sidecar, collector }) => sidecarCollectorId(sidecar, collector)) + : []); + + setSelected(newSelection); + }; + + const formatHeader = (selectedSidecarCollectorPairs) => { + const selectedItems = selected.length; + + let headerMenu; + + if (selectedItems === 0) { + headerMenu = ( + + ); + } else { + headerMenu = ( + + ); + } + + return ( + + {headerMenu} + + + + ); + }; + + const handleSidecarCollectorSelect = (collectorId) => { + return (event) => { + const newSelection = (event.target.checked + ? lodash.union(selected, [collectorId]) + : lodash.without(selected, collectorId)); + + setSelected(newSelection); + }; + }; + + const formatSidecarNoCollectors = (sidecar) => { + return ( + + + + +

+ {sidecar.node_name} +  {sidecar.node_id} +

+ +
+ + + + No collectors compatible with {sidecar.node_details.operating_system} + + + +
+
+ ); + }; + + const formatCollector = (sidecar, collector, _configurations) => { + const collectorId = sidecarCollectorId(sidecar, collector); + const configAssignmentIDs = sidecar.assignments.filter((assignment) => assignment.collector_id === collector.id).map((assignment) => assignment.configuration_id); + const configAssignments = _configurations.filter((config) => configAssignmentIDs.includes(config.id)).sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)); + let collectorStatus = { status: null, message: null, id: null }; + + try { + const result = sidecar.node_details.status.collectors.find((c) => c.collector_id === collector.id); + + if (result) { + collectorStatus = { + status: result.status, + message: result.message, + id: result.collector_id, + }; + } + } catch (e) { + // Do nothing + } + + return ( + + + + + + + {(configAssignments.length > 0) && ( + + )} + + + + + {(configAssignments.length > 0) + && ( + { + setSelected([collectorId]); + setShowConfigurationModal(true); + }} /> + )} + {configAssignments.map((configuration) => ( + + + + ), + )} + + + + ); + }; + + const formatSidecar = (sidecar, _collectors, _configurations) => { + if (_collectors.length === 0) { + return formatSidecarNoCollectors(sidecar); + } + + return ( + +
+ + +

+ {sidecar.node_name} +  {sidecar.node_id} {!sidecar.active && — inactive} +

+ +
+ {_collectors.map((collector) => formatCollector(sidecar, collector, _configurations))} +
+
+ ); + }; + + const handleSearch = (_query, callback) => { + onQueryChange(_query, callback()); + }; + + const handleReset = () => { + onQueryChange(); + }; + + const handleResetFilters = () => { + onFilter(); + }; + + const selectedSidecarCollectorPairs = selected.map((selectedSidecarCollectorId) => { + return sidecarCollectorPairs.find(({ sidecar, collector }) => sidecarCollectorId(sidecar, collector) === selectedSidecarCollectorId); + }); + + let formattedCollectors; + + if (sidecarCollectorPairs.length === 0) { + formattedCollectors = ( + + {sidecarCollectorPairs.length === 0 ? 'There are no collectors to display' : 'Filters do not match any collectors'} + + ); + } else { + const sidecars = lodash.uniq(sidecarCollectorPairs.map(({ sidecar }) => sidecar)); + + formattedCollectors = sidecars.map((sidecarToMap) => { + const sidecarCollectors = sidecarCollectorPairs + .filter(({ sidecar }) => sidecar.node_id === sidecarToMap.node_id) + .map(({ collector }) => collector) + .filter((collector) => !lodash.isEmpty(collector)); + + return formatSidecar(sidecarToMap, sidecarCollectors, configurations); + }); + } + + return ( +
+ + + + + + + {formatHeader(selectedSidecarCollectorPairs)} + {formattedCollectors} + + + + + { + setSelected([]); + setShowConfigurationModal(false); + }} /> +
+ ); +}; + +CollectorsAdministration.propTypes = { + sidecarCollectorPairs: PropTypes.array.isRequired, + collectors: PropTypes.array.isRequired, + configurations: PropTypes.array.isRequired, + pagination: PropTypes.object.isRequired, + query: PropTypes.string.isRequired, + filters: PropTypes.object.isRequired, + onPageChange: PropTypes.func.isRequired, + onFilter: PropTypes.func.isRequired, + onQueryChange: PropTypes.func.isRequired, + onConfigurationChange: PropTypes.func.isRequired, + onProcessAction: PropTypes.func.isRequired, +}; + +export default CollectorsAdministration; From f35ece096b0a17a7176c7c6f040486f81d413cc0 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Wed, 12 Oct 2022 13:02:00 +0200 Subject: [PATCH 59/82] reset file --- .../SidecarCollectorConfigurationFacade.java | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/graylog2-server/src/main/java/org/graylog2/contentpacks/facades/SidecarCollectorConfigurationFacade.java b/graylog2-server/src/main/java/org/graylog2/contentpacks/facades/SidecarCollectorConfigurationFacade.java index 583ad2b45be3..e8d0c24eb3e7 100644 --- a/graylog2-server/src/main/java/org/graylog2/contentpacks/facades/SidecarCollectorConfigurationFacade.java +++ b/graylog2-server/src/main/java/org/graylog2/contentpacks/facades/SidecarCollectorConfigurationFacade.java @@ -23,7 +23,6 @@ import com.google.common.graph.GraphBuilder; import com.google.common.graph.ImmutableGraph; import com.google.common.graph.MutableGraph; -import org.graylog.plugins.sidecar.rest.models.Collector; import org.graylog.plugins.sidecar.rest.models.Configuration; import org.graylog.plugins.sidecar.services.ConfigurationService; import org.graylog2.contentpacks.EntityDescriptorIds; @@ -38,7 +37,6 @@ import org.graylog2.contentpacks.model.entities.NativeEntityDescriptor; import org.graylog2.contentpacks.model.entities.SidecarCollectorConfigurationEntity; import org.graylog2.contentpacks.model.entities.references.ValueReference; -import org.graylog2.shared.utilities.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -85,24 +83,16 @@ public NativeEntity createNativeEntity(Entity entity, Map nativeEntities, String username) { if (entity instanceof EntityV1) { - return decode((EntityV1) entity, parameters, nativeEntities); + return decode((EntityV1) entity, parameters); } else { throw new IllegalArgumentException("Unsupported entity version: " + entity.getClass()); } } - private NativeEntity decode(EntityV1 entity, Map parameters, Map nativeEntities) { + private NativeEntity decode(EntityV1 entity, Map parameters) { final SidecarCollectorConfigurationEntity configurationEntity = objectMapper.convertValue(entity.data(), SidecarCollectorConfigurationEntity.class); - // Configuration needs to reference the DB ID of the Collector and not the logical ID - String collectorEntityId = configurationEntity.collectorId().asString(parameters); - String collectorDbId = nativeEntities.entrySet().stream() - .filter(entry -> entry.getKey().id().id().equals(collectorEntityId)) - .map(Map.Entry::getValue) - .map(collector -> ((Collector) collector).id()) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException(StringUtils.f("Unable to find database ID of Collector with logical ID [%s]", collectorEntityId))); - final Configuration configuration = Configuration.create( - collectorDbId, + final Configuration configuration = Configuration.createWithoutId( + configurationEntity.collectorId().asString(parameters), configurationEntity.title().asString(parameters), configurationEntity.color().asString(parameters), configurationEntity.template().asString(parameters), From 8ee80e437c6c9ead450d3021cabd94b20a3050ab Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Wed, 12 Oct 2022 13:36:14 +0200 Subject: [PATCH 60/82] convert CollectorsAdministrationActions to typescript and using React functional component --- ...onActions.jsx => CollectorsAdministrationActions.tsx} | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) rename graylog2-web-interface/src/components/sidecars/administration/{CollectorsAdministrationActions.jsx => CollectorsAdministrationActions.tsx} (92%) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.tsx similarity index 92% rename from graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx rename to graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.tsx index c91def33bb5a..495ff2f36808 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.jsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.tsx @@ -29,21 +29,22 @@ const ConfigurationButton = styled(Button)` margin-right: 6px `; -const CollectorsAdministrationActions = ({ - collectors, +const CollectorsAdministrationActions = ({ + collectors, configurations, selectedSidecarCollectorPairs, onConfigurationSelectionChange, - onProcessAction + onProcessAction, }) => { const [showConfigurationModal, setShowConfigurationModal] = React.useState(false); const onCancelConfigurationModal = React.useCallback(() => setShowConfigurationModal(false), []); const selectedLogCollectorsNames = lodash.uniq(selectedSidecarCollectorPairs.map(({ collector }) => collector.name)); + const configButtonTooltip = `Cannot change configurations of ${selectedLogCollectorsNames.join(', ')} collectors simultaneously`; return ( - 1) ? `Cannot change configurations of ${selectedLogCollectorsNames.join(', ')} collectors simultaneously` : undefined} + 1) ? configButtonTooltip : undefined} bsStyle="primary" bsSize="small" disabled={selectedLogCollectorsNames.length !== 1} From 491e23402f574403dad1625d7f3dab93cb45bd74 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Wed, 12 Oct 2022 17:40:02 +0200 Subject: [PATCH 61/82] added types --- .../CollectorConfigurationModal.test.tsx | 20 ++++- .../CollectorConfigurationModal.tsx | 21 ++++- .../CollectorConfigurationModalContainer.tsx | 27 ++++-- .../CollectorsAdministration.tsx | 3 +- .../src/components/sidecars/types.d.ts | 89 +++++++++++++++++++ 5 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 graylog2-web-interface/src/components/sidecars/types.d.ts diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.test.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.test.tsx index 989ccb7c99d2..9e215180291c 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.test.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.test.tsx @@ -35,8 +35,24 @@ describe('CollectorConfigurationModal', () => { onCancel={() => {}} onSave={() => {}} getRowData={() => ({ - configuration: { id: 'id', name: 'name', color: 'black' }, - collector: { id: 'id', name: 'name', node_operating_system: 'mac' }, + configuration: { + id: 'id', + name: 'name', + color: 'black', + template: '', + collector_id: '', + tags: [], + }, + collector: { + id: 'id', + name: 'name', + node_operating_system: 'mac', + service_type: '', + validation_parameters: '', + executable_path: '', + execute_parameters: '', + default_template: '', + }, sidecars: [], autoAssignedTags: [], })} /> diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index 8bd49f95aa57..abafe36b4b3d 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -26,6 +26,8 @@ import CollectorIndicator from 'components/sidecars/common/CollectorIndicator'; import ColorLabel from 'components/sidecars/common/ColorLabel'; import { Link } from 'components/common/router'; +import type { Collector, Configuration, SidecarSummary } from '../types'; + const ConfigurationContainer = styled.div` overflow: auto; height: 360px; @@ -102,6 +104,23 @@ const ModalSubTitle = styled.div` text-overflow: ellipsis; `; +type Props = { + show: boolean, + onCancel: () => void, + onSave: (selectedConfigurations: string[], partiallySelectedConfigurations: string[]) => void, + selectedCollectorName: string, + selectedSidecarNames: string[], + initialAssignedConfigs: string[], + initialPartiallyAssignedConfigs: string[], + unassignedConfigs: string[], + getRowData: (configName: string) => { + configuration: Configuration, + collector: Collector, + sidecars: SidecarSummary[], + autoAssignedTags: string[], + } +}; + const CollectorConfigurationModal = ({ show, onCancel, @@ -112,7 +131,7 @@ const CollectorConfigurationModal = ({ initialPartiallyAssignedConfigs, unassignedConfigs, getRowData, -}) => { +}: Props) => { const [searchQuery, setSearchQuery] = useState(''); const [selectedConfigurations, setSelectedConfigurations] = useState(initialAssignedConfigs); const [partiallySelectedConfigurations, setPartiallySelectedConfigurations] = useState(initialPartiallyAssignedConfigs); diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx index 2f9d6f329f4c..517fbef30f37 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx @@ -24,10 +24,21 @@ import { BootstrapModalConfirm } from 'components/bootstrap'; import CollectorConfigurationModal from './CollectorConfigurationModal'; +import type { Collector, Configuration, SidecarSummary } from '../types'; + const ConfigurationSummary = styled.div` word-break: break-all; `; +type Props = { + collectors: Collector[], + configurations: Configuration[], + selectedSidecarCollectorPairs: { collector: Collector, sidecar: SidecarSummary }[], + onConfigurationSelectionChange: (pairs: { collector: Collector, sidecar: SidecarSummary }[], configs: Configuration[], callback: () => void) => void, + show: boolean, + onCancel: () => void, +}; + const CollectorConfigurationModalContainer = ({ collectors, configurations, @@ -35,21 +46,21 @@ const CollectorConfigurationModalContainer = ({ onConfigurationSelectionChange, show, onCancel, -}) => { +}: Props) => { const [nextAssignedConfigurations, setNextAssignedConfigurations] = useState([]); const [nextPartiallyAssignedConfigurations, setNextPartiallyAssignedConfigurations] = useState([]); const modalConfirm = useRef(null); const getSelectedLogCollector = () => { - return (lodash.uniq(selectedSidecarCollectorPairs.map(({ collector }) => collector)))[0]; + return (lodash.uniq(selectedSidecarCollectorPairs.map(({ collector }) => collector)))[0]; }; - const sortConfigurationNames = (configs) => { + const sortConfigurationNames = (configs: Configuration[]) => { return configs.sort((config1, config2) => naturalSortIgnoreCase(config1.name, config2.name)) .map((config) => config.name); }; - const getAssignedConfigurations = (_selectedSidecarCollectorPairs, selectedCollector) => { + const getAssignedConfigurations = (_selectedSidecarCollectorPairs: { collector: Collector, sidecar: SidecarSummary }[], selectedCollector: Collector) => { const assignments = _selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar).reduce((accumulator, sidecar) => accumulator.concat(sidecar.assignments), []); const filteredAssignments = assignments.map((assignment) => configurations.find((configuration) => configuration.id === assignment.configuration_id)) @@ -58,7 +69,7 @@ const CollectorConfigurationModalContainer = ({ return sortConfigurationNames(filteredAssignments); }; - const getUnassignedConfigurations = (assignedConfigurations: string[], selectedCollector) => { + const getUnassignedConfigurations = (assignedConfigurations: string[], selectedCollector: Collector) => { const filteredConfigs = configurations.filter((config) => !assignedConfigurations.includes(config.name) && (selectedCollector?.id === config.collector_id)); return sortConfigurationNames(filteredConfigs); @@ -68,8 +79,8 @@ const CollectorConfigurationModalContainer = ({ const occurrences = lodash.countBy(_assignedConfigurations); return [ - lodash.uniq(_assignedConfigurations.filter((config) => occurrences[config] === selectedSidecarCollectorPairs.length)), - lodash.uniq(_assignedConfigurations.filter((config) => occurrences[config] < selectedSidecarCollectorPairs.length)), + lodash.uniq(_assignedConfigurations.filter((config) => occurrences[config] === selectedSidecarCollectorPairs.length)), + lodash.uniq(_assignedConfigurations.filter((config) => occurrences[config] < selectedSidecarCollectorPairs.length)), ]; }; @@ -119,7 +130,7 @@ const CollectorConfigurationModalContainer = ({ return selectedSidecarCollectorPairs.filter(({ sidecar }) => sidecar.assignments.map((assignment) => assignment.configuration_id).includes(configuration.id)).map((assignment) => assignment.sidecar); }; - const getAssignedFromTags = (configId: string, collectorId: string, sidecars) => { + const getAssignedFromTags = (configId: string, collectorId: string, sidecars: SidecarSummary[]) => { const assigned_from_tags = sidecars.reduce((accumulator, sidecar) => { return accumulator.concat( sidecar.assignments.find((assignment) => (assignment.collector_id === collectorId) && (assignment.configuration_id === configId)).assigned_from_tags, diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx index a09f20ba6adc..8d50424833e4 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx @@ -35,6 +35,7 @@ import CollectorsAdministrationFilters from './CollectorsAdministrationFilters'; import CollectorConfigurationModalContainer from './CollectorConfigurationModalContainer'; import FiltersSummary from './FiltersSummary'; import style from './CollectorsAdministration.css'; +import { SidecarSummary } from '../types'; const HeaderComponentsWrapper = styled.div(({ theme }) => css` float: right; @@ -297,7 +298,7 @@ const CollectorsAdministration = ({
); } else { - const sidecars = lodash.uniq(sidecarCollectorPairs.map(({ sidecar }) => sidecar)); + const sidecars = lodash.uniq(sidecarCollectorPairs.map(({ sidecar }) => sidecar)); formattedCollectors = sidecars.map((sidecarToMap) => { const sidecarCollectors = sidecarCollectorPairs diff --git a/graylog2-web-interface/src/components/sidecars/types.d.ts b/graylog2-web-interface/src/components/sidecars/types.d.ts new file mode 100644 index 000000000000..b0692b23bec4 --- /dev/null +++ b/graylog2-web-interface/src/components/sidecars/types.d.ts @@ -0,0 +1,89 @@ +/* + * 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 + * . + */ +export type Configuration = { + readonly template: string; + readonly color: string; + readonly collector_id: string; + readonly name: string; + readonly id: string; + readonly tags: string[]; +} + +export type Collector = { + readonly service_type: string; + readonly node_operating_system: string; + readonly name: string; + readonly validation_parameters: string; + readonly executable_path: string; + readonly execute_parameters: string; + readonly default_template: string; + readonly id: string; +} + +export type CollectorStatus = { + readonly verbose_message: string; + readonly collector_id: string; + readonly message: string; + readonly configuration_id: string; + readonly status: number; +} + +export type ConfigurationAssignment = { + readonly assigned_from_tags: string[]; + readonly collector_id: string; + readonly configuration_id: string; +} + +export type NodeLogFile = { + readonly path: string; + readonly mod_time: string; + readonly size: number; + readonly is_dir: boolean; +} + +export type SidecarSummary = { + readonly node_details: NodeDetails; + readonly assignments: ConfigurationAssignment[]; + readonly collectors: string[]; + readonly last_seen: string; + readonly sidecar_version: string; + readonly node_name: string; + readonly active: boolean; + readonly node_id: string; +} + +export type NodeMetrics = { + readonly cpu_idle: number; + readonly disks_75: string[]; + readonly load_1: number; +} + +export type CollectorStatusList = { + readonly collectors: CollectorStatus[]; + readonly message: string; + readonly status: number; +} + +export type NodeDetails = { + readonly ip: string; + readonly collector_configuration_directory: string; + readonly operating_system: string; + readonly metrics: NodeMetrics; + readonly log_file_list: NodeLogFile[]; + readonly status: CollectorStatusList; + readonly tags: string[]; +} From 4ccdd7ef0b0d490935c18b1096ebff0a9924be07 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Thu, 13 Oct 2022 10:20:53 +0200 Subject: [PATCH 62/82] fix eslint issues --- .../sidecars/administration/CollectorsAdministration.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx index 8d50424833e4..7ea67ce0f923 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx @@ -35,7 +35,8 @@ import CollectorsAdministrationFilters from './CollectorsAdministrationFilters'; import CollectorConfigurationModalContainer from './CollectorConfigurationModalContainer'; import FiltersSummary from './FiltersSummary'; import style from './CollectorsAdministration.css'; -import { SidecarSummary } from '../types'; + +import type { SidecarSummary } from '../types'; const HeaderComponentsWrapper = styled.div(({ theme }) => css` float: right; From e39f841a64cc264e62799f431767a23dc773d622 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Thu, 13 Oct 2022 10:57:57 +0200 Subject: [PATCH 63/82] typed CollectorProcessControl props --- .../sidecars/administration/CollectorProcessControl.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.tsx index f64006d5db91..02bb28e17589 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.tsx @@ -21,9 +21,16 @@ import lodash from 'lodash'; import { Button, Panel, BootstrapModalConfirm } from 'components/bootstrap'; import { Pluralize, SelectPopover } from 'components/common'; +import type { Collector, SidecarSummary } from '../types'; + const PROCESS_ACTIONS = ['start', 'restart', 'stop']; -const CollectorProcessControl = ({ selectedSidecarCollectorPairs, onProcessAction }) => { +type Props = { + selectedSidecarCollectorPairs: { collector: Collector, sidecar: SidecarSummary }[], + onProcessAction: (action: string, pairs: { collector: Collector, sidecar: SidecarSummary }[], callback: () => void) => void, +}; + +const CollectorProcessControl = ({ selectedSidecarCollectorPairs, onProcessAction }: Props) => { const [selectedAction, setSelectedAction] = useState(undefined); const [isConfigurationWarningHidden, setIsConfigurationWarningHidden] = useState(false); const modalRef = useRef(null); From b41c1908ba8597e057caf196b7535c6a5f10e4eb Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Thu, 13 Oct 2022 10:58:16 +0200 Subject: [PATCH 64/82] typed CollectorsAdministration props --- .../CollectorsAdministration.tsx | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx index 7ea67ce0f923..637325b61e2e 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx @@ -29,6 +29,7 @@ import OperatingSystemIcon from 'components/sidecars/common/OperatingSystemIcon' import SidecarSearchForm from 'components/sidecars/common/SidecarSearchForm'; import StatusIndicator from 'components/sidecars/common/StatusIndicator'; import commonStyle from 'components/sidecars/common/CommonSidecarStyles.css'; +import type { Pagination } from 'views/stores/DashboardsStore'; import CollectorsAdministrationActions from './CollectorsAdministrationActions'; import CollectorsAdministrationFilters from './CollectorsAdministrationFilters'; @@ -36,7 +37,7 @@ import CollectorConfigurationModalContainer from './CollectorConfigurationModalC import FiltersSummary from './FiltersSummary'; import style from './CollectorsAdministration.css'; -import type { SidecarSummary } from '../types'; +import type { Collector, Configuration, SidecarSummary } from '../types'; const HeaderComponentsWrapper = styled.div(({ theme }) => css` float: right; @@ -53,6 +54,20 @@ const DisabledCollector = styled.div(({ theme }) => css` export const PAGE_SIZES = [10, 25, 50, 100]; +type Props = { + collectors: Collector[], + configurations: Configuration[], + sidecarCollectorPairs: { collector: Collector, sidecar: SidecarSummary }[], + query: string, + filters: { [_key: string]: string }, + pagination: Pagination, + onPageChange: (currentPage: number, pageSize: number) => void, + onFilter: (collectorIds?: string[], callback?: () => void) => void, + onQueryChange: (query?: string, callback?: () => void) => void, + onConfigurationChange: (pairs: { collector: Collector, sidecar: SidecarSummary }[], configs: Configuration[], callback: () => void) => void, + onProcessAction: (action: string, collectorDict: { [sidecarId: string]: string[] }, callback: () => void) => void, +}; + const CollectorsAdministration = ({ configurations, collectors, @@ -65,13 +80,13 @@ const CollectorsAdministration = ({ onQueryChange, onConfigurationChange, onProcessAction, -}) => { +}: Props) => { const [showConfigurationModal, setShowConfigurationModal] = useState(false); const [selected, setSelected] = useState([]); const selectAllInput = useRef(null); // Filter out sidecars with no compatible collectors - const enabledCollectors = collectors.filter(({ collector }) => !lodash.isEmpty(collector)); + const enabledCollectors = sidecarCollectorPairs.filter(({ collector }) => !lodash.isEmpty(collector)); const sidecarCollectorId = (sidecar, collector) => { return `${sidecar.node_id}-${collector.name}`; From b2322051e6a9136fa19fb90afa30fcb1dae6a0b5 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Thu, 13 Oct 2022 10:59:24 +0200 Subject: [PATCH 65/82] typed CollectorsAdministrationActions props --- .../CollectorsAdministrationActions.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.tsx index 495ff2f36808..7c4c4c204f6b 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.tsx @@ -14,7 +14,7 @@ * along with this program. If not, see * . */ -import React from 'react'; +import React, { useState, useCallback } from 'react'; import PropTypes from 'prop-types'; import lodash from 'lodash'; import styled from 'styled-components'; @@ -25,19 +25,29 @@ import { Icon } from 'components/common'; import CollectorConfigurationModalContainer from './CollectorConfigurationModalContainer'; import CollectorProcessControl from './CollectorProcessControl'; +import type { Collector, Configuration, SidecarSummary } from '../types'; + const ConfigurationButton = styled(Button)` margin-right: 6px `; +type Props = { + collectors: Collector[], + configurations: Configuration[], + selectedSidecarCollectorPairs: { collector: Collector, sidecar: SidecarSummary }[], + onConfigurationSelectionChange: (pairs: { collector: Collector, sidecar: SidecarSummary }[], configs: Configuration[], callback: () => void) => void, + onProcessAction: (action: string, pairs: { collector: Collector, sidecar: SidecarSummary }[], callback: () => void) => void, +}; + const CollectorsAdministrationActions = ({ collectors, configurations, selectedSidecarCollectorPairs, onConfigurationSelectionChange, onProcessAction, -}) => { - const [showConfigurationModal, setShowConfigurationModal] = React.useState(false); - const onCancelConfigurationModal = React.useCallback(() => setShowConfigurationModal(false), []); +}: Props) => { + const [showConfigurationModal, setShowConfigurationModal] = useState(false); + const onCancelConfigurationModal = useCallback(() => setShowConfigurationModal(false), []); const selectedLogCollectorsNames = lodash.uniq(selectedSidecarCollectorPairs.map(({ collector }) => collector.name)); const configButtonTooltip = `Cannot change configurations of ${selectedLogCollectorsNames.join(', ')} collectors simultaneously`; From 384e0f82294ecadae899c18778a2bd013b371507 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Thu, 13 Oct 2022 11:45:22 +0200 Subject: [PATCH 66/82] typed function args and states --- .../CollectorConfigurationModalContainer.tsx | 4 ++-- .../CollectorProcessControl.tsx | 12 +++++----- .../CollectorsAdministration.tsx | 24 +++++++++---------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx index 517fbef30f37..8d5044b06d0d 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx @@ -47,8 +47,8 @@ const CollectorConfigurationModalContainer = ({ show, onCancel, }: Props) => { - const [nextAssignedConfigurations, setNextAssignedConfigurations] = useState([]); - const [nextPartiallyAssignedConfigurations, setNextPartiallyAssignedConfigurations] = useState([]); + const [nextAssignedConfigurations, setNextAssignedConfigurations] = useState([]); + const [nextPartiallyAssignedConfigurations, setNextPartiallyAssignedConfigurations] = useState([]); const modalConfirm = useRef(null); const getSelectedLogCollector = () => { diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.tsx index 02bb28e17589..67187d84e4b7 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.tsx @@ -31,7 +31,7 @@ type Props = { }; const CollectorProcessControl = ({ selectedSidecarCollectorPairs, onProcessAction }: Props) => { - const [selectedAction, setSelectedAction] = useState(undefined); + const [selectedAction, setSelectedAction] = useState(''); const [isConfigurationWarningHidden, setIsConfigurationWarningHidden] = useState(false); const modalRef = useRef(null); @@ -39,13 +39,13 @@ const CollectorProcessControl = ({ selectedSidecarCollectorPairs, onProcessActio setSelectedAction(undefined); }; - const handleProcessActionSelect = (processAction, hideCallback) => { + const handleProcessActionSelect = (processAction: string[], hideCallback: () => void) => { hideCallback(); setSelectedAction(processAction ? processAction[0] : undefined); modalRef.current?.open(); }; - const confirmProcessAction = (doneCallback) => { + const confirmProcessAction = (doneCallback: () => void) => { const callback = () => { doneCallback(); resetSelectedAction(); @@ -62,7 +62,7 @@ const CollectorProcessControl = ({ selectedSidecarCollectorPairs, onProcessActio setIsConfigurationWarningHidden(true); }; - const renderSummaryContent = (selectedSidecars) => { + const renderSummaryContent = (selectedSidecars: string[]) => { return ( <>

@@ -93,7 +93,7 @@ const CollectorProcessControl = ({ selectedSidecarCollectorPairs, onProcessActio }; const renderProcessActionSummary = () => { - const selectedSidecars = lodash.uniq(selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name)); + const selectedSidecars = lodash.uniq(selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar.node_name)); // Check if all selected collectors have assigned configurations const allHaveConfigurationsAssigned = selectedSidecarCollectorPairs.every(({ collector, sidecar }) => { @@ -117,7 +117,7 @@ const CollectorProcessControl = ({ selectedSidecarCollectorPairs, onProcessActio ); }; - const actionFormatter = (action) => lodash.capitalize(action); + const actionFormatter = (action: string) => lodash.capitalize(action); return ( diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx index 637325b61e2e..c105f64f0248 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx @@ -82,17 +82,17 @@ const CollectorsAdministration = ({ onProcessAction, }: Props) => { const [showConfigurationModal, setShowConfigurationModal] = useState(false); - const [selected, setSelected] = useState([]); + const [selected, setSelected] = useState([]); const selectAllInput = useRef(null); // Filter out sidecars with no compatible collectors const enabledCollectors = sidecarCollectorPairs.filter(({ collector }) => !lodash.isEmpty(collector)); - const sidecarCollectorId = (sidecar, collector) => { + const sidecarCollectorId = (sidecar: SidecarSummary, collector: Collector) => { return `${sidecar.node_id}-${collector.name}`; }; - const isAllSelected = (_collectors, _selected) => { + const isAllSelected = (_collectors: ({ collector: Collector, sidecar: SidecarSummary }|Collector)[], _selected: string[]) => { return _collectors.length > 0 && _collectors.length === _selected.length; }; @@ -105,11 +105,11 @@ const CollectorsAdministration = ({ } }, [selectAllInput, collectors, selected]); - const handleConfigurationChange = (selectedSidecars, selectedConfigurations, doneCallback) => { + const handleConfigurationChange = (selectedSidecars: { collector: Collector, sidecar: SidecarSummary }[], selectedConfigurations: Configuration[], doneCallback: () => void) => { onConfigurationChange(selectedSidecars, selectedConfigurations, doneCallback); }; - const handleProcessAction = (action, selectedSidecarCollectorPairs, doneCallback) => { + const handleProcessAction = (action: string, selectedSidecarCollectorPairs: { collector: Collector, sidecar: SidecarSummary }[], doneCallback: () => void) => { const selectedCollectors = {}; selectedSidecarCollectorPairs.forEach(({ sidecar, collector }) => { @@ -131,7 +131,7 @@ const CollectorsAdministration = ({ setSelected(newSelection); }; - const formatHeader = (selectedSidecarCollectorPairs) => { + const formatHeader = (selectedSidecarCollectorPairs: { collector: Collector, sidecar: SidecarSummary }[]) => { const selectedItems = selected.length; let headerMenu; @@ -169,7 +169,7 @@ const CollectorsAdministration = ({ ); }; - const handleSidecarCollectorSelect = (collectorId) => { + const handleSidecarCollectorSelect = (collectorId: string) => { return (event) => { const newSelection = (event.target.checked ? lodash.union(selected, [collectorId]) @@ -179,7 +179,7 @@ const CollectorsAdministration = ({ }; }; - const formatSidecarNoCollectors = (sidecar) => { + const formatSidecarNoCollectors = (sidecar: SidecarSummary) => { return ( @@ -203,7 +203,7 @@ const CollectorsAdministration = ({ ); }; - const formatCollector = (sidecar, collector, _configurations) => { + const formatCollector = (sidecar: SidecarSummary, collector: Collector, _configurations: Configuration[]) => { const collectorId = sidecarCollectorId(sidecar, collector); const configAssignmentIDs = sidecar.assignments.filter((assignment) => assignment.collector_id === collector.id).map((assignment) => assignment.configuration_id); const configAssignments = _configurations.filter((config) => configAssignmentIDs.includes(config.id)).sort((c1, c2) => naturalSortIgnoreCase(c1.name, c2.name)); @@ -267,7 +267,7 @@ const CollectorsAdministration = ({ ); }; - const formatSidecar = (sidecar, _collectors, _configurations) => { + const formatSidecar = (sidecar: SidecarSummary, _collectors: Collector[], _configurations: Configuration[]) => { if (_collectors.length === 0) { return formatSidecarNoCollectors(sidecar); } @@ -289,8 +289,8 @@ const CollectorsAdministration = ({ ); }; - const handleSearch = (_query, callback) => { - onQueryChange(_query, callback()); + const handleSearch = (_query: string, callback: () => void) => { + onQueryChange(_query, callback); }; const handleReset = () => { From e97aaa14525fc7c9102fa6098a38b46c2fa7bc6b Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Thu, 13 Oct 2022 12:13:56 +0200 Subject: [PATCH 67/82] converted ConfigurationTagsSelect to typescript and functional component --- ...Select.jsx => ConfigurationTagsSelect.tsx} | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) rename graylog2-web-interface/src/components/sidecars/configuration-forms/{ConfigurationTagsSelect.jsx => ConfigurationTagsSelect.tsx} (51%) diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.jsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.tsx similarity index 51% rename from graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.jsx rename to graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.tsx index 3950a8cd3065..82869ddf82a6 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.jsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.tsx @@ -19,31 +19,39 @@ import React from 'react'; import MultiSelect from 'components/common/MultiSelect'; -class ConfigurationTagsSelect extends React.Component { - static propTypes = { - tags: PropTypes.arrayOf(PropTypes.string), - availableTags: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, - }; +type Props = { + tags: string[], + availableTags: { name: string }[], + onChange: () => void, +}; - static defaultProps = { - tags: [], - }; +const ConfigurationTagsSelect = ({ + tags, + availableTags, + onChange, +}: Props) => { + const tagsValue = tags.join(','); + const tagsOptions = availableTags.map((tag) => { + return { value: tag.name, label: tag.name }; + }); - render() { - const tagsValue = this.props.tags.join(','); - const tagsOptions = this.props.availableTags.map((tag) => { - return { value: tag.name, label: tag.name }; - }); + return ( + + ); +}; - return ( - - ); - } -} +ConfigurationTagsSelect.propTypes = { + tags: PropTypes.arrayOf(PropTypes.string), + availableTags: PropTypes.array.isRequired, + onChange: PropTypes.func.isRequired, +}; + +ConfigurationTagsSelect.defaultProps = { + tags: [], +}; export default ConfigurationTagsSelect; From 645bf64209b4b83c3b25a37b845d192aac335baa Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Thu, 13 Oct 2022 12:14:33 +0200 Subject: [PATCH 68/82] converted TemplatesHelper to typescript and functional component --- .../configuration-forms/TemplatesHelper.jsx | 70 ------------------- .../configuration-forms/TemplatesHelper.tsx | 68 ++++++++++++++++++ 2 files changed, 68 insertions(+), 70 deletions(-) delete mode 100644 graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx create mode 100644 graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.tsx diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx deleted file mode 100644 index d9af3f57e4e6..000000000000 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.jsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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 - * . - */ -import React from 'react'; - -import { Table } from 'components/bootstrap'; - -class TemplatesHelper extends React.Component { - static _buildVariableName = (name) => { - return `\${sidecar.${name}}`; - }; - - render() { - return ( -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameDescription
{TemplatesHelper._buildVariableName('operatingSystem')}Name of the operating system the sidecar is running on, e.g. "Linux", "Windows"
{TemplatesHelper._buildVariableName('nodeName')}The name of the sidecar, defaults to hostname if not set.
{TemplatesHelper._buildVariableName('nodeId')}UUID of the sidecar.
{TemplatesHelper._buildVariableName('sidecarVersion')}Version string of the running sidecar.
{TemplatesHelper._buildVariableName('spoolDir')}A directory that is unique per configuration and can be used to store collector data.
{TemplatesHelper._buildVariableName('tags.')}A map of tags that are set for the sidecar. This can be used to render conditional configuration snippets. e.g.:
- <#if sidecar.tags.webserver??>
  - /var/log/apache/*.log
</#if>
-
-
- ); - } -} - -export default TemplatesHelper; diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.tsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.tsx new file mode 100644 index 000000000000..e6c7248061e9 --- /dev/null +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/TemplatesHelper.tsx @@ -0,0 +1,68 @@ +/* + * 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 + * . + */ +import React from 'react'; + +import { Table } from 'components/bootstrap'; + +const TemplatesHelper = () => { + const _buildVariableName = (name) => { + return `\${sidecar.${name}}`; + }; + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescription
{_buildVariableName('operatingSystem')}Name of the operating system the sidecar is running on, e.g. "Linux", "Windows"
{_buildVariableName('nodeName')}The name of the sidecar, defaults to hostname if not set.
{_buildVariableName('nodeId')}UUID of the sidecar.
{_buildVariableName('sidecarVersion')}Version string of the running sidecar.
{_buildVariableName('spoolDir')}A directory that is unique per configuration and can be used to store collector data.
{_buildVariableName('tags.')}A map of tags that are set for the sidecar. This can be used to render conditional configuration snippets. e.g.:
+ <#if sidecar.tags.webserver??>
  - /var/log/apache/*.log
</#if>
+
+
+ ); +}; + +export default TemplatesHelper; From f1bef6841a1ef5a02ba48f8be3160e8095396ced Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Thu, 13 Oct 2022 13:27:18 +0200 Subject: [PATCH 69/82] converted ConfigurationForm to typescript and functional component --- .../configuration-forms/ConfigurationForm.jsx | 399 ------------------ .../configuration-forms/ConfigurationForm.tsx | 377 +++++++++++++++++ .../ConfigurationTagsSelect.tsx | 2 +- .../src/components/sidecars/types.d.ts | 5 + 4 files changed, 383 insertions(+), 400 deletions(-) delete mode 100644 graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx create mode 100644 graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx deleted file mode 100644 index b10bd3e45222..000000000000 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.jsx +++ /dev/null @@ -1,399 +0,0 @@ -/* - * 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 - * . - */ -import PropTypes from 'prop-types'; -import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import createReactClass from 'create-react-class'; -import Reflux from 'reflux'; -import lodash from 'lodash'; - -import { ColorPickerPopover, FormSubmit, Select, SourceCodeEditor } from 'components/common'; -import { Button, Col, ControlLabel, FormControl, FormGroup, HelpBlock, Row, Input } from 'components/bootstrap'; -import history from 'util/History'; -import Routes from 'routing/Routes'; -import ColorLabel from 'components/sidecars/common/ColorLabel'; -import { CollectorConfigurationsActions } from 'stores/sidecars/CollectorConfigurationsStore'; -import { CollectorsActions, CollectorsStore } from 'stores/sidecars/CollectorsStore'; - -import SourceViewModal from './SourceViewModal'; -import ImportsViewModal from './ImportsViewModal'; -import ConfigurationTagsSelect from './ConfigurationTagsSelect'; - -const ConfigurationForm = createReactClass({ - // eslint-disable-next-line react/no-unused-class-component-methods - displayName: 'ConfigurationForm', - // eslint-disable-next-line react/no-unused-class-component-methods - propTypes: { - action: PropTypes.oneOf(['create', 'edit']), - configuration: PropTypes.object, - configurationSidecars: PropTypes.object, - }, - - mixins: [Reflux.connect(CollectorsStore)], - - getDefaultProps() { - return { - action: 'edit', - configuration: { - color: '#FFFFFF', - template: '', - }, - configurationSidecars: {}, - }; - }, - - getInitialState() { - const { configuration } = this.props; - - return { - error: false, - validation_errors: {}, - formData: { - id: configuration.id, - name: configuration.name, - color: configuration.color, - collector_id: configuration.collector_id, - template: configuration.template || '', - tags: configuration.tags || [], - }, - }; - }, - - UNSAFE_componentWillMount() { - this._debouncedValidateFormData = lodash.debounce(this._validateFormData, 200); - }, - - componentDidMount() { - CollectorsActions.all(); - }, - - defaultTemplates: {}, - - _isTemplateSet(template) { - return template !== undefined && template !== ''; - }, - - _hasErrors() { - const { error, formData } = this.state; - - return error || !this._isTemplateSet(formData.template); - }, - - _validateFormData(nextFormData, checkForRequiredFields) { - CollectorConfigurationsActions.validate(nextFormData).then((validation) => { - const nextValidation = lodash.clone(validation); - - if (checkForRequiredFields && !this._isTemplateSet(nextFormData.template)) { - nextValidation.errors.template = ['Please fill out the configuration field.']; - nextValidation.failed = true; - } - - this.setState({ validation_errors: nextValidation.errors, error: nextValidation.failed }); - }); - }, - - _save() { - const { action } = this.props; - const { formData } = this.state; - - if (this._hasErrors()) { - // Ensure we display an error on the template field, as this is not validated by the browser - this._validateFormData(formData, true); - - return; - } - - if (action === 'create') { - CollectorConfigurationsActions.createConfiguration(formData) - .then(() => history.push(Routes.SYSTEM.SIDECARS.CONFIGURATION)); - } else { - CollectorConfigurationsActions.updateConfiguration(formData); - } - }, - - _formDataUpdate(key) { - const { formData } = this.state; - - return (nextValue, _, hideCallback) => { - const nextFormData = lodash.cloneDeep(formData); - - nextFormData[key] = nextValue; - this._debouncedValidateFormData(nextFormData, false); - this.setState({ formData: nextFormData }, hideCallback); - }; - }, - - // eslint-disable-next-line react/no-unused-class-component-methods - replaceConfigurationVariableName(oldname, newname) { - const { formData } = this.state; - - if (oldname === '' || oldname === newname) { - return; - } - - // replaceAll without having to use a Regex - const updatedTemplate = formData.template.split(`\${user.${oldname}}`).join(`\${user.${newname}}`); - - this._onTemplateChange(updatedTemplate); - }, - - _onNameChange(event) { - const nextName = event.target.value; - - this._formDataUpdate('name')(nextName); - }, - - _onTagsChange(nextTags) { - const nextTagsArray = nextTags.split(','); - - this._formDataUpdate('tags')(nextTagsArray); - }, - - _getCollectorDefaultTemplate(collectorId) { - const storedTemplate = this.defaultTemplates[collectorId]; - - if (storedTemplate !== undefined) { - // eslint-disable-next-line no-promise-executor-return - return new Promise((resolve) => resolve(storedTemplate)); - } - - return CollectorsActions.getCollector(collectorId).then((collector) => { - this.defaultTemplates[collectorId] = collector.default_template; - - return collector.default_template; - }); - }, - - _onCollectorChange(nextId) { - const { formData } = this.state; - - // Start loading the request to get the default template, so it is available asap. - const defaultTemplatePromise = this._getCollectorDefaultTemplate(nextId); - - const nextFormData = lodash.cloneDeep(formData); - - nextFormData.collector_id = nextId; - - // eslint-disable-next-line no-alert - if (!nextFormData.template || window.confirm('Do you want to use the default template for the selected Configuration?')) { - // Wait for the promise to resolve and then update the whole formData state - defaultTemplatePromise.then((defaultTemplate) => { - this._onTemplateChange(defaultTemplate); - nextFormData.template = defaultTemplate; - }); - } - - this.setState({ formData: nextFormData }); - }, - - _onTemplateImport(nextTemplate) { - const { formData } = this.state; - - const nextFormData = lodash.cloneDeep(formData); - - // eslint-disable-next-line no-alert - if (!nextFormData.template || window.confirm('Do you want to overwrite your current work with this Configuration?')) { - this._onTemplateChange(nextTemplate); - } - }, - - _onTemplateChange(nextTemplate) { - this._formDataUpdate('template')(nextTemplate); - }, - - _onSubmit(event) { - event.preventDefault(); - this._save(); - }, - - _onCancel() { - history.goBack(); - }, - - _onShowSource() { - this.previewModal.open(); - }, - - _onShowImports() { - this.uploadsModal.open(); - }, - - _formatCollector(collector) { - return collector ? `${collector.name} on ${lodash.upperFirst(collector.node_operating_system)}` : 'Unknown collector'; - }, - - _formatCollectorOptions() { - const { collectors } = this.state; - - const options = []; - - if (collectors) { - collectors.forEach((collector) => { - options.push({ value: collector.id, label: this._formatCollector(collector) }); - }); - } else { - options.push({ value: 'none', label: 'Loading collector list...', disable: true }); - } - - return options; - }, - - // eslint-disable-next-line react/no-unstable-nested-components - _formatValidationMessage(fieldName, defaultText) { - const { validation_errors: validationErrors } = this.state; - - if (validationErrors[fieldName]) { - return {validationErrors[fieldName][0]}; - } - - return {defaultText}; - }, - - _validationState(fieldName) { - const { validation_errors: validationErrors } = this.state; - - if (validationErrors[fieldName]) { - return 'error'; - } - - return null; - }, - - // eslint-disable-next-line react/no-unstable-nested-components - _renderCollectorTypeField(collectorId, collectors, configurationSidecars) { - const isConfigurationInUse = configurationSidecars.sidecar_ids && configurationSidecars.sidecar_ids.length > 0; - - if (isConfigurationInUse) { - const collector = collectors ? collectors.find((c) => c.id === collectorId) : undefined; - - return ( - - {this._formatCollector(collector)} - - Note: Log Collector cannot change while the Configuration is in use. Clone the Configuration - to test it using another Collector. - - - ); - } - - return ( - - - - - Configuration color -
- -
- Change color} - onChange={this._formDataUpdate('color')} /> -
-
- Choose a color to use for this configuration. -
- - - Configuration Tags -
- ({ name: tag }))} - tags={formData.tags} - onChange={this._onTagsChange} - className="form-control" /> -
- Choose tags to use for this configuration. -
- - - Collector - {this._renderCollectorTypeField(formData.collector_id, collectors, configurationSidecars)} - - - - Configuration - - - - - {this._formatValidationMessage('template', 'Required. Collector configuration, see quick reference for more information.')} - - - - - - - - - - - { this.previewModal = c; }} - templateString={formData.template} /> - { this.uploadsModal = c; }} - onApply={this._onTemplateImport} /> - - ); - }, -}); - -export default ConfigurationForm; diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx new file mode 100644 index 000000000000..69eab4756503 --- /dev/null +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx @@ -0,0 +1,377 @@ +/* + * 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 + * . + */ +import PropTypes from 'prop-types'; +import React, { useState, useRef, useEffect } from 'react'; +import lodash from 'lodash'; + +import history from 'util/History'; +import { ColorPickerPopover, FormSubmit, Select, SourceCodeEditor } from 'components/common'; +import { Button, Col, ControlLabel, FormControl, FormGroup, HelpBlock, Row, Input } from 'components/bootstrap'; +import Routes from 'routing/Routes'; +import ColorLabel from 'components/sidecars/common/ColorLabel'; +import { CollectorConfigurationsActions } from 'stores/sidecars/CollectorConfigurationsStore'; +import { CollectorsActions } from 'stores/sidecars/CollectorsStore'; + +import SourceViewModal from './SourceViewModal'; +import ImportsViewModal from './ImportsViewModal'; +import ConfigurationTagsSelect from './ConfigurationTagsSelect'; + +import type { Collector, Configuration, ConfigurationSidecarsResponse } from '../types'; + +type Props = { + action: string, + configuration: Configuration, + configurationSidecars: ConfigurationSidecarsResponse, +}; + +const ConfigurationForm = ({ + action, + configuration, + configurationSidecars, +}: Props) => { + const initFormData = { + id: configuration.id, + name: configuration.name, + color: configuration.color, + collector_id: configuration.collector_id, + template: configuration.template || '', + tags: configuration.tags || [], + }; + + const [collectors, setCollectors] = useState([]); + const [formData, setFormData] = useState(initFormData); + const [error, setError] = useState(false); + const [validationErrors, setValidationErrors] = useState({}); + const previewModal = useRef(null); + const uploadsModal = useRef(null); + + useEffect(() => { + CollectorsActions.all().then((response) => setCollectors(response.collectors)); + }, []); + + const defaultTemplates = {}; + + const _isTemplateSet = (template) => { + return template !== undefined && template !== ''; + }; + + const _hasErrors = () => { + return error || !_isTemplateSet(formData.template); + }; + + const _validateFormData = (nextFormData, checkForRequiredFields) => { + CollectorConfigurationsActions.validate(nextFormData).then((validation) => { + const nextValidation = lodash.clone(validation); + + if (checkForRequiredFields && !_isTemplateSet(nextFormData.template)) { + nextValidation.errors.template = ['Please fill out the configuration field.']; + nextValidation.failed = true; + } + + setValidationErrors(nextValidation.errors); + setError(nextValidation.failed); + }); + }; + + const _save = () => { + if (_hasErrors()) { + // Ensure we display an error on the template field, as this is not validated by the browser + _validateFormData(formData, true); + + return; + } + + if (action === 'create') { + CollectorConfigurationsActions.createConfiguration(formData) + .then(() => history.push(Routes.SYSTEM.SIDECARS.CONFIGURATION)); + } else { + CollectorConfigurationsActions.updateConfiguration(formData); + } + }; + + const _debouncedValidateFormData = lodash.debounce(_validateFormData, 200); + + const _formDataUpdate = (key) => { + return (nextValue, _?: React.ChangeEvent, hideCallback?: () => void) => { + const nextFormData = lodash.cloneDeep(formData); + + nextFormData[key] = nextValue; + _debouncedValidateFormData(nextFormData, false); + setFormData(nextFormData); + + if (hideCallback) { + hideCallback(); + } + }; + }; + + const _onTemplateChange = (nextTemplate) => { + _formDataUpdate('template')(nextTemplate); + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const replaceConfigurationVariableName = (oldname, newname) => { + if (oldname === '' || oldname === newname) { + return; + } + + // replaceAll without having to use a Regex + const updatedTemplate = formData.template.split(`\${user.${oldname}}`).join(`\${user.${newname}}`); + + _onTemplateChange(updatedTemplate); + }; + + const _onNameChange = (event) => { + const nextName = event.target.value; + + _formDataUpdate('name')(nextName); + }; + + const _onTagsChange = (nextTags) => { + const nextTagsArray = nextTags.split(','); + + _formDataUpdate('tags')(nextTagsArray); + }; + + const _getCollectorDefaultTemplate = (collectorId) => { + const storedTemplate = defaultTemplates[collectorId]; + + if (storedTemplate !== undefined) { + // eslint-disable-next-line no-promise-executor-return + return new Promise((resolve) => resolve(storedTemplate)); + } + + return CollectorsActions.getCollector(collectorId).then((collector) => { + defaultTemplates[collectorId] = collector.default_template; + + return collector.default_template; + }); + }; + + const _onCollectorChange = (nextId) => { + // Start loading the request to get the default template, so it is available asap. + const defaultTemplatePromise = _getCollectorDefaultTemplate(nextId); + + const nextFormData = lodash.cloneDeep(formData); + + nextFormData.collector_id = nextId; + + // eslint-disable-next-line no-alert + if (!nextFormData.template || window.confirm('Do you want to use the default template for the selected Configuration?')) { + // Wait for the promise to resolve and then update the whole formData state + defaultTemplatePromise.then((defaultTemplate) => { + _onTemplateChange(defaultTemplate); + nextFormData.template = defaultTemplate; + }); + } + + setFormData(nextFormData); + }; + + const _onTemplateImport = (nextTemplate) => { + const nextFormData = lodash.cloneDeep(formData); + + // eslint-disable-next-line no-alert + if (!nextFormData.template || window.confirm('Do you want to overwrite your current work with this Configuration?')) { + _onTemplateChange(nextTemplate); + } + }; + + const _onSubmit = (event) => { + event.preventDefault(); + _save(); + }; + + const _onCancel = () => { + history.goBack(); + }; + + const _onShowSource = () => { + previewModal.current.open(); + }; + + const _onShowImports = () => { + uploadsModal.current.open(); + }; + + const _formatCollector = (collector) => { + return collector ? `${collector.name} on ${lodash.upperFirst(collector.node_operating_system)}` : 'Unknown collector'; + }; + + const _formatCollectorOptions = () => { + const options = []; + + if (collectors) { + collectors.forEach((collector) => { + options.push({ value: collector.id, label: _formatCollector(collector) }); + }); + } else { + options.push({ value: 'none', label: 'Loading collector list...', disable: true }); + } + + return options; + }; + + const _formatValidationMessage = (fieldName, defaultText) => { + if (validationErrors[fieldName]) { + return {validationErrors[fieldName][0]}; + } + + return {defaultText}; + }; + + const _validationState = (fieldName) => { + if (validationErrors[fieldName]) { + return 'error'; + } + + return null; + }; + + const _renderCollectorTypeField = (collectorId, _collectors, _configurationSidecars) => { + const isConfigurationInUse = _configurationSidecars.sidecar_ids && _configurationSidecars.sidecar_ids.length > 0; + + if (isConfigurationInUse) { + const collector = _collectors ? _collectors.find((c) => c.id === collectorId) : undefined; + + return ( + + {_formatCollector(collector)} + + Note: Log Collector cannot change while the Configuration is in use. Clone the Configuration + to test it using another Collector. + + + ); + } + + return ( + + + + + Configuration color +
+ +
+ Change color} + onChange={_formDataUpdate('color')} /> +
+
+ Choose a color to use for this configuration. +
+ + + Configuration Tags +
+ ({ name: tag }))} + tags={formData.tags} + onChange={_onTagsChange} /> +
+ Choose tags to use for this configuration. +
+ + + Collector + {_renderCollectorTypeField(formData.collector_id, collectors, configurationSidecars)} + + + + Configuration + {/* TODO: Figure out issue with props */} + {/* @ts-ignore */} + + + + + {_formatValidationMessage('template', 'Required. Collector configuration, see quick reference for more information.')} + + + + + + + + + + + + + + ); +}; + +ConfigurationForm.propTypes = { + action: PropTypes.oneOf(['create', 'edit']), + configuration: PropTypes.object, + configurationSidecars: PropTypes.object, +}; + +ConfigurationForm.defaultProps = { + action: 'edit', + configuration: { + color: '#FFFFFF', + template: '', + }, + configurationSidecars: {}, +}; + +export default ConfigurationForm; diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.tsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.tsx index 82869ddf82a6..c4fddb68f8bf 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.tsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.tsx @@ -22,7 +22,7 @@ import MultiSelect from 'components/common/MultiSelect'; type Props = { tags: string[], availableTags: { name: string }[], - onChange: () => void, + onChange: (tagsAsString: string) => void, }; const ConfigurationTagsSelect = ({ diff --git a/graylog2-web-interface/src/components/sidecars/types.d.ts b/graylog2-web-interface/src/components/sidecars/types.d.ts index b0692b23bec4..243a60689af9 100644 --- a/graylog2-web-interface/src/components/sidecars/types.d.ts +++ b/graylog2-web-interface/src/components/sidecars/types.d.ts @@ -87,3 +87,8 @@ export type NodeDetails = { readonly status: CollectorStatusList; readonly tags: string[]; } + +export type ConfigurationSidecarsResponse = { + readonly sidecar_ids: string[]; + readonly configuration_id: string; +} From 9b812d139aebce52041205226f1734396ad0b9d5 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Thu, 13 Oct 2022 13:47:35 +0200 Subject: [PATCH 70/82] removed unused function --- .../configuration-forms/ConfigurationForm.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx index 69eab4756503..1ae32b2f2508 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx @@ -123,18 +123,6 @@ const ConfigurationForm = ({ _formDataUpdate('template')(nextTemplate); }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const replaceConfigurationVariableName = (oldname, newname) => { - if (oldname === '' || oldname === newname) { - return; - } - - // replaceAll without having to use a Regex - const updatedTemplate = formData.template.split(`\${user.${oldname}}`).join(`\${user.${newname}}`); - - _onTemplateChange(updatedTemplate); - }; - const _onNameChange = (event) => { const nextName = event.target.value; From 02237f999b00386aae8e5146f02cdec06b9a97bf Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Fri, 14 Oct 2022 12:54:08 +0200 Subject: [PATCH 71/82] fixed config creation form --- .../configuration-forms/ConfigurationForm.tsx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx index 1ae32b2f2508..c76a7bc7fb67 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx @@ -58,13 +58,12 @@ const ConfigurationForm = ({ const [validationErrors, setValidationErrors] = useState({}); const previewModal = useRef(null); const uploadsModal = useRef(null); + const defaultTemplates = useRef({}); useEffect(() => { CollectorsActions.all().then((response) => setCollectors(response.collectors)); }, []); - const defaultTemplates = {}; - const _isTemplateSet = (template) => { return template !== undefined && template !== ''; }; @@ -136,7 +135,7 @@ const ConfigurationForm = ({ }; const _getCollectorDefaultTemplate = (collectorId) => { - const storedTemplate = defaultTemplates[collectorId]; + const storedTemplate = defaultTemplates.current[collectorId]; if (storedTemplate !== undefined) { // eslint-disable-next-line no-promise-executor-return @@ -144,15 +143,15 @@ const ConfigurationForm = ({ } return CollectorsActions.getCollector(collectorId).then((collector) => { - defaultTemplates[collectorId] = collector.default_template; + defaultTemplates.current[collectorId] = collector.default_template; return collector.default_template; }); }; - const _onCollectorChange = (nextId) => { + const _onCollectorChange = async (nextId) => { // Start loading the request to get the default template, so it is available asap. - const defaultTemplatePromise = _getCollectorDefaultTemplate(nextId); + const defaultTemplate = await _getCollectorDefaultTemplate(nextId); const nextFormData = lodash.cloneDeep(formData); @@ -160,11 +159,8 @@ const ConfigurationForm = ({ // eslint-disable-next-line no-alert if (!nextFormData.template || window.confirm('Do you want to use the default template for the selected Configuration?')) { - // Wait for the promise to resolve and then update the whole formData state - defaultTemplatePromise.then((defaultTemplate) => { - _onTemplateChange(defaultTemplate); - nextFormData.template = defaultTemplate; - }); + _onTemplateChange(defaultTemplate); + nextFormData.template = defaultTemplate; } setFormData(nextFormData); From 3e106c4ec959a8eb13864bdc07060f7f4716a27f Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Fri, 14 Oct 2022 12:56:47 +0200 Subject: [PATCH 72/82] removed read only from types --- .../src/components/sidecars/types.d.ts | 98 +++++++++---------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/types.d.ts b/graylog2-web-interface/src/components/sidecars/types.d.ts index 243a60689af9..363af7858f7d 100644 --- a/graylog2-web-interface/src/components/sidecars/types.d.ts +++ b/graylog2-web-interface/src/components/sidecars/types.d.ts @@ -15,80 +15,80 @@ * . */ export type Configuration = { - readonly template: string; - readonly color: string; - readonly collector_id: string; - readonly name: string; - readonly id: string; - readonly tags: string[]; + template: string; + color: string; + collector_id: string; + name: string; + id: string; + tags: string[]; } export type Collector = { - readonly service_type: string; - readonly node_operating_system: string; - readonly name: string; - readonly validation_parameters: string; - readonly executable_path: string; - readonly execute_parameters: string; - readonly default_template: string; - readonly id: string; + service_type: string; + node_operating_system: string; + name: string; + validation_parameters: string; + executable_path: string; + execute_parameters: string; + default_template: string; + id: string; } export type CollectorStatus = { - readonly verbose_message: string; - readonly collector_id: string; - readonly message: string; - readonly configuration_id: string; - readonly status: number; + verbose_message: string; + collector_id: string; + message: string; + configuration_id: string; + status: number; } export type ConfigurationAssignment = { - readonly assigned_from_tags: string[]; - readonly collector_id: string; - readonly configuration_id: string; + assigned_from_tags: string[]; + collector_id: string; + configuration_id: string; } export type NodeLogFile = { - readonly path: string; - readonly mod_time: string; - readonly size: number; - readonly is_dir: boolean; + path: string; + mod_time: string; + size: number; + is_dir: boolean; } export type SidecarSummary = { - readonly node_details: NodeDetails; - readonly assignments: ConfigurationAssignment[]; - readonly collectors: string[]; - readonly last_seen: string; - readonly sidecar_version: string; - readonly node_name: string; - readonly active: boolean; - readonly node_id: string; + node_details: NodeDetails; + assignments: ConfigurationAssignment[]; + collectors: string[]; + last_seen: string; + sidecar_version: string; + node_name: string; + active: boolean; + node_id: string; } export type NodeMetrics = { - readonly cpu_idle: number; - readonly disks_75: string[]; - readonly load_1: number; + cpu_idle: number; + disks_75: string[]; + load_1: number; } export type CollectorStatusList = { - readonly collectors: CollectorStatus[]; - readonly message: string; - readonly status: number; + collectors: CollectorStatus[]; + message: string; + status: number; } export type NodeDetails = { - readonly ip: string; - readonly collector_configuration_directory: string; - readonly operating_system: string; - readonly metrics: NodeMetrics; - readonly log_file_list: NodeLogFile[]; - readonly status: CollectorStatusList; - readonly tags: string[]; + ip: string; + collector_configuration_directory: string; + operating_system: string; + metrics: NodeMetrics; + log_file_list: NodeLogFile[]; + status: CollectorStatusList; + tags: string[]; } export type ConfigurationSidecarsResponse = { - readonly sidecar_ids: string[]; - readonly configuration_id: string; + sidecar_ids: string[]; + configuration_id: string; } From 63adfbc56f907fe039ebdbcf8bb45e469b2c1df6 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Fri, 14 Oct 2022 13:04:29 +0200 Subject: [PATCH 73/82] used Proptypes.shape for configuration --- .../sidecars/configuration-forms/ConfigurationForm.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx index c76a7bc7fb67..f6a418a84e6c 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx @@ -345,7 +345,14 @@ const ConfigurationForm = ({ ConfigurationForm.propTypes = { action: PropTypes.oneOf(['create', 'edit']), - configuration: PropTypes.object, + configuration: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + color: PropTypes.string.isRequired, + collector_id: PropTypes.string.isRequired, + template: PropTypes.string.isRequired, + tags: PropTypes.array.isRequired, + }), configurationSidecars: PropTypes.object, }; From 7e4bf2eedcfc9bbafca6924f85a8dc1f1e8094b8 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Fri, 14 Oct 2022 16:04:44 +0200 Subject: [PATCH 74/82] fixed console error in SidecarNewConfigurationPage --- .../configuration-forms/ConfigurationForm.tsx | 184 ++++++++++-------- .../pages/SidecarEditConfigurationPage.jsx | 120 ------------ .../pages/SidecarEditConfigurationPage.tsx | 99 ++++++++++ .../src/pages/SidecarNewConfigurationPage.jsx | 75 ------- .../src/pages/SidecarNewConfigurationPage.tsx | 56 ++++++ 5 files changed, 256 insertions(+), 278 deletions(-) delete mode 100644 graylog2-web-interface/src/pages/SidecarEditConfigurationPage.jsx create mode 100644 graylog2-web-interface/src/pages/SidecarEditConfigurationPage.tsx delete mode 100644 graylog2-web-interface/src/pages/SidecarNewConfigurationPage.jsx create mode 100644 graylog2-web-interface/src/pages/SidecarNewConfigurationPage.tsx diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx index f6a418a84e6c..7025dbb693d9 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationForm.tsx @@ -25,6 +25,7 @@ import Routes from 'routing/Routes'; import ColorLabel from 'components/sidecars/common/ColorLabel'; import { CollectorConfigurationsActions } from 'stores/sidecars/CollectorConfigurationsStore'; import { CollectorsActions } from 'stores/sidecars/CollectorsStore'; +import ConfigurationHelper from 'components/sidecars/configuration-forms/ConfigurationHelper'; import SourceViewModal from './SourceViewModal'; import ImportsViewModal from './ImportsViewModal'; @@ -122,6 +123,17 @@ const ConfigurationForm = ({ _formDataUpdate('template')(nextTemplate); }; + const replaceConfigurationVariableName = (oldname: string, newname: string) => { + if (oldname === '' || oldname === newname) { + return; + } + + // replaceAll without having to use a Regex + const updatedTemplate = formData.template.split(`\${user.${oldname}}`).join(`\${user.${newname}}`); + + _onTemplateChange(updatedTemplate); + }; + const _onNameChange = (event) => { const nextName = event.target.value; @@ -257,89 +269,95 @@ const ConfigurationForm = ({ }; return ( -
-
-
- - - - Configuration color -
- -
- Change color} - onChange={_formDataUpdate('color')} /> -
-
- Choose a color to use for this configuration. -
- - - Configuration Tags -
- ({ name: tag }))} - tags={formData.tags} - onChange={_onTagsChange} /> -
- Choose tags to use for this configuration. -
- - - Collector - {_renderCollectorTypeField(formData.collector_id, collectors, configurationSidecars)} - - - - Configuration - {/* TODO: Figure out issue with props */} - {/* @ts-ignore */} - - - - - {_formatValidationMessage('template', 'Required. Collector configuration, see quick reference for more information.')} - - -
- - - - - - -
- - -
+ + +
+
+
+ + + Configuration color +
+ +
+ Change color} + onChange={_formDataUpdate('color')} /> +
+
+ Choose a color to use for this configuration. +
+ + + Configuration Tags +
+ ({ name: tag }))} + tags={formData.tags} + onChange={_onTagsChange} /> +
+ Choose tags to use for this configuration. +
+ + + Collector + {_renderCollectorTypeField(formData.collector_id, collectors, configurationSidecars)} + + + + Configuration + {/* TODO: Figure out issue with props */} + {/* @ts-ignore */} + + + + + {_formatValidationMessage('template', 'Required. Collector configuration, see quick reference for more information.')} + + +
+ + + + + + +
+ + +
+ + + + +
); }; diff --git a/graylog2-web-interface/src/pages/SidecarEditConfigurationPage.jsx b/graylog2-web-interface/src/pages/SidecarEditConfigurationPage.jsx deleted file mode 100644 index 4b160bc27ed8..000000000000 --- a/graylog2-web-interface/src/pages/SidecarEditConfigurationPage.jsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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 - * . - */ -import PropTypes from 'prop-types'; -import React from 'react'; -import createReactClass from 'create-react-class'; - -import { LinkContainer } from 'components/common/router'; -import { ButtonToolbar, Col, Row, Button } from 'components/bootstrap'; -import { DocumentTitle, PageHeader, Spinner } from 'components/common'; -import Routes from 'routing/Routes'; -import ConfigurationForm from 'components/sidecars/configuration-forms/ConfigurationForm'; -import ConfigurationHelper from 'components/sidecars/configuration-forms/ConfigurationHelper'; -import history from 'util/History'; -import withParams from 'routing/withParams'; -import { CollectorConfigurationsActions } from 'stores/sidecars/CollectorConfigurationsStore'; - -const SidecarEditConfigurationPage = createReactClass({ - displayName: 'SidecarEditConfigurationPage', - - propTypes: { - params: PropTypes.object.isRequired, - }, - - getInitialState() { - return { - configuration: undefined, - }; - }, - - componentDidMount() { - this._reloadConfiguration(); - }, - - _reloadConfiguration() { - const { configurationId } = this.props.params; - - CollectorConfigurationsActions.getConfiguration(configurationId).then( - (configuration) => { - this.setState({ configuration: configuration }); - - CollectorConfigurationsActions.getConfigurationSidecars(configurationId) - .then((configurationSidecars) => this.setState({ configurationSidecars: configurationSidecars })); - }, - (error) => { - if (error.status === 404) { - history.push(Routes.SYSTEM.SIDECARS.CONFIGURATION); - } - }, - ); - }, - - _isLoading() { - return !this.state.configuration || !this.state.configurationSidecars; - }, - - _variableRenameHandler(oldname, newname) { - this.configurationForm.replaceConfigurationVariableName(oldname, newname); - }, - - render() { - if (this._isLoading()) { - return ; - } - - return ( - - - - - Some words about collector configurations. - - - - Read more about the Graylog Sidecar in the documentation. - - - - - - - - - - - - - - - - - - { this.configurationForm = c; }} - configuration={this.state.configuration} - configurationSidecars={this.state.configurationSidecars} /> - - - - - - - - ); - }, -}); - -export default withParams(SidecarEditConfigurationPage); diff --git a/graylog2-web-interface/src/pages/SidecarEditConfigurationPage.tsx b/graylog2-web-interface/src/pages/SidecarEditConfigurationPage.tsx new file mode 100644 index 000000000000..1a4c502d2c00 --- /dev/null +++ b/graylog2-web-interface/src/pages/SidecarEditConfigurationPage.tsx @@ -0,0 +1,99 @@ +/* + * 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 + * . + */ +import PropTypes from 'prop-types'; +import React, { useState, useEffect } from 'react'; + +import history from 'util/History'; +import { LinkContainer } from 'components/common/router'; +import { ButtonToolbar, Button } from 'components/bootstrap'; +import { DocumentTitle, PageHeader, Spinner } from 'components/common'; +import Routes from 'routing/Routes'; +import ConfigurationForm from 'components/sidecars/configuration-forms/ConfigurationForm'; +import withParams from 'routing/withParams'; +import { CollectorConfigurationsActions } from 'stores/sidecars/CollectorConfigurationsStore'; +import type { Configuration, ConfigurationSidecarsResponse } from 'components/sidecars/types'; + +const SidecarEditConfigurationPage = ({ params }) => { + const [configuration, setConfiguration] = useState(null); + const [configurationSidecars, setConfigurationSidecars] = useState(null); + + useEffect(() => { + const _reloadConfiguration = () => { + const { configurationId } = params; + + CollectorConfigurationsActions.getConfiguration(configurationId).then( + (_configuration) => { + setConfiguration(_configuration); + + CollectorConfigurationsActions.getConfigurationSidecars(configurationId) + .then((_configurationSidecars) => setConfigurationSidecars(_configurationSidecars)); + }, + (error) => { + if (error.status === 404) { + history.push(Routes.SYSTEM.SIDECARS.CONFIGURATION); + } + }, + ); + }; + + _reloadConfiguration(); + }, [params]); + + const _isLoading = () => { + return !configuration || !configurationSidecars; + }; + + if (_isLoading()) { + return ; + } + + return ( + + + + + Some words about collector configurations. + + + + Read more about the Graylog Sidecar in the documentation. + + + + + + + + + + + + + + + + + + ); +}; + +SidecarEditConfigurationPage.propTypes = { + params: PropTypes.object.isRequired, +}; + +export default withParams(SidecarEditConfigurationPage); diff --git a/graylog2-web-interface/src/pages/SidecarNewConfigurationPage.jsx b/graylog2-web-interface/src/pages/SidecarNewConfigurationPage.jsx deleted file mode 100644 index a193e946d97a..000000000000 --- a/graylog2-web-interface/src/pages/SidecarNewConfigurationPage.jsx +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 - * . - */ -import React from 'react'; -import createReactClass from 'create-react-class'; - -import { LinkContainer } from 'components/common/router'; -import { ButtonToolbar, Col, Row, Button } from 'components/bootstrap'; -import { DocumentTitle, PageHeader } from 'components/common'; -import Routes from 'routing/Routes'; -import ConfigurationForm from 'components/sidecars/configuration-forms/ConfigurationForm'; -import ConfigurationHelper from 'components/sidecars/configuration-forms/ConfigurationHelper'; - -const SidecarNewConfigurationPage = createReactClass({ - displayName: 'SidecarNewConfigurationPage', - - _variableRenameHandler(oldname, newname) { - this.configurationForm.replaceConfigurationVariableName(oldname, newname); - }, - - render() { - return ( - - - - - Some words about collector configurations. - - - - Read more about the Graylog Sidecar in the documentation. - - - - - - - - - - - - - - - - - - { this.configurationForm = c; }} - action="create" /> - - - - - - - - ); - }, -}); - -export default SidecarNewConfigurationPage; diff --git a/graylog2-web-interface/src/pages/SidecarNewConfigurationPage.tsx b/graylog2-web-interface/src/pages/SidecarNewConfigurationPage.tsx new file mode 100644 index 000000000000..a3b97a72ada3 --- /dev/null +++ b/graylog2-web-interface/src/pages/SidecarNewConfigurationPage.tsx @@ -0,0 +1,56 @@ +/* + * 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 + * . + */ +import React from 'react'; + +import { LinkContainer } from 'components/common/router'; +import { ButtonToolbar, Button } from 'components/bootstrap'; +import { DocumentTitle, PageHeader } from 'components/common'; +import Routes from 'routing/Routes'; +import ConfigurationForm from 'components/sidecars/configuration-forms/ConfigurationForm'; + +const SidecarNewConfigurationPage = () => { + return ( + + + + + Some words about collector configurations. + + + + Read more about the Graylog Sidecar in the documentation. + + + + + + + + + + + + + + + + + + ); +}; + +export default SidecarNewConfigurationPage; From 14404f9b95c2272dabdd8fac0622e25f67ff765b Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Fri, 14 Oct 2022 16:23:26 +0200 Subject: [PATCH 75/82] added type SidecarCollectorPairType --- .../CollectorConfigurationModalContainer.tsx | 8 ++++---- .../administration/CollectorProcessControl.tsx | 6 +++--- .../administration/CollectorsAdministration.tsx | 14 +++++++------- .../CollectorsAdministrationActions.tsx | 8 ++++---- .../src/components/sidecars/types.d.ts | 5 +++++ 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx index 8d5044b06d0d..bbe87567537b 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModalContainer.tsx @@ -24,7 +24,7 @@ import { BootstrapModalConfirm } from 'components/bootstrap'; import CollectorConfigurationModal from './CollectorConfigurationModal'; -import type { Collector, Configuration, SidecarSummary } from '../types'; +import type { Collector, Configuration, SidecarCollectorPairType, SidecarSummary } from '../types'; const ConfigurationSummary = styled.div` word-break: break-all; @@ -33,8 +33,8 @@ const ConfigurationSummary = styled.div` type Props = { collectors: Collector[], configurations: Configuration[], - selectedSidecarCollectorPairs: { collector: Collector, sidecar: SidecarSummary }[], - onConfigurationSelectionChange: (pairs: { collector: Collector, sidecar: SidecarSummary }[], configs: Configuration[], callback: () => void) => void, + selectedSidecarCollectorPairs: SidecarCollectorPairType[], + onConfigurationSelectionChange: (pairs: SidecarCollectorPairType[], configs: Configuration[], callback: () => void) => void, show: boolean, onCancel: () => void, }; @@ -60,7 +60,7 @@ const CollectorConfigurationModalContainer = ({ .map((config) => config.name); }; - const getAssignedConfigurations = (_selectedSidecarCollectorPairs: { collector: Collector, sidecar: SidecarSummary }[], selectedCollector: Collector) => { + const getAssignedConfigurations = (_selectedSidecarCollectorPairs: SidecarCollectorPairType[], selectedCollector: Collector) => { const assignments = _selectedSidecarCollectorPairs.map(({ sidecar }) => sidecar).reduce((accumulator, sidecar) => accumulator.concat(sidecar.assignments), []); const filteredAssignments = assignments.map((assignment) => configurations.find((configuration) => configuration.id === assignment.configuration_id)) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.tsx index 67187d84e4b7..49364eb5ba27 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorProcessControl.tsx @@ -21,13 +21,13 @@ import lodash from 'lodash'; import { Button, Panel, BootstrapModalConfirm } from 'components/bootstrap'; import { Pluralize, SelectPopover } from 'components/common'; -import type { Collector, SidecarSummary } from '../types'; +import type { SidecarCollectorPairType } from '../types'; const PROCESS_ACTIONS = ['start', 'restart', 'stop']; type Props = { - selectedSidecarCollectorPairs: { collector: Collector, sidecar: SidecarSummary }[], - onProcessAction: (action: string, pairs: { collector: Collector, sidecar: SidecarSummary }[], callback: () => void) => void, + selectedSidecarCollectorPairs: SidecarCollectorPairType[], + onProcessAction: (action: string, pairs: SidecarCollectorPairType[], callback: () => void) => void, }; const CollectorProcessControl = ({ selectedSidecarCollectorPairs, onProcessAction }: Props) => { diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx index c105f64f0248..dbf6df389920 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx @@ -37,7 +37,7 @@ import CollectorConfigurationModalContainer from './CollectorConfigurationModalC import FiltersSummary from './FiltersSummary'; import style from './CollectorsAdministration.css'; -import type { Collector, Configuration, SidecarSummary } from '../types'; +import type { Collector, Configuration, SidecarCollectorPairType, SidecarSummary } from '../types'; const HeaderComponentsWrapper = styled.div(({ theme }) => css` float: right; @@ -57,14 +57,14 @@ export const PAGE_SIZES = [10, 25, 50, 100]; type Props = { collectors: Collector[], configurations: Configuration[], - sidecarCollectorPairs: { collector: Collector, sidecar: SidecarSummary }[], + sidecarCollectorPairs: SidecarCollectorPairType[], query: string, filters: { [_key: string]: string }, pagination: Pagination, onPageChange: (currentPage: number, pageSize: number) => void, onFilter: (collectorIds?: string[], callback?: () => void) => void, onQueryChange: (query?: string, callback?: () => void) => void, - onConfigurationChange: (pairs: { collector: Collector, sidecar: SidecarSummary }[], configs: Configuration[], callback: () => void) => void, + onConfigurationChange: (pairs: SidecarCollectorPairType[], configs: Configuration[], callback: () => void) => void, onProcessAction: (action: string, collectorDict: { [sidecarId: string]: string[] }, callback: () => void) => void, }; @@ -92,7 +92,7 @@ const CollectorsAdministration = ({ return `${sidecar.node_id}-${collector.name}`; }; - const isAllSelected = (_collectors: ({ collector: Collector, sidecar: SidecarSummary }|Collector)[], _selected: string[]) => { + const isAllSelected = (_collectors: (SidecarCollectorPairType|Collector)[], _selected: string[]) => { return _collectors.length > 0 && _collectors.length === _selected.length; }; @@ -105,11 +105,11 @@ const CollectorsAdministration = ({ } }, [selectAllInput, collectors, selected]); - const handleConfigurationChange = (selectedSidecars: { collector: Collector, sidecar: SidecarSummary }[], selectedConfigurations: Configuration[], doneCallback: () => void) => { + const handleConfigurationChange = (selectedSidecars: SidecarCollectorPairType[], selectedConfigurations: Configuration[], doneCallback: () => void) => { onConfigurationChange(selectedSidecars, selectedConfigurations, doneCallback); }; - const handleProcessAction = (action: string, selectedSidecarCollectorPairs: { collector: Collector, sidecar: SidecarSummary }[], doneCallback: () => void) => { + const handleProcessAction = (action: string, selectedSidecarCollectorPairs: SidecarCollectorPairType[], doneCallback: () => void) => { const selectedCollectors = {}; selectedSidecarCollectorPairs.forEach(({ sidecar, collector }) => { @@ -131,7 +131,7 @@ const CollectorsAdministration = ({ setSelected(newSelection); }; - const formatHeader = (selectedSidecarCollectorPairs: { collector: Collector, sidecar: SidecarSummary }[]) => { + const formatHeader = (selectedSidecarCollectorPairs: SidecarCollectorPairType[]) => { const selectedItems = selected.length; let headerMenu; diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.tsx index 7c4c4c204f6b..c7d247195020 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministrationActions.tsx @@ -25,7 +25,7 @@ import { Icon } from 'components/common'; import CollectorConfigurationModalContainer from './CollectorConfigurationModalContainer'; import CollectorProcessControl from './CollectorProcessControl'; -import type { Collector, Configuration, SidecarSummary } from '../types'; +import type { Collector, Configuration, SidecarCollectorPairType } from '../types'; const ConfigurationButton = styled(Button)` margin-right: 6px @@ -34,9 +34,9 @@ const ConfigurationButton = styled(Button)` type Props = { collectors: Collector[], configurations: Configuration[], - selectedSidecarCollectorPairs: { collector: Collector, sidecar: SidecarSummary }[], - onConfigurationSelectionChange: (pairs: { collector: Collector, sidecar: SidecarSummary }[], configs: Configuration[], callback: () => void) => void, - onProcessAction: (action: string, pairs: { collector: Collector, sidecar: SidecarSummary }[], callback: () => void) => void, + selectedSidecarCollectorPairs: SidecarCollectorPairType[], + onConfigurationSelectionChange: (pairs: SidecarCollectorPairType[], configs: Configuration[], callback: () => void) => void, + onProcessAction: (action: string, pairs: SidecarCollectorPairType[], callback: () => void) => void, }; const CollectorsAdministrationActions = ({ diff --git a/graylog2-web-interface/src/components/sidecars/types.d.ts b/graylog2-web-interface/src/components/sidecars/types.d.ts index 363af7858f7d..a26bb9745aa7 100644 --- a/graylog2-web-interface/src/components/sidecars/types.d.ts +++ b/graylog2-web-interface/src/components/sidecars/types.d.ts @@ -92,3 +92,8 @@ export type ConfigurationSidecarsResponse = { sidecar_ids: string[]; configuration_id: string; } + +export type SidecarCollectorPairType = { + collector: Collector; + sidecar: SidecarSummary; +} From 95561d3ebb23f1266285d0c39968b259eeca3445 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Mon, 17 Oct 2022 10:43:18 +0200 Subject: [PATCH 76/82] make sure the filterQuery handles case error --- .../administration/CollectorConfigurationModal.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx index abafe36b4b3d..97ccf7d71450 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorConfigurationModal.tsx @@ -104,6 +104,14 @@ const ModalSubTitle = styled.div` text-overflow: ellipsis; `; +const getFilterQuery = (_query: string) => { + try { + return new RegExp(_query, 'i'); + } catch (error) { + return ' '; + } +}; + type Props = { show: boolean, onCancel: () => void, @@ -144,9 +152,7 @@ const CollectorConfigurationModal = ({ const isNotDirty = lodash.isEqual(selectedConfigurations, initialAssignedConfigs) && lodash.isEqual(partiallySelectedConfigurations, initialPartiallyAssignedConfigs); - const searchQueryRegexPattern = new RegExp(searchQuery, 'i'); - - const filteredOptions = [...initialAssignedConfigs, ...initialPartiallyAssignedConfigs, ...unassignedConfigs].filter((configuration) => configuration.match(searchQueryRegexPattern)); + const filteredOptions = [...initialAssignedConfigs, ...initialPartiallyAssignedConfigs, ...unassignedConfigs].filter((configuration) => configuration.match(getFilterQuery(searchQuery))); const rows = filteredOptions.map((configName) => { const { configuration, collector, sidecars, autoAssignedTags } = getRowData(configName); From d62c23327b8b1d59b1729a4ed815516641885a4e Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Mon, 17 Oct 2022 11:41:24 +0200 Subject: [PATCH 77/82] made tags field required --- .../configuration-forms/ConfigurationTagsSelect.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.tsx b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.tsx index c4fddb68f8bf..0f3081f9ec4e 100644 --- a/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.tsx +++ b/graylog2-web-interface/src/components/sidecars/configuration-forms/ConfigurationTagsSelect.tsx @@ -45,13 +45,9 @@ const ConfigurationTagsSelect = ({ }; ConfigurationTagsSelect.propTypes = { - tags: PropTypes.arrayOf(PropTypes.string), + tags: PropTypes.arrayOf(PropTypes.string).isRequired, availableTags: PropTypes.array.isRequired, onChange: PropTypes.func.isRequired, }; -ConfigurationTagsSelect.defaultProps = { - tags: [], -}; - export default ConfigurationTagsSelect; From 9ac33a37d67fa6ded9beb45b4d933d6539e5b15f Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Mon, 17 Oct 2022 15:17:39 +0200 Subject: [PATCH 78/82] ColorLabel using className instead of inline style --- .../administration/CollectorsAdministration.css | 4 ++++ .../administration/CollectorsAdministration.tsx | 2 +- .../src/components/sidecars/common/ColorLabel.tsx | 12 ++++++------ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.css b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.css index a3bafcab70de..6edc3ed7381a 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.css +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.css @@ -33,3 +33,7 @@ :local(.paginatedList) .search { margin-bottom: 15px; } + +.flex-color-label { + display: flex; +} \ No newline at end of file diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx index dbf6df389920..47b23ef898a1 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx @@ -257,7 +257,7 @@ const CollectorsAdministration = ({ to={Routes.SYSTEM.SIDECARS.EDIT_CONFIGURATION(configuration.id)}> + className="flex-color-label" /> ), )} diff --git a/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx b/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx index 147181929425..d33d15dbfe0a 100644 --- a/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx +++ b/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx @@ -33,8 +33,8 @@ interface ColorLabelProps { color: string, size?: Size, text?: string | React.ReactNode, - style?: React.CSSProperties, - theme: DefaultTheme + theme: DefaultTheme, + className?: string, } const ColorLabelWrap = styled.span(({ size, theme }: ColorLabelWrapProps) => { @@ -47,12 +47,12 @@ const ColorLabelWrap = styled.span(({ size, theme }: ColorLabelWrapProps) => { `; }); -const ColorLabel = ({ color, size, text, theme, style }: ColorLabelProps) => { +const ColorLabel = ({ color, size, text, theme, className }: ColorLabelProps) => { const borderColor = theme.utils.colorLevel(color, 5); const textColor = theme.utils.contrastingColor(color); return ( - +
, size: 'normal', - style: undefined, + className: '', }; export default withTheme(ColorLabel); From 0e8a8a039019cd00a2257b8d8cf9a21a9b9352ca Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Tue, 18 Oct 2022 11:48:30 +0200 Subject: [PATCH 79/82] converted inline-styling to styled-component in SearchForm --- .../src/components/common/SearchForm.jsx | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/graylog2-web-interface/src/components/common/SearchForm.jsx b/graylog2-web-interface/src/components/common/SearchForm.jsx index a9c96731fb14..c48033ec8759 100644 --- a/graylog2-web-interface/src/components/common/SearchForm.jsx +++ b/graylog2-web-interface/src/components/common/SearchForm.jsx @@ -44,6 +44,18 @@ const HelpFeedback = styled.span` } `; +const StyledContainer = styled.div(({ topMargin }) => ` + margin-top: ${topMargin}; +`); + +const StyledInput = styled.input(({ queryWidth }) => ` + width: ${queryWidth}; +`); + +const StyledInputContainer = styled.div(({ queryWidth }) => ` + width: ${queryWidth}; +`); + /** * Component that renders a customizable search form. The component * supports a loading state, adding children next to the form, and @@ -238,30 +250,30 @@ class SearchForm extends React.Component { const { query, isLoading } = this.state; return ( -
+
-
+ {label && ( )} - + {queryHelpComponent && ( {queryHelpComponent} )} -
+ {onSearch && (
+ ); } } From 319857305e3ea0a27340917923ecd4b7dcdbd4f9 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Tue, 18 Oct 2022 14:28:10 +0200 Subject: [PATCH 80/82] converted CollectorsAdministration.css into styled-components --- .../CollectorsAdministration.css | 39 ---------- .../CollectorsAdministration.tsx | 71 ++++++++++++++----- 2 files changed, 55 insertions(+), 55 deletions(-) delete mode 100644 graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.css diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.css b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.css deleted file mode 100644 index 6edc3ed7381a..000000000000 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.css +++ /dev/null @@ -1,39 +0,0 @@ -:local(.collectorEntry) .row { - margin-bottom: 5px; -} - -:local(.collectorEntry) .form-group { - display: inline-block; - margin: 0 10px 0 0; -} - -:local(.collectorEntry) .checkbox { - margin-top: 5px; - margin-bottom: 5px; -} - -:local(.collectorEntry) .checkbox label { - font-size: 1rem; /* theme.fonts.size.body */ -} - -:local(.alignedInformation) { - margin-left: 20px; -} - -:local(.additionalContent) { - display: flex; - margin-top: 5px; - flex-wrap: wrap; -} - -:local(.paginatedList) .page-size { - padding-top: 4px; -} - -:local(.paginatedList) .search { - margin-bottom: 15px; -} - -.flex-color-label { - display: flex; -} \ No newline at end of file diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx index 47b23ef898a1..30f47ed3f98e 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx @@ -35,7 +35,6 @@ import CollectorsAdministrationActions from './CollectorsAdministrationActions'; import CollectorsAdministrationFilters from './CollectorsAdministrationFilters'; import CollectorConfigurationModalContainer from './CollectorConfigurationModalContainer'; import FiltersSummary from './FiltersSummary'; -import style from './CollectorsAdministration.css'; import type { Collector, Configuration, SidecarCollectorPairType, SidecarSummary } from '../types'; @@ -48,10 +47,51 @@ const HeaderComponentsWrapper = styled.div(({ theme }) => css` } `); -const DisabledCollector = styled.div(({ theme }) => css` +const CollectorEntry = styled.div` + .row { + margin-bottom: 5px; + } + .form-group { + display: inline-block; + margin: 0 10px 0 0; + } + .checkbox { + margin-top: 5px; + margin-bottom: 5px; + } + .checkbox label { + font-size: 1rem; /* theme.fonts.size.body */ + } +`; + +const DisabledCollector = styled(CollectorEntry)(({ theme }) => css` color: ${theme.colors.variant.light.default}; + margin-left: 20px; `); +const AdditionalContent = styled.span` + display: flex; + margin-top: 5px; + flex-wrap: wrap; +`; + +const AlignedInformation = styled.span` + margin-left: 20px; +`; + +const PaginatedListContainer = styled.div` + .page-size { + padding-top: 4px; + } + .search { + margin-bottom: 15px; + } +`; + +const StyledColorLabel = styled(ColorLabel)` + display: flex; +`; + export const PAGE_SIZES = [10, 25, 50, 100]; type Props = { @@ -182,7 +222,7 @@ const CollectorsAdministration = ({ const formatSidecarNoCollectors = (sidecar: SidecarSummary) => { return ( - +

@@ -233,17 +273,17 @@ const CollectorsAdministration = ({ onChange={handleSidecarCollectorSelect(collectorId)} /> - + {(configAssignments.length > 0) && ( )} - + - + {(configAssignments.length > 0) && ( ( - + ), )} - + ); @@ -274,17 +313,17 @@ const CollectorsAdministration = ({ return ( -
+ -

+ {sidecar.node_name} {sidecar.node_id} {!sidecar.active && — inactive} -

+
{_collectors.map((collector) => formatCollector(sidecar, collector, _configurations))} -
+
); }; @@ -327,7 +366,7 @@ const CollectorsAdministration = ({ } return ( -
+ @@ -354,7 +393,7 @@ const CollectorsAdministration = ({ setSelected([]); setShowConfigurationModal(false); }} /> -
+ ); }; From 5abd33b7137f5cda3226da25e39e5c1c75369538 Mon Sep 17 00:00:00 2001 From: Mohamed Ould Hocine Date: Tue, 18 Oct 2022 15:30:29 +0200 Subject: [PATCH 81/82] addedstyled component for ColorLabel --- .../administration/CollectorsAdministration.tsx | 12 ++++++++---- .../src/components/sidecars/common/ColorLabel.tsx | 7 ++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx index 30f47ed3f98e..dd11b23efe3a 100644 --- a/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx +++ b/graylog2-web-interface/src/components/sidecars/administration/CollectorsAdministration.tsx @@ -88,8 +88,10 @@ const PaginatedListContainer = styled.div` } `; -const StyledColorLabel = styled(ColorLabel)` - display: flex; +const StyledColorLabelContainer = styled.span` + .color-label-wrapper { + display: flex; + } `; export const PAGE_SIZES = [10, 25, 50, 100]; @@ -295,8 +297,10 @@ const CollectorsAdministration = ({ {configAssignments.map((configuration) => ( - + + + ), )} diff --git a/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx b/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx index d33d15dbfe0a..de8bb63acda4 100644 --- a/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx +++ b/graylog2-web-interface/src/components/sidecars/common/ColorLabel.tsx @@ -34,7 +34,6 @@ interface ColorLabelProps { size?: Size, text?: string | React.ReactNode, theme: DefaultTheme, - className?: string, } const ColorLabelWrap = styled.span(({ size, theme }: ColorLabelWrapProps) => { @@ -47,12 +46,12 @@ const ColorLabelWrap = styled.span(({ size, theme }: ColorLabelWrapProps) => { `; }); -const ColorLabel = ({ color, size, text, theme, className }: ColorLabelProps) => { +const ColorLabel = ({ color, size, text, theme }: ColorLabelProps) => { const borderColor = theme.utils.colorLevel(color, 5); const textColor = theme.utils.contrastingColor(color); return ( - +