Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically generate datanode.conf.example #19141

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 28 additions & 0 deletions data-node/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,34 @@
<artifactId>exec-maven-plugin</artifactId>
<groupId>org.codehaus.mojo</groupId>
<executions>
<execution>
<id>generate-csv-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>org.graylog.datanode.docs.ConfigurationDocsGenerator</mainClass>
<arguments>
<argument>csv</argument>
<argument>${project.build.directory}/datanode-conf-docs.csv</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>generate-conf-example</id>
<phase>prepare-package</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>org.graylog.datanode.docs.ConfigurationDocsGenerator</mainClass>
<arguments>
<argument>conf</argument>
<argument>${project.build.directory}/datanode.conf.example</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>install-required-opensearch-plugins-x64</id>
<phase>prepare-package</phase>
Expand Down
2 changes: 1 addition & 1 deletion data-node/src/main/assembly/datanode.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
<outputDirectory>.</outputDirectory>
</file>
<file>
<source>${project.basedir}/../misc/datanode.conf</source>
<source>${project.build.directory}/datanode.conf.example</source>
<destName>datanode.conf.example</destName>
<outputDirectory>.</outputDirectory>
</file>
Expand Down
90 changes: 62 additions & 28 deletions data-node/src/main/java/org/graylog/datanode/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,22 @@ public class Configuration {
@Parameter(value = "opensearch_location")
private String opensearchDistributionRoot = "dist";

@Documentation("Data directory of the embedded opensearch. Contains indices of the opensearch. May be pointed to an existing" +
"opensearch directory during in-place migration to Datanode")
@Documentation("""
Data directory of the embedded opensearch. Contains indices of the opensearch.
May be pointed to an existing opensearch directory during in-place migration to Datanode
""")
@Parameter(value = "opensearch_data_location", required = true)
private Path opensearchDataLocation = Path.of("datanode/data");

@Documentation("Logs directory of the embedded opensearch")
@Parameter(value = "opensearch_logs_location", required = true, validators = DirectoryWritableValidator.class)
private Path opensearchLogsLocation = Path.of("datanode/logs");

@Documentation("Configuration directory of the embedded opensearch. This is the directory where the opensearch" +
"process will store its configuration files. Caution, each start of the Datanode will regenerate the complete content of the directory!")
@Documentation("""
Configuration directory of the embedded opensearch. This is the directory where the opensearch
process will store its configuration files. Caution, each start of the Datanode will regenerate
the complete content of the directory!
""")
@Parameter(value = "opensearch_config_location", required = true, validators = DirectoryWritableValidator.class)
private Path opensearchConfigLocation = Path.of("datanode/config");

Expand All @@ -113,7 +118,10 @@ public class Configuration {
private Integer opensearchProcessLogsBufferSize = 500;


@Documentation("Unique name of this Datanode instance. use this, if your node name should be different from the hostname that's found by programmatically looking it up")
@Documentation("""
Unique name of this Datanode instance. use this, if your node name should be different from the hostname
that's found by programmatically looking it up.
""")
@Parameter(value = "node_name")
private String datanodeNodeName;

Expand All @@ -122,7 +130,10 @@ public class Configuration {
@Parameter(value = "initial_cluster_manager_nodes")
private String initialClusterManagerNodes;

@Documentation("Opensearch heap memory. Initial and maxmium heap must be identical for OpenSearch, otherwise the boot fails. So it's only one config option")
@Documentation("""
Opensearch heap memory. Initial and maxmium heap must be identical for OpenSearch, otherwise the boot fails.
So it's only one config option.
""")
@Parameter(value = "opensearch_heap")
private String opensearchHeap = "1g";

Expand All @@ -138,7 +149,10 @@ public class Configuration {
@Parameter(value = "opensearch_discovery_seed_hosts", converter = StringListConverter.class)
private List<String> opensearchDiscoverySeedHosts = Collections.emptyList();

@Documentation("Binds an OpenSearch node to an address. Use 0.0.0.0 to include all available network interfaces, or specify an IP address assigned to a specific interface. ")
@Documentation("""
Binds an OpenSearch node to an address. Use 0.0.0.0 to include all available network interfaces,
or specify an IP address assigned to a specific interface.
""")
@Parameter(value = "opensearch_network_host")
private String opensearchNetworkHost = null;

Expand All @@ -158,31 +172,44 @@ public class Configuration {
@Parameter(value = HTTP_CERTIFICATE_PASSWORD_PROPERTY)
private String datanodeHttpCertificatePassword;

@Documentation("You MUST specify a hash password for the root user (which you only need to initially set up the " +
"system and in case you lose connectivity to your authentication backend)." +
"This password cannot be changed using the API or via the web interface. If you need to change it, " +
"modify it in this file. " +
"Create one by using for example: echo -n yourpassword | shasum -a 256")
@Parameter(value = "root_password_sha2")
@Documentation("""
You MUST specify a hash password for the root user (which you only need to initially set up the
system and in case you lose connectivity to your authentication backend).
This password cannot be changed using the API or via the web interface. If you need to change it,
modify it in this file.
Create one by using for example: echo -n yourpassword | shasum -a 256
""")
@Parameter(value = "root_password_sha2", required = true)
private String rootPasswordSha2;

@Documentation("You MUST set a secret to secure/pepper the stored user passwords here. Use at least 64 characters." +
"Generate one by using for example: pwgen -N 1 -s 96 \n" +
"ATTENTION: This value must be the same on all Graylog and Datanode nodes in the cluster. " +
"Changing this value after installation will render all user sessions and encrypted values in the database invalid. (e.g. encrypted access tokens)")
@Documentation("""
You MUST set a secret to secure/pepper the stored user passwords here. Use at least 64 characters.
Generate one by using for example: pwgen -N 1 -s 96
ATTENTION: This value must be the same on all Graylog and Datanode nodes in the cluster.
Changing this value after installation will render all user sessions and encrypted values
in the database invalid. (e.g. encrypted access tokens)
""")
@Parameter(value = "password_secret", required = true, validators = StringNotBlankValidator.class)
private String passwordSecret;

@Documentation("communication between Graylog and OpenSearch is secured by JWT. This configuration defines interval between token regenerations.")
@Documentation("""
communication between Graylog and OpenSearch is secured by JWT.
This configuration defines interval between token regenerations.
""")
@Parameter(value = "indexer_jwt_auth_token_caching_duration")
Duration indexerJwtAuthTokenCachingDuration = Duration.seconds(60);

@Documentation("communication between Graylog and OpenSearch is secured by JWT. This configuration defines validity interval of JWT tokens.")
@Documentation("""
communication between Graylog and OpenSearch is secured by JWT.
This configuration defines validity interval of JWT tokens.
""")
@Parameter(value = "indexer_jwt_auth_token_expiration_duration")
Duration indexerJwtAuthTokenExpirationDuration = Duration.seconds(180);

@Documentation("The auto-generated node ID will be stored in this file and read after restarts. It is a good idea " +
"to use an absolute file path here if you are starting Graylog DataNode from init scripts or similar.")
@Documentation("""
The auto-generated node ID will be stored in this file and read after restarts. It is a good idea
to use an absolute file path here if you are starting Graylog DataNode from init scripts or similar.
""")
@Parameter(value = "node_id_file", validators = NodeIdFileValidator.class)
private String nodeIdFile = "data/node-id";

Expand All @@ -207,15 +234,19 @@ public class Configuration {
@Parameter(value = "clustername")
private String clustername = "datanode-cluster";

@Documentation("This configuration should be used if you want to connect to this Graylog DataNode's REST API and it is available on " +
"another network interface than $http_bind_address, " +
"for example if the machine has multiple network interfaces or is behind a NAT gateway.")
@Parameter(value = "http_publish_uri", validators = URIAbsoluteValidator.class)
@Documentation("""
This configuration should be used if you want to connect to this Graylog DataNode's REST API
and it is available on another network interface than $http_bind_address,
for example if the machine has multiple network interfaces or is behind a NAT gateway.
""")
@Parameter(value = "http_publish_uri", validators = URIAbsoluteValidator.class)
private URI httpPublishUri;


@Documentation("Enable GZIP support for HTTP interface. This compresses API responses and therefore helps to reduce " +
" overall round trip times.")
@Documentation("""
Enable GZIP support for HTTP interface. This compresses API responses and therefore helps to reduce
overall round trip times.
""")
@Parameter(value = "http_enable_gzip")
private boolean httpEnableGzip = true;

Expand Down Expand Up @@ -298,7 +329,10 @@ public Integer getIndicesQueryBoolMaxClauseCount() {
return indicesQueryBoolMaxClauseCount;
}

@Documentation("Configures verbosity of embedded opensearch logs. Possible values OFF, FATAL, ERROR, WARN, INFO, DEBUG, and TRACE, default is INFO")
@Documentation("""
Configures verbosity of embedded opensearch logs.
Possible values OFF, FATAL, ERROR, WARN, INFO, DEBUG, and TRACE, default is INFO
""")
@Parameter(value = "opensearch_logger_org_opensearch")
private String opensearchDebug;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* 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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog.datanode.docs;

import com.github.joschi.jadconfig.Parameter;
import com.github.joschi.jadconfig.ReflectionUtils;
import jakarta.annotation.Nonnull;
import org.apache.commons.lang3.ClassUtils;
import org.graylog.datanode.commands.Server;
import org.graylog.datanode.docs.printers.ConfigFileDocsPrinter;
import org.graylog.datanode.docs.printers.CsvDocsPrinter;
import org.graylog.datanode.docs.printers.DocsPrinter;
import org.graylog2.configuration.Documentation;

import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;

public class ConfigurationDocsGenerator {

/**
* This class is linked from the datanode pom.xml and generates conf.example and csv documentation.
*/
public static void main(String[] args) throws IOException {
final ConfigurationDocsGenerator generator = new ConfigurationDocsGenerator();
generator.generateDocumentation(parseDocumentationFormat(args), ConfigurationDocsGenerator::getDatanodeConfigurationBeans);
}

@Nonnull
private static DocumentationFormat parseDocumentationFormat(String[] args) {
if (args.length != 2) {
throw new IllegalArgumentException("This command needs two arguments - a format and file path. For example" + "csv ${project.build.directory}/configuration-documentation.csv");
}
final String format = args[0].toLowerCase(Locale.ROOT).trim();
final String file = args[1];
return new DocumentationFormat(format, file);
}

protected void generateDocumentation(DocumentationFormat format, Supplier<List<Object>> configurationBeans) throws IOException {
final List<ConfigurationEntry> configuration = detectConfigurationFields(configurationBeans);
try (final DocsPrinter writer = createWriter(format)) {
writer.writeHeader();

for (ConfigurationEntry f : configuration) {
writer.writeField(f);
}
}
}

private DocsPrinter createWriter(DocumentationFormat format) throws IOException {
final FileWriter fileWriter = new FileWriter(format.outputFile(), StandardCharsets.UTF_8);
return switch (format.format()) {
case "csv" -> new CsvDocsPrinter(fileWriter);
case "conf" -> new ConfigFileDocsPrinter(fileWriter);
default -> throw new IllegalArgumentException("Unsupported format " + format.format());
};
}

/**
* Collects all configuration options from all available configuration beans.
*/
private List<ConfigurationEntry> detectConfigurationFields(Supplier<List<Object>> configurationBeans) {
return configurationBeans.get()
.stream()
.flatMap(ConfigurationDocsGenerator::beanToConfigEntries)
.sorted(Comparator.comparing(ConfigurationEntry::isPriority).reversed())
.toList();
}

@Nonnull
private static Stream<ConfigurationEntry> beanToConfigEntries(Object configurationBean) {
return Arrays.stream(configurationBean.getClass().getDeclaredFields())
.filter(f -> f.isAnnotationPresent(Parameter.class))
.filter(ConfigurationDocsGenerator::isPublicFacing)
.map(f -> toConfigurationEntry(f, configurationBean));
}

private static List<Object> getDatanodeConfigurationBeans() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should already move that out of the generator to be able to move the generator to a server package more easily?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any simple idea how/where to get this?

return new Server().getCommandConfigurationBeans();
}

/**
* There are some configuration options not intended for general usage, mainly just for system packages configuration.
*
* @see Documentation#visible()
*/
private static boolean isPublicFacing(Field f) {
return !f.isAnnotationPresent(Documentation.class) || f.getAnnotation(Documentation.class).visible();
}

private static ConfigurationEntry toConfigurationEntry(Field f, Object instance) {
final String documentation = Optional.ofNullable(f.getAnnotation(Documentation.class)).map(Documentation::value).orElse(null);
final Parameter parameter = f.getAnnotation(Parameter.class);
final String propertyName = parameter.value();
final Object defaultValue = getDefaultValue(f, instance);
final String type = getType(f);
final boolean required = parameter.required();
return new ConfigurationEntry(instance.getClass(), f.getName(), type, propertyName, defaultValue, required, documentation);
}

private static Object getDefaultValue(Field f, Object instance) {
try {
return ReflectionUtils.getFieldValue(instance, f);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}

private static String getType(Field f) {
if (f.getType().isPrimitive()) { // unify primitive types and wrappers, e.g. int -> Integer
return ClassUtils.primitiveToWrapper(f.getType()).getSimpleName();
} else {
return f.getType().getSimpleName();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog.datanode.docs;

import jakarta.annotation.Nullable;

/**
* @param configurationBean Class that defines this configuration entry
* @param fieldName java field name
* @param type Java type name, e.g. String or Integer.
* @param configName configuration property name, as written in the config file
* @param defaultValue default value declared in the java field, null if not defined
* @param required if the configuration property is mandatory (needs default or entry in the config file)
* @param documentation textual documentation of this configuration propery
*/
public record ConfigurationEntry(
Class<?> configurationBean,
String fieldName,
String type,
String configName,
@Nullable Object defaultValue,
boolean required,
String documentation
) {

public boolean isPriority() {
return required && defaultValue == null;
}
}