Add operator-owned setup public addresses

This commit is contained in:
Logic
2026-08-09 08:54:31 +08:00
parent 7d0b84a20c
commit 2702c55e73
24 changed files with 579 additions and 221 deletions
@@ -193,7 +193,7 @@ public final class SetupApiContract {
public enum ValidationSection implements WireValue {
METADATA_DATABASE("metadata_database"),
TELEMETRY_STORE("telemetry_store"),
SERVER_INSTRUMENTATION("server_instrumentation"),
PUBLIC_ACCESS("public_access"),
MAIL("mail");
private final String value;
@@ -244,7 +244,7 @@ public final class SetupApiContract {
METADATA_SCHEMA_MISMATCH("metadata_schema_mismatch"),
METADATA_INSUFFICIENT_PRIVILEGES("metadata_insufficient_privileges"),
TELEMETRY_CONNECTION_FAILED("telemetry_connection_failed"),
SERVER_INSTRUMENTATION_INVALID("server_instrumentation_invalid"),
PUBLIC_ADDRESS_INVALID("public_address_invalid"),
MAIL_CONNECTION_FAILED("mail_connection_failed"),
ADMINISTRATOR_ALREADY_CONFIGURED("administrator_already_configured"),
ADMINISTRATOR_USERNAME_INVALID("administrator_username_invalid"),
@@ -274,7 +274,7 @@ public final class SetupApiContract {
public enum SetupWarningCode implements WireValue {
EXTERNAL_APPLY_REQUIRED("external_apply_required"),
RESTART_REQUIRED("restart_required"),
SERVER_OTLP_PLAINTEXT("server_otlp_plaintext"),
PUBLIC_ADDRESS_PLAINTEXT("public_address_plaintext"),
MAIL_SECURITY_NONE("mail_security_none"),
H2_NON_PRODUCTION("h2_non_production");
@@ -338,6 +338,7 @@ public final class SetupApiContract {
/** Secret-free optional configuration status. */
public record OptionalConfigurationSummary(
boolean publicBaseUrlConfigured,
boolean serverOtlpHttpConfigured,
boolean serverOtlpGrpcConfigured,
boolean retentionConfigured,
@@ -407,10 +408,18 @@ public final class SetupApiContract {
}
}
/** Server OTLP endpoint input; HTTP and HTTPS are both contractually valid. */
public record ServerInstrumentationConfiguration(
/** Operator-owned public addresses; values are never inferred from the setup request. */
public record PublicAccessConfiguration(
String publicBaseUrl,
String serverOtlpHttpEndpoint,
String serverOtlpGrpcEndpoint) {
@Override
public String toString() {
return "PublicAccessConfiguration[publicBaseUrlProvided=" + hasText(publicBaseUrl)
+ ", serverOtlpHttpEndpointProvided=" + hasText(serverOtlpHttpEndpoint)
+ ", serverOtlpGrpcEndpointProvided=" + hasText(serverOtlpGrpcEndpoint) + "]";
}
}
/** Mail input. */
@@ -440,16 +449,16 @@ public final class SetupApiContract {
@NotNull ValidationSection section,
@Valid MetadataDatabaseConfiguration managementDatabase,
@Valid TelemetryStoreConfiguration telemetryStore,
@Valid ServerInstrumentationConfiguration serverInstrumentation,
@Valid PublicAccessConfiguration publicAccess,
@Valid MailConfiguration mail) {
public ValidateRequest {
Objects.requireNonNull(section, "section");
int supplied = countPresent(managementDatabase, telemetryStore, serverInstrumentation, mail);
int supplied = countPresent(managementDatabase, telemetryStore, publicAccess, mail);
boolean matches = switch (section) {
case METADATA_DATABASE -> managementDatabase != null;
case TELEMETRY_STORE -> telemetryStore != null;
case SERVER_INSTRUMENTATION -> serverInstrumentation != null;
case PUBLIC_ACCESS -> publicAccess != null;
case MAIL -> mail != null;
};
if (supplied != 1 || !matches) {
@@ -527,13 +536,14 @@ public final class SetupApiContract {
/** Optional setup input. */
public record OptionsRequest(
@Valid ServerInstrumentationConfiguration serverInstrumentation,
@Valid PublicAccessConfiguration publicAccess,
@Valid RetentionConfiguration retention,
@Valid MailConfiguration mail) {
}
/** Secret-free optional setup result. */
public record OptionsResponse(
boolean publicBaseUrlConfigured,
boolean serverOtlpHttpConfigured,
boolean serverOtlpGrpcConfigured,
boolean retentionConfigured,
@@ -13,6 +13,7 @@ import static org.apache.hertzbeat.manager.setup.config.ManagedConfigurationKeys
import static org.apache.hertzbeat.manager.setup.config.ManagedConfigurationKeys.MAIL_HOST;
import static org.apache.hertzbeat.manager.setup.config.ManagedConfigurationKeys.MAIL_SSL_ENABLED;
import static org.apache.hertzbeat.manager.setup.config.ManagedConfigurationKeys.MAIL_STARTTLS_ENABLED;
import static org.apache.hertzbeat.manager.setup.config.ManagedConfigurationKeys.PUBLIC_BASE_URL;
import static org.apache.hertzbeat.manager.setup.config.ManagedConfigurationKeys.SERVER_OTLP_GRPC;
import static org.apache.hertzbeat.manager.setup.config.ManagedConfigurationKeys.SERVER_OTLP_HTTP;
@@ -27,8 +28,8 @@ import org.apache.hertzbeat.manager.setup.api.SetupApiContract.TelemetryStoreSum
import org.apache.hertzbeat.manager.setup.config.EffectiveConfigurationResolver;
import org.apache.hertzbeat.manager.setup.config.ManagedActiveConfigurationInspector.Inspection;
import org.apache.hertzbeat.manager.setup.config.ManagedActiveConfigurationInspector.State;
import org.apache.hertzbeat.manager.setup.config.ManagedOptionalConfiguration.ServerInstrumentationSettings;
import org.apache.hertzbeat.manager.setup.config.RestartRequirement;
import org.apache.hertzbeat.manager.setup.config.SetupPublicAddress;
import org.apache.hertzbeat.manager.setup.workflow.SetupConfigurationProjection;
import org.apache.hertzbeat.manager.setup.workflow.SetupWarningPolicy;
import org.springframework.core.env.Environment;
@@ -49,8 +50,11 @@ final class SetupStatusProjectionFactory {
MetadataDatabaseKind kind = MetadataDatabaseKind.valueOf(database.value().toUpperCase(Locale.ROOT));
boolean mailConfigured = externallyConfigured(environment, MAIL_HOST, true);
OptionalConfigurationSummary optional = new OptionalConfigurationSummary(
externallyConfiguredEndpoint(environment, SERVER_OTLP_HTTP),
externallyConfiguredEndpoint(environment, SERVER_OTLP_GRPC),
externallyConfiguredAddress(environment, PUBLIC_BASE_URL, SetupPublicAddress.Kind.PUBLIC_BASE_URL),
externallyConfiguredAddress(
environment, SERVER_OTLP_HTTP, SetupPublicAddress.Kind.SERVER_OTLP_ENDPOINT),
externallyConfiguredAddress(
environment, SERVER_OTLP_GRPC, SetupPublicAddress.Kind.SERVER_OTLP_ENDPOINT),
externallyConfigured(environment, GREPTIME_EXPIRE_TIME, true), mailConfigured);
MailSecurity mailSecurity = mailConfigured ? mailSecurity(environment) : null;
return new SetupConfigurationProjection(
@@ -59,7 +63,7 @@ final class SetupStatusProjectionFactory {
new TelemetryStoreSummary(TelemetryStoreKind.GREPTIME,
managedPresent || telemetrySource != ConfigSource.BUILT_IN_DEFAULT,
telemetrySource, false), optional, SetupWarningPolicy.INSTANCE.evaluate(
kind, environment.getProperty(SERVER_OTLP_HTTP),
kind, environment.getProperty(PUBLIC_BASE_URL), environment.getProperty(SERVER_OTLP_HTTP),
environment.getProperty(SERVER_OTLP_GRPC), mailSecurity));
}
@@ -72,13 +76,15 @@ final class SetupStatusProjectionFactory {
&& (!requireText || !resolved.value().isBlank());
}
private boolean externallyConfiguredEndpoint(Environment environment, String key) {
private boolean externallyConfiguredAddress(Environment environment, String key, SetupPublicAddress.Kind kind) {
if (!environment.containsProperty(key)) {
return false;
}
var resolved = resolver.resolve(environment, key, RestartRequirement.LIVE_RELOAD);
return resolved.source() != ConfigSource.BUILT_IN_DEFAULT
&& ServerInstrumentationSettings.normalize(resolved.value()).isPresent();
boolean valid = kind == SetupPublicAddress.Kind.PUBLIC_BASE_URL
? SetupPublicAddress.tryPublicBaseUrl(resolved.value()).isPresent()
: SetupPublicAddress.tryServerOtlpEndpoint(resolved.value()).isPresent();
return resolved.source() != ConfigSource.BUILT_IN_DEFAULT && valid;
}
private static MailSecurity mailSecurity(Environment environment) {
@@ -30,6 +30,7 @@ import static org.apache.hertzbeat.manager.setup.config.ManagedConfigurationKeys
import static org.apache.hertzbeat.manager.setup.config.ManagedConfigurationKeys.MAIL_FROM_ADDRESS;
import static org.apache.hertzbeat.manager.setup.config.ManagedConfigurationKeys.MAIL_SSL_ENABLED;
import static org.apache.hertzbeat.manager.setup.config.ManagedConfigurationKeys.MAIL_STARTTLS_ENABLED;
import static org.apache.hertzbeat.manager.setup.config.ManagedConfigurationKeys.PUBLIC_BASE_URL;
import static org.apache.hertzbeat.manager.setup.config.ManagedConfigurationKeys.SERVER_OTLP_GRPC;
import static org.apache.hertzbeat.manager.setup.config.ManagedConfigurationKeys.SERVER_OTLP_HTTP;
import static org.apache.hertzbeat.manager.setup.config.ManagedConfigurationKeys.SERVER_AUTHENTICATION;
@@ -62,7 +63,7 @@ final class ApplicationConfigDocumentCodec implements ManagedDocumentCodec<Manag
DATASOURCE_URL, DATASOURCE_USERNAME, DATABASE_KIND,
DUCKDB_ENABLED, GREPTIME_ENABLED, GREPTIME_GRPC, GREPTIME_HTTP,
GREPTIME_DATABASE, GREPTIME_USERNAME, GREPTIME_EXPIRE_TIME, SERVER_OTLP_HTTP,
SERVER_OTLP_GRPC, SERVER_PROFILE_ID, SERVER_AUTHENTICATION, MAIL_HOST, MAIL_PORT,
SERVER_OTLP_GRPC, SERVER_PROFILE_ID, SERVER_AUTHENTICATION, PUBLIC_BASE_URL, MAIL_HOST, MAIL_PORT,
MAIL_SSL_ENABLED, MAIL_STARTTLS_ENABLED, MAIL_USERNAME, MAIL_FROM_ADDRESS);
@Override
@@ -92,11 +93,15 @@ final class ApplicationConfigDocumentCodec implements ManagedDocumentCodec<Manag
values.put(GREPTIME_HTTP, value.telemetryStore().endpoints().http());
values.put(GREPTIME_DATABASE, value.telemetryStore().database());
value.telemetryStore().username().ifPresent(username -> values.put(GREPTIME_USERNAME, username));
value.optional().serverInstrumentation().ifPresent(instrumentation -> {
values.put(SERVER_PROFILE_ID, MANAGED_SERVER_PROFILE_ID);
values.put(SERVER_AUTHENTICATION, MANAGED_SERVER_AUTHENTICATION);
instrumentation.serverOtlpHttpEndpoint().ifPresent(item -> values.put(SERVER_OTLP_HTTP, item));
instrumentation.serverOtlpGrpcEndpoint().ifPresent(item -> values.put(SERVER_OTLP_GRPC, item));
value.optional().publicAccess().ifPresent(publicAccess -> {
publicAccess.publicBaseUrl().ifPresent(item -> values.put(PUBLIC_BASE_URL, item));
if (publicAccess.serverOtlpHttpEndpoint().isPresent()
|| publicAccess.serverOtlpGrpcEndpoint().isPresent()) {
values.put(SERVER_PROFILE_ID, MANAGED_SERVER_PROFILE_ID);
values.put(SERVER_AUTHENTICATION, MANAGED_SERVER_AUTHENTICATION);
}
publicAccess.serverOtlpHttpEndpoint().ifPresent(item -> values.put(SERVER_OTLP_HTTP, item));
publicAccess.serverOtlpGrpcEndpoint().ifPresent(item -> values.put(SERVER_OTLP_GRPC, item));
});
value.optional().retention().ifPresent(retention ->
values.put(GREPTIME_EXPIRE_TIME, retention.days() + "d"));
@@ -129,7 +134,7 @@ final class ApplicationConfigDocumentCodec implements ManagedDocumentCodec<Manag
if (!values.keySet().stream().allMatch(String.class::isInstance)
|| !values.keySet().containsAll(REQUIRED_KEYS)
|| !ALLOWED_KEYS.containsAll(values.keySet())
|| !completeServerInstrumentationGroup(values)
|| !completeServerOtlpGroup(values)
|| !completeMailGroup(values)
|| !usesSupportedTelemetryStorage(values)) {
throw DocumentException.corrupt();
@@ -153,10 +158,11 @@ final class ApplicationConfigDocumentCodec implements ManagedDocumentCodec<Manag
}
private static ManagedOptionalConfiguration optional(Map<?, ?> values) {
boolean instrumentationPresent = containsAny(values, SERVER_OTLP_HTTP, SERVER_OTLP_GRPC);
Optional<ManagedOptionalConfiguration.ServerInstrumentationSettings> instrumentation =
instrumentationPresent ? Optional.of(new ManagedOptionalConfiguration.ServerInstrumentationSettings(
optionalText(values, SERVER_OTLP_HTTP), optionalText(values, SERVER_OTLP_GRPC)))
boolean publicAccessPresent = containsAny(values, PUBLIC_BASE_URL, SERVER_OTLP_HTTP, SERVER_OTLP_GRPC);
Optional<ManagedOptionalConfiguration.PublicAccessSettings> publicAccess =
publicAccessPresent ? Optional.of(new ManagedOptionalConfiguration.PublicAccessSettings(
optionalText(values, PUBLIC_BASE_URL), optionalText(values, SERVER_OTLP_HTTP),
optionalText(values, SERVER_OTLP_GRPC)))
: Optional.empty();
Optional<ManagedOptionalConfiguration.RetentionSettings> retention = values.containsKey(GREPTIME_EXPIRE_TIME)
? Optional.of(new ManagedOptionalConfiguration.RetentionSettings(retentionDays(values)))
@@ -166,7 +172,7 @@ final class ApplicationConfigDocumentCodec implements ManagedDocumentCodec<Manag
text(values, MAIL_HOST), Integer.parseInt(text(values, MAIL_PORT)),
mailSecurity(values), optionalText(values, MAIL_USERNAME),
text(values, MAIL_FROM_ADDRESS))) : Optional.empty();
return new ManagedOptionalConfiguration(instrumentation, retention, mail);
return new ManagedOptionalConfiguration(publicAccess, retention, mail);
}
private static boolean completeMailGroup(Map<?, ?> values) {
@@ -176,7 +182,7 @@ final class ApplicationConfigDocumentCodec implements ManagedDocumentCodec<Manag
MAIL_STARTTLS_ENABLED, MAIL_FROM_ADDRESS));
}
private static boolean completeServerInstrumentationGroup(Map<?, ?> values) {
private static boolean completeServerOtlpGroup(Map<?, ?> values) {
boolean endpointKeyPresent = containsAny(values, SERVER_OTLP_HTTP, SERVER_OTLP_GRPC);
boolean endpointPresent = meaningfulEndpoint(values, SERVER_OTLP_HTTP)
|| meaningfulEndpoint(values, SERVER_OTLP_GRPC);
@@ -193,7 +199,7 @@ final class ApplicationConfigDocumentCodec implements ManagedDocumentCodec<Manag
private static boolean meaningfulEndpoint(Map<?, ?> values, String key) {
return values.get(key) instanceof String endpoint
&& ManagedOptionalConfiguration.ServerInstrumentationSettings.normalize(endpoint).isPresent();
&& SetupPublicAddress.tryServerOtlpEndpoint(endpoint).isPresent();
}
private static boolean containsAny(Map<?, ?> values, String... keys) {
@@ -29,6 +29,7 @@ public final class ManagedConfigurationKeys {
public static final String GREPTIME_DATABASE = "warehouse.store.greptime.database";
public static final String GREPTIME_USERNAME = "warehouse.store.greptime.username";
public static final String GREPTIME_PASSWORD = "warehouse.store.greptime.password";
public static final String PUBLIC_BASE_URL = "hertzbeat.setup.public-base-url";
public static final String SERVER_OTLP_HTTP = "hertzbeat.instrumentation.server.otlp-http-endpoint";
public static final String SERVER_OTLP_GRPC = "hertzbeat.instrumentation.server.otlp-grpc-endpoint";
public static final String SERVER_PROFILE_ID = "hertzbeat.instrumentation.server.profile-id";
@@ -23,12 +23,12 @@ import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MailSecurity;
/** Typed optional setup overlay kept in the existing managed application document. */
public record ManagedOptionalConfiguration(
Optional<ServerInstrumentationSettings> serverInstrumentation,
Optional<PublicAccessSettings> publicAccess,
Optional<RetentionSettings> retention,
Optional<MailSettings> mail) {
public ManagedOptionalConfiguration {
Objects.requireNonNull(serverInstrumentation, "serverInstrumentation");
Objects.requireNonNull(publicAccess, "publicAccess");
Objects.requireNonNull(retention, "retention");
Objects.requireNonNull(mail, "mail");
}
@@ -37,33 +37,36 @@ public record ManagedOptionalConfiguration(
return new ManagedOptionalConfiguration(Optional.empty(), Optional.empty(), Optional.empty());
}
/** Optional server OTLP intake endpoints. */
public record ServerInstrumentationSettings(
/** Explicit operator-owned public access addresses. */
public record PublicAccessSettings(
Optional<String> publicBaseUrl,
Optional<String> serverOtlpHttpEndpoint,
Optional<String> serverOtlpGrpcEndpoint) {
public ServerInstrumentationSettings {
public PublicAccessSettings {
Objects.requireNonNull(publicBaseUrl, "publicBaseUrl");
Objects.requireNonNull(serverOtlpHttpEndpoint, "serverOtlpHttpEndpoint");
Objects.requireNonNull(serverOtlpGrpcEndpoint, "serverOtlpGrpcEndpoint");
serverOtlpHttpEndpoint = normalizeConfigured(serverOtlpHttpEndpoint);
serverOtlpGrpcEndpoint = normalizeConfigured(serverOtlpGrpcEndpoint);
if (serverOtlpHttpEndpoint.isEmpty() && serverOtlpGrpcEndpoint.isEmpty()) {
throw new IllegalArgumentException("At least one server instrumentation endpoint is required");
publicBaseUrl = validateConfigured(publicBaseUrl, SetupPublicAddress.Kind.PUBLIC_BASE_URL);
serverOtlpHttpEndpoint = validateConfigured(
serverOtlpHttpEndpoint, SetupPublicAddress.Kind.SERVER_OTLP_ENDPOINT);
serverOtlpGrpcEndpoint = validateConfigured(
serverOtlpGrpcEndpoint, SetupPublicAddress.Kind.SERVER_OTLP_ENDPOINT);
if (publicBaseUrl.isEmpty() && serverOtlpHttpEndpoint.isEmpty() && serverOtlpGrpcEndpoint.isEmpty()) {
throw new IllegalArgumentException("At least one public access address is required");
}
}
public static Optional<String> normalize(String value) {
if (value == null) {
private static Optional<String> validateConfigured(Optional<String> endpoint, SetupPublicAddress.Kind kind) {
if (endpoint.isEmpty()) {
return Optional.empty();
}
String normalized = value.trim();
return normalized.isEmpty() ? Optional.empty() : Optional.of(normalized);
}
private static Optional<String> normalizeConfigured(Optional<String> endpoint) {
if (endpoint.isPresent() && normalize(endpoint.orElseThrow()).isEmpty()) {
throw new IllegalArgumentException("Server instrumentation endpoint must not be blank");
String value = endpoint.orElseThrow();
Optional<SetupPublicAddress> address = kind == SetupPublicAddress.Kind.PUBLIC_BASE_URL
? SetupPublicAddress.publicBaseUrl(value) : SetupPublicAddress.serverOtlpEndpoint(value);
if (address.isEmpty()) {
throw new IllegalArgumentException("Public access address must not be blank");
}
return endpoint.flatMap(ServerInstrumentationSettings::normalize);
return address.map(SetupPublicAddress::value);
}
}
@@ -0,0 +1,159 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.config;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.URI;
import java.util.Locale;
import java.util.Optional;
import org.apache.hertzbeat.observability.instrumentation.v2.api.InstrumentationIntakeProfileV2.IntakeEndpoint;
/** A validated operator-advertised address; validation is purely syntactic and never resolves DNS. */
public record SetupPublicAddress(String value, Kind kind) {
/** Address contracts differ between the browser-facing base URL and OTLP intake endpoints. */
public enum Kind {
PUBLIC_BASE_URL,
SERVER_OTLP_ENDPOINT
}
public SetupPublicAddress {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Public address must not be blank");
}
value = value.trim();
URI uri = URI.create(value);
if (kind == null || uri.getHost() == null || uri.getHost().indexOf('%') >= 0
|| wildcardHost(uri.getHost()) || invalidPort(uri.getPort())) {
throw new IllegalArgumentException("Public address is invalid");
}
if (kind == Kind.PUBLIC_BASE_URL) {
if (!("http".equalsIgnoreCase(uri.getScheme()) || "https".equalsIgnoreCase(uri.getScheme()))
|| uri.getUserInfo() != null || uri.getRawQuery() != null || uri.getRawFragment() != null) {
throw new IllegalArgumentException("Public base URL is invalid");
}
} else {
IntakeEndpoint.fromUrl(value);
}
}
public static Optional<SetupPublicAddress> publicBaseUrl(String value) {
return parse(value, Kind.PUBLIC_BASE_URL);
}
public static Optional<SetupPublicAddress> serverOtlpEndpoint(String value) {
return parse(value, Kind.SERVER_OTLP_ENDPOINT);
}
public static Optional<SetupPublicAddress> tryPublicBaseUrl(String value) {
return tryParse(value, Kind.PUBLIC_BASE_URL);
}
public static Optional<SetupPublicAddress> tryServerOtlpEndpoint(String value) {
return tryParse(value, Kind.SERVER_OTLP_ENDPOINT);
}
public boolean plaintextPublic() {
URI uri = URI.create(value);
return "http".equalsIgnoreCase(uri.getScheme()) && !internalHost(uri.getHost());
}
private static Optional<SetupPublicAddress> parse(String value, Kind kind) {
if (value == null || value.isBlank()) {
return Optional.empty();
}
return Optional.of(new SetupPublicAddress(value, kind));
}
private static Optional<SetupPublicAddress> tryParse(String value, Kind kind) {
try {
return parse(value, kind);
} catch (IllegalArgumentException failure) {
return Optional.empty();
}
}
private static boolean invalidPort(int port) {
return port == 0 || port > 65_535;
}
private static boolean wildcardHost(String value) {
String host = withoutIpv6Brackets(value.toLowerCase(Locale.ROOT));
InetAddress address = literalAddress(host);
return address != null && address.isAnyLocalAddress();
}
private static boolean internalHost(String value) {
String host = withoutIpv6Brackets(value.toLowerCase(Locale.ROOT));
InetAddress address = literalAddress(host);
if (address instanceof Inet4Address) {
return privateIpv4(address.getHostAddress());
}
if (address != null) {
return address.isLoopbackAddress() || address.isLinkLocalAddress()
|| privateIpv6(address.getHostAddress());
}
return host.equals("localhost") || host.endsWith(".localhost") || host.endsWith(".local")
|| host.endsWith(".internal") || (!host.contains(".") && !host.contains(":"))
|| privateIpv4(host);
}
private static String withoutIpv6Brackets(String host) {
return host.length() > 1 && host.charAt(0) == '[' && host.charAt(host.length() - 1) == ']'
? host.substring(1, host.length() - 1) : host;
}
private static boolean privateIpv4(String host) {
String[] parts = host.split("\\.", -1);
if (parts.length != 4) {
return false;
}
try {
int first = octet(parts[0]);
int second = octet(parts[1]);
octet(parts[2]);
octet(parts[3]);
return first == 10 || first == 127 || first == 0 || (first == 169 && second == 254)
|| (first == 172 && second >= 16 && second <= 31) || (first == 192 && second == 168);
} catch (IllegalArgumentException failure) {
return false;
}
}
private static InetAddress literalAddress(String host) {
try {
return InetAddress.ofLiteral(host);
} catch (IllegalArgumentException failure) {
return null;
}
}
private static int octet(String value) {
int parsed = Integer.parseInt(value);
if (parsed < 0 || parsed > 255) {
throw new IllegalArgumentException("Invalid IPv4 octet");
}
return parsed;
}
private static boolean privateIpv6(String host) {
return host.equals("::1") || host.equals("0:0:0:0:0:0:0:1") || host.startsWith("fc")
|| host.startsWith("fd") || host.matches("fe[89ab].*");
}
}
@@ -14,7 +14,7 @@ import org.apache.hertzbeat.manager.setup.api.SetupApiContract.OptionsRequest;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.OptionsResponse;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupPhase;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupWarningCode;
import org.apache.hertzbeat.manager.setup.config.ManagedOptionalConfiguration.ServerInstrumentationSettings;
import org.apache.hertzbeat.manager.setup.config.SetupPublicAddress;
/** Projects persisted optional settings into the secret-free runtime and response shape. */
record OptionalConfigurationProjection(
@@ -22,19 +22,22 @@ record OptionalConfigurationProjection(
static OptionalConfigurationProjection from(MetadataDatabaseKind databaseKind, OptionsRequest request) {
OptionalConfigurationSummary summary = new OptionalConfigurationSummary(
request.serverInstrumentation() != null
&& ServerInstrumentationSettings.normalize(
request.serverInstrumentation().serverOtlpHttpEndpoint()).isPresent(),
request.serverInstrumentation() != null
&& ServerInstrumentationSettings.normalize(
request.serverInstrumentation().serverOtlpGrpcEndpoint()).isPresent(),
request.publicAccess() != null
&& SetupPublicAddress.tryPublicBaseUrl(request.publicAccess().publicBaseUrl()).isPresent(),
request.publicAccess() != null
&& SetupPublicAddress.tryServerOtlpEndpoint(
request.publicAccess().serverOtlpHttpEndpoint()).isPresent(),
request.publicAccess() != null
&& SetupPublicAddress.tryServerOtlpEndpoint(
request.publicAccess().serverOtlpGrpcEndpoint()).isPresent(),
request.retention() != null, request.mail() != null);
return new OptionalConfigurationProjection(
summary, SetupWarningPolicy.INSTANCE.evaluate(databaseKind, request));
}
OptionsResponse response() {
return new OptionsResponse(summary.serverOtlpHttpConfigured(), summary.serverOtlpGrpcConfigured(),
summary.retentionConfigured(), summary.mailConfigured(), SetupPhase.OPTIONAL_CONFIGURATION);
return new OptionsResponse(summary.publicBaseUrlConfigured(), summary.serverOtlpHttpConfigured(),
summary.serverOtlpGrpcConfigured(), summary.retentionConfigured(), summary.mailConfigured(),
SetupPhase.OPTIONAL_CONFIGURATION);
}
}
@@ -0,0 +1,46 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import java.util.List;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.PublicAccessConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupErrorCode;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupWarningCode;
import org.apache.hertzbeat.manager.setup.config.SetupPublicAddress;
import org.apache.hertzbeat.manager.setup.workflow.MetadataConfigurationValidator.Validation;
/** Validates explicit public addresses without consulting request origin, host headers, or network state. */
final class PublicAccessConfigurationValidator {
Validation validate(PublicAccessConfiguration configuration) {
try {
var publicBaseUrl = SetupPublicAddress.publicBaseUrl(configuration.publicBaseUrl());
var http = SetupPublicAddress.serverOtlpEndpoint(configuration.serverOtlpHttpEndpoint());
var grpc = SetupPublicAddress.serverOtlpEndpoint(configuration.serverOtlpGrpcEndpoint());
if (publicBaseUrl.isEmpty() && http.isEmpty() && grpc.isEmpty()) {
return Validation.failed(SetupErrorCode.PUBLIC_ADDRESS_INVALID);
}
List<SetupWarningCode> warnings = publicBaseUrl.filter(SetupPublicAddress::plaintextPublic).isPresent()
|| http.filter(SetupPublicAddress::plaintextPublic).isPresent()
|| grpc.filter(SetupPublicAddress::plaintextPublic).isPresent()
? List.of(SetupWarningCode.PUBLIC_ADDRESS_PLAINTEXT) : List.of();
return new Validation(true, null, warnings);
} catch (IllegalArgumentException failure) {
return Validation.failed(SetupErrorCode.PUBLIC_ADDRESS_INVALID);
}
}
}
@@ -1,59 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import java.util.List;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.ServerInstrumentationConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupErrorCode;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupWarningCode;
import org.apache.hertzbeat.manager.setup.config.ManagedOptionalConfiguration.ServerInstrumentationSettings;
import org.apache.hertzbeat.manager.setup.workflow.MetadataConfigurationValidator.Validation;
import org.apache.hertzbeat.observability.instrumentation.v2.api.InstrumentationIntakeProfileV2.IntakeEndpoint;
/** Validates optional server OTLP intake endpoints. */
final class ServerInstrumentationConfigurationValidator {
Validation validate(ServerInstrumentationConfiguration configuration) {
String http = ServerInstrumentationSettings.normalize(
configuration.serverOtlpHttpEndpoint()).orElse(null);
String grpc = ServerInstrumentationSettings.normalize(
configuration.serverOtlpGrpcEndpoint()).orElse(null);
if ((http == null && grpc == null) || !validEndpoint(http) || !validEndpoint(grpc)) {
return Validation.failed(SetupErrorCode.SERVER_INSTRUMENTATION_INVALID);
}
List<SetupWarningCode> warnings = plaintext(http) || plaintext(grpc)
? List.of(SetupWarningCode.SERVER_OTLP_PLAINTEXT) : List.of();
return new Validation(true, null, warnings);
}
private static boolean validEndpoint(String value) {
if (value == null || value.isBlank()) {
return true;
}
try {
IntakeEndpoint.fromUrl(value);
return true;
} catch (IllegalArgumentException failure) {
return false;
}
}
private static boolean plaintext(String value) {
return value != null && value.regionMatches(true, 0, "http://", 0, 7);
}
}
@@ -33,7 +33,7 @@ public record SetupConfigurationProjection(
ConfigSource.BUILT_IN_DEFAULT, false),
new TelemetryStoreSummary(TelemetryStoreKind.GREPTIME, false,
ConfigSource.BUILT_IN_DEFAULT, false),
new OptionalConfigurationSummary(false, false, false, false),
new OptionalConfigurationSummary(false, false, false, false, false),
List.of(SetupWarningCode.H2_NON_PRODUCTION));
}
}
@@ -25,6 +25,7 @@ import org.apache.hertzbeat.manager.setup.api.SetupApiException;
import org.apache.hertzbeat.manager.setup.config.ManagedConfigurationTransaction;
import org.apache.hertzbeat.manager.setup.config.ManagedOptionalConfiguration;
import org.apache.hertzbeat.manager.setup.config.SecretValue;
import org.apache.hertzbeat.manager.setup.config.SetupPublicAddress;
import org.springframework.http.HttpStatus;
/** Maps and atomically persists optional setup settings through the existing two-file transaction. */
@@ -37,14 +38,16 @@ public final class SetupOptionsCoordinator {
public void persist(OptionsRequest request) {
ManagedOptionalConfiguration options = new ManagedOptionalConfiguration(
Optional.ofNullable(request.serverInstrumentation()).flatMap(value -> {
Optional<String> httpEndpoint = ManagedOptionalConfiguration.ServerInstrumentationSettings
.normalize(value.serverOtlpHttpEndpoint());
Optional<String> grpcEndpoint = ManagedOptionalConfiguration.ServerInstrumentationSettings
.normalize(value.serverOtlpGrpcEndpoint());
return httpEndpoint.isEmpty() && grpcEndpoint.isEmpty() ? Optional.empty()
: Optional.of(new ManagedOptionalConfiguration.ServerInstrumentationSettings(
httpEndpoint, grpcEndpoint));
Optional.ofNullable(request.publicAccess()).flatMap(value -> {
Optional<String> publicBaseUrl = SetupPublicAddress.publicBaseUrl(value.publicBaseUrl())
.map(SetupPublicAddress::value);
Optional<String> httpEndpoint = SetupPublicAddress
.serverOtlpEndpoint(value.serverOtlpHttpEndpoint()).map(SetupPublicAddress::value);
Optional<String> grpcEndpoint = SetupPublicAddress
.serverOtlpEndpoint(value.serverOtlpGrpcEndpoint()).map(SetupPublicAddress::value);
return publicBaseUrl.isEmpty() && httpEndpoint.isEmpty() && grpcEndpoint.isEmpty()
? Optional.empty() : Optional.of(new ManagedOptionalConfiguration.PublicAccessSettings(
publicBaseUrl, httpEndpoint, grpcEndpoint));
}),
Optional.ofNullable(request.retention()).map(value ->
new ManagedOptionalConfiguration.RetentionSettings(value.days())),
@@ -32,8 +32,7 @@ public final class SetupRequestValidator {
private final Clock clock;
private final MetadataConfigurationValidator metadata = new MetadataConfigurationValidator();
private final TelemetryConfigurationValidator telemetry = new TelemetryConfigurationValidator();
private final ServerInstrumentationConfigurationValidator serverInstrumentation =
new ServerInstrumentationConfigurationValidator();
private final PublicAccessConfigurationValidator publicAccess = new PublicAccessConfigurationValidator();
private final MailConfigurationValidator mail = new MailConfigurationValidator();
private final MetadataConnectionProbe metadataConnection;
private final TelemetryConnectionProbe telemetryConnection;
@@ -58,7 +57,7 @@ public final class SetupRequestValidator {
Validation structural = switch (request.section()) {
case METADATA_DATABASE -> metadata.validate(request.managementDatabase());
case TELEMETRY_STORE -> telemetry.validate(request.telemetryStore());
case SERVER_INSTRUMENTATION -> serverInstrumentation.validate(request.serverInstrumentation());
case PUBLIC_ACCESS -> publicAccess.validate(request.publicAccess());
case MAIL -> mail.validate(request.mail());
};
Validation result = structural.valid() ? liveValidation(request, structural) : structural;
@@ -87,7 +86,7 @@ public final class SetupRequestValidator {
case METADATA_DATABASE -> metadataConnection.probe(request.managementDatabase());
case TELEMETRY_STORE -> telemetryConnection.probe(request.telemetryStore());
case MAIL -> mailConnection.probe(request.mail());
case SERVER_INSTRUMENTATION -> Optional.empty();
case PUBLIC_ACCESS -> Optional.empty();
};
return failure.map(Validation::failed).orElse(structural);
}
@@ -91,9 +91,9 @@ public final class SetupTransitionService {
public OptionsResponse configureOptions(OptionsRequest request) {
requireWritable();
state.ensurePhase(SetupPhase.OPTIONAL_CONFIGURATION);
if (request.serverInstrumentation() != null) {
requireValid(validator, new ValidateRequest(ValidationSection.SERVER_INSTRUMENTATION,
null, null, request.serverInstrumentation(), null));
if (request.publicAccess() != null) {
requireValid(validator, new ValidateRequest(ValidationSection.PUBLIC_ACCESS,
null, null, request.publicAccess(), null));
}
if (request.mail() != null) {
requireValid(validator, new ValidateRequest(ValidationSection.MAIL,
@@ -23,7 +23,7 @@ import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MailSecurity;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.OptionsRequest;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupWarningCode;
import org.apache.hertzbeat.manager.setup.config.ManagedOptionalConfiguration.ServerInstrumentationSettings;
import org.apache.hertzbeat.manager.setup.config.SetupPublicAddress;
/** Single warning policy shared by live setup and restart status projection. */
public final class SetupWarningPolicy {
@@ -33,33 +33,32 @@ public final class SetupWarningPolicy {
}
public List<SetupWarningCode> evaluate(MetadataDatabaseKind kind, OptionsRequest options) {
String otlpHttpEndpoint = options.serverInstrumentation() == null ? null
: options.serverInstrumentation().serverOtlpHttpEndpoint();
String otlpGrpcEndpoint = options.serverInstrumentation() == null ? null
: options.serverInstrumentation().serverOtlpGrpcEndpoint();
String publicBaseUrl = options.publicAccess() == null ? null : options.publicAccess().publicBaseUrl();
String otlpHttpEndpoint = options.publicAccess() == null ? null
: options.publicAccess().serverOtlpHttpEndpoint();
String otlpGrpcEndpoint = options.publicAccess() == null ? null
: options.publicAccess().serverOtlpGrpcEndpoint();
MailSecurity mailSecurity = options.mail() == null ? null : options.mail().security();
return evaluate(kind, otlpHttpEndpoint, otlpGrpcEndpoint, mailSecurity);
return evaluate(kind, publicBaseUrl, otlpHttpEndpoint, otlpGrpcEndpoint, mailSecurity);
}
public List<SetupWarningCode> evaluate(
MetadataDatabaseKind kind, String otlpHttpEndpoint, String otlpGrpcEndpoint,
MailSecurity mailSecurity) {
MetadataDatabaseKind kind, String publicBaseUrl, String otlpHttpEndpoint,
String otlpGrpcEndpoint, MailSecurity mailSecurity) {
List<SetupWarningCode> warnings = new ArrayList<>();
if (kind == MetadataDatabaseKind.H2) {
warnings.add(SetupWarningCode.H2_NON_PRODUCTION);
}
if (plaintext(otlpHttpEndpoint) || plaintext(otlpGrpcEndpoint)) {
warnings.add(SetupWarningCode.SERVER_OTLP_PLAINTEXT);
if (SetupPublicAddress.tryPublicBaseUrl(publicBaseUrl).filter(SetupPublicAddress::plaintextPublic).isPresent()
|| SetupPublicAddress.tryServerOtlpEndpoint(otlpHttpEndpoint)
.filter(SetupPublicAddress::plaintextPublic).isPresent()
|| SetupPublicAddress.tryServerOtlpEndpoint(otlpGrpcEndpoint)
.filter(SetupPublicAddress::plaintextPublic).isPresent()) {
warnings.add(SetupWarningCode.PUBLIC_ADDRESS_PLAINTEXT);
}
if (mailSecurity == MailSecurity.NONE) {
warnings.add(SetupWarningCode.MAIL_SECURITY_NONE);
}
return List.copyOf(warnings);
}
private static boolean plaintext(String endpoint) {
return ServerInstrumentationSettings.normalize(endpoint)
.filter(value -> value.regionMatches(true, 0, "http://", 0, 7))
.isPresent();
}
}
@@ -35,7 +35,7 @@ import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MailConfiguration
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MailSecurity;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.ServerInstrumentationConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.PublicAccessConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupAccess;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupErrorCode;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupOperationState;
@@ -77,11 +77,11 @@ class SetupApiContractTest {
assertWireValues(MetadataDatabaseKind.values(), "h2", "mysql", "postgresql");
assertWireValues(TelemetryStoreKind.values(), "greptime");
assertWireValues(ValidationSection.values(), "metadata_database", "telemetry_store",
"server_instrumentation", "mail");
"public_access", "mail");
assertWireValues(MailSecurity.values(), "none", "starttls", "tls");
assertWireValues(SetupApiContract.ExportFormat.values(), "yaml", "env", "kubernetes_secret");
assertWireValues(SetupApiContract.SetupWarningCode.values(), "external_apply_required", "restart_required",
"server_otlp_plaintext", "mail_security_none", "h2_non_production");
"public_address_plaintext", "mail_security_none", "h2_non_production");
}
@Test
@@ -93,12 +93,12 @@ class SetupApiContractTest {
"restartRequired");
assertComponents(SetupApiContract.TelemetryStoreSummary.class, "kind", "configured", "source",
"restartRequired");
assertComponents(SetupApiContract.OptionalConfigurationSummary.class, "serverOtlpHttpConfigured",
"serverOtlpGrpcConfigured", "retentionConfigured", "mailConfigured");
assertComponents(SetupApiContract.OptionalConfigurationSummary.class, "publicBaseUrlConfigured",
"serverOtlpHttpConfigured", "serverOtlpGrpcConfigured", "retentionConfigured", "mailConfigured");
assertComponents(SetupApiContract.UnlockRequest.class, "code");
assertComponents(SetupApiContract.UnlockResponse.class, "access", "expiresAt");
assertComponents(SetupApiContract.ValidateRequest.class, "section", "managementDatabase", "telemetryStore",
"serverInstrumentation", "mail");
"publicAccess", "mail");
assertComponents(SetupApiContract.TelemetryStoreConfiguration.class, "kind", "grpcEndpoints", "httpEndpoint",
"database", "username", "password");
assertComponents(SetupApiContract.ValidationResponse.class, "valid", "observedAt", "errorCode", "warnings");
@@ -110,10 +110,13 @@ class SetupApiContractTest {
"startedAt", "completedAt", "errorCode", "nextPollAfterMillis", "exportAvailable");
assertComponents(SetupApiContract.AdministratorRequest.class, "username", "password");
assertComponents(SetupApiContract.AdministratorResponse.class, "username", "phase");
assertComponents(SetupApiContract.OptionsRequest.class, "serverInstrumentation", "retention", "mail");
assertComponents(SetupApiContract.OptionsRequest.class, "publicAccess", "retention", "mail");
assertComponents(SetupApiContract.PublicAccessConfiguration.class, "publicBaseUrl",
"serverOtlpHttpEndpoint", "serverOtlpGrpcEndpoint");
assertComponents(SetupApiContract.RetentionConfiguration.class, "days");
assertComponents(SetupApiContract.OptionsResponse.class, "serverOtlpHttpConfigured",
"serverOtlpGrpcConfigured", "retentionConfigured", "mailConfigured", "phase");
assertComponents(SetupApiContract.OptionsResponse.class, "publicBaseUrlConfigured",
"serverOtlpHttpConfigured", "serverOtlpGrpcConfigured", "retentionConfigured", "mailConfigured",
"phase");
assertComponents(SetupApiContract.ExportRequest.class, "format", "configuration");
assertComponents(SetupApiContract.ExportResponse.class, "fileName", "mediaType");
assertComponents(SetupApiContract.CompleteRequest.class, "expectedPhase", "acknowledgedWarnings");
@@ -164,6 +167,20 @@ class SetupApiContractTest {
assertEquals(SECRET, decoded.code());
}
@Test
void publicAccessInputNeverRendersAddressBodies() {
PublicAccessConfiguration configuration = new PublicAccessConfiguration(
"https://user:" + SECRET + "@hertzbeat.example.test",
"https://collector.example.test:4318?token=" + SECRET,
"https://collector.example.test:4317");
assertEquals("PublicAccessConfiguration[publicBaseUrlProvided=true, "
+ "serverOtlpHttpEndpointProvided=true, serverOtlpGrpcEndpointProvided=true]",
configuration.toString());
assertFalse(configuration.toString().contains(SECRET));
assertFalse(configuration.toString().contains("example.test"));
}
@Test
void validateRequestRequiresExactlyOneMatchingSection() {
MetadataDatabaseConfiguration metadata = new MetadataDatabaseConfiguration(
@@ -174,7 +191,7 @@ class SetupApiContractTest {
() -> new ValidateRequest(ValidationSection.METADATA_DATABASE, null, null, null, null));
assertThrows(IllegalArgumentException.class, () -> new ValidateRequest(
ValidationSection.METADATA_DATABASE, metadata, null,
new ServerInstrumentationConfiguration("http://localhost:4318", null), null));
new PublicAccessConfiguration(null, "http://localhost:4318", null), null));
assertThrows(IllegalArgumentException.class, () -> new ValidateRequest(
ValidationSection.MAIL, metadata, null, null, null));
}
@@ -202,7 +219,7 @@ class SetupApiContractTest {
"config_read_only", "config_write_failed",
"config_recovery_required", "metadata_connection_failed", "metadata_kind_unsupported",
"metadata_schema_mismatch", "metadata_insufficient_privileges", "telemetry_connection_failed",
"server_instrumentation_invalid", "mail_connection_failed", "administrator_already_configured",
"public_address_invalid", "mail_connection_failed", "administrator_already_configured",
"administrator_username_invalid", "operation_not_found", "operation_conflict",
"migration_source_unsupported", "migration_target_not_empty", "migration_multi_node_unsupported",
"migration_copy_failed", "migration_verification_failed", "migration_activation_failed",
@@ -224,7 +241,7 @@ class SetupApiContractTest {
new SetupApiContract.TelemetryStoreSummary(
TelemetryStoreKind.GREPTIME, false, ConfigSource.BUILT_IN_DEFAULT, false),
false,
new SetupApiContract.OptionalConfigurationSummary(false, false, false, false));
new SetupApiContract.OptionalConfigurationSummary(false, false, false, false, false));
String json = objectMapper.writeValueAsString(response);
assertFalse(json.contains("jdbc"));
assertFalse(json.contains("username"));
@@ -90,7 +90,7 @@ class SetupControllerTest {
ConfigSource.BUILT_IN_DEFAULT, false),
new TelemetryStoreSummary(TelemetryStoreKind.GREPTIME, false,
ConfigSource.BUILT_IN_DEFAULT, false),
false, new OptionalConfigurationSummary(false, false, false, false)));
false, new OptionalConfigurationSummary(false, false, false, false, false)));
mvc.perform(get(SetupApiContract.STATUS_PATH))
.andExpect(status().isOk())
@@ -72,6 +72,25 @@ class SetupStatusProjectionFactoryTest {
assertThat(projection.optional().serverOtlpHttpConfigured()).isFalse();
}
@Test
void invalidExternalAddressesAreNotReportedAsConfigured() {
StandardEnvironment environment = new StandardEnvironment();
environment.getPropertySources().replace(
StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME,
new MapPropertySource(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, Map.of(
"spring.jpa.database", "H2",
"warehouse.store.greptime.enabled", "true",
"hertzbeat.setup.public-base-url", "http://0.0.0.0:1157",
"hertzbeat.instrumentation.server.otlp-http-endpoint", "http://collector.example.test:70000")));
var inspection = new ManagedActiveConfigurationInspector.Inspection(
ManagedActiveConfigurationInspector.State.ABSENT, Map.of(), Map.of());
var projection = new SetupStatusProjectionFactory().create(environment, inspection);
assertThat(projection.optional().publicBaseUrlConfigured()).isFalse();
assertThat(projection.optional().serverOtlpHttpConfigured()).isFalse();
}
@Test
void restartProjectionUsesEffectiveSourceAndRehydratesSafeManagedOptions() {
StandardEnvironment environment = new StandardEnvironment();
@@ -83,6 +102,7 @@ class SetupStatusProjectionFactoryTest {
ManagedActiveConfigurationInspector.MANAGED_APPLICATION_SOURCE,
Map.of("spring.jpa.database", "H2",
"warehouse.store.greptime.enabled", "true",
"hertzbeat.setup.public-base-url", "http://hertzbeat.example.test",
"hertzbeat.instrumentation.server.otlp-http-endpoint", "http://localhost:4318",
"warehouse.store.greptime.expire-time", "30d",
"spring.mail.host", "mail.example.test",
@@ -95,11 +115,12 @@ class SetupStatusProjectionFactoryTest {
assertThat(projection.managementDatabase().kind()).isEqualTo(MetadataDatabaseKind.POSTGRESQL);
assertThat(projection.managementDatabase().source()).isEqualTo(ConfigSource.SYSTEM_PROPERTY);
assertThat(projection.optional().publicBaseUrlConfigured()).isTrue();
assertThat(projection.optional().serverOtlpHttpConfigured()).isTrue();
assertThat(projection.optional().retentionConfigured()).isTrue();
assertThat(projection.optional().mailConfigured()).isTrue();
assertThat(projection.warnings()).containsExactlyInAnyOrder(
SetupWarningCode.SERVER_OTLP_PLAINTEXT, SetupWarningCode.MAIL_SECURITY_NONE);
SetupWarningCode.PUBLIC_ADDRESS_PLAINTEXT, SetupWarningCode.MAIL_SECURITY_NONE);
}
@Test
@@ -35,7 +35,8 @@ class ManagedOptionalConfigurationPersistenceTest {
ManagedConfigurationTransaction transaction = new ManagedConfigurationTransaction(root);
assertThat(transaction.apply(required())).isEqualTo(ManagedConfigurationTransaction.Outcome.APPLIED);
ManagedOptionalConfiguration options = new ManagedOptionalConfiguration(
Optional.of(new ManagedOptionalConfiguration.ServerInstrumentationSettings(
Optional.of(new ManagedOptionalConfiguration.PublicAccessSettings(
Optional.of("https://hertzbeat.example"),
Optional.of("https://hertzbeat.example/otlp"),
Optional.of("https://hertzbeat.example:4317"))),
Optional.of(new ManagedOptionalConfiguration.RetentionSettings(30)),
@@ -53,6 +54,7 @@ class ManagedOptionalConfigurationPersistenceTest {
assertThat(secrets.mailPassword()).get().isEqualTo(SecretValue.of("mail-secret"));
var properties = ApplicationConfigDocumentCodec.springProperties(application);
assertThat(properties)
.containsEntry("hertzbeat.setup.public-base-url", "https://hertzbeat.example")
.containsEntry("hertzbeat.instrumentation.server.otlp-http-endpoint",
"https://hertzbeat.example/otlp")
.containsEntry("hertzbeat.instrumentation.server.otlp-grpc-endpoint",
@@ -63,9 +65,7 @@ class ManagedOptionalConfigurationPersistenceTest {
.containsEntry("spring.mail.properties.mail.smtp.ssl.enable", "true")
.containsEntry("spring.mail.properties.mail.smtp.starttls.enable", "false")
.containsEntry("hertzbeat.mail.from-address", "alerts@example.test")
.doesNotContainKeys("hertzbeat.setup.public-base-url", "hertzbeat.setup.retention.metrics-days",
"hertzbeat.setup.retention.logs-days", "hertzbeat.setup.retention.traces-days",
"hertzbeat.setup.mail.security");
.doesNotContainKey("hertzbeat.setup.mail.security");
assertThat(properties.toString()).doesNotContain("mail-secret");
}
@@ -73,8 +73,8 @@ class ManagedOptionalConfigurationPersistenceTest {
void rejectsServerEndpointsWithoutCompleteInternalProfileSettings() throws Exception {
ManagedApplicationConfig application = required().application();
ManagedOptionalConfiguration options = new ManagedOptionalConfiguration(
Optional.of(new ManagedOptionalConfiguration.ServerInstrumentationSettings(
Optional.of("https://hertzbeat.example/otlp"), Optional.empty())),
Optional.of(new ManagedOptionalConfiguration.PublicAccessSettings(
Optional.empty(), Optional.of("https://hertzbeat.example/otlp"), Optional.empty())),
Optional.empty(), Optional.empty());
application = new ManagedApplicationConfig(
application.metadataDatabase(), application.telemetryStore(), options);
@@ -90,16 +90,38 @@ class ManagedOptionalConfigurationPersistenceTest {
}
@Test
void rejectsBlankManagedServerEndpoint() {
assertThatThrownBy(() -> new ManagedOptionalConfiguration.ServerInstrumentationSettings(
Optional.of(" "), Optional.empty()))
void publicBaseUrlRoundTripsWithoutInventingServerEndpoints() throws Exception {
ManagedApplicationConfig application = required().application();
ManagedOptionalConfiguration options = new ManagedOptionalConfiguration(
Optional.of(new ManagedOptionalConfiguration.PublicAccessSettings(
Optional.of("http://192.168.10.5:1157"), Optional.empty(), Optional.empty())),
Optional.empty(), Optional.empty());
application = new ManagedApplicationConfig(
application.metadataDatabase(), application.telemetryStore(), options);
ApplicationConfigDocumentCodec codec = new ApplicationConfigDocumentCodec();
ManagedApplicationConfig decoded = codec.decode(codec.encode(application, "generation")).value();
assertThat(decoded.optional()).isEqualTo(options);
assertThat(ApplicationConfigDocumentCodec.springProperties(decoded))
.containsEntry("hertzbeat.setup.public-base-url", "http://192.168.10.5:1157")
.doesNotContainKeys("hertzbeat.instrumentation.server.otlp-http-endpoint",
"hertzbeat.instrumentation.server.otlp-grpc-endpoint",
"hertzbeat.instrumentation.server.profile-id",
"hertzbeat.instrumentation.server.authentication");
}
@Test
void rejectsBlankManagedPublicAddress() {
assertThatThrownBy(() -> new ManagedOptionalConfiguration.PublicAccessSettings(
Optional.of(" "), Optional.empty(), Optional.empty()))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void rejectsControlOnlyManagedServerEndpoint() {
assertThatThrownBy(() -> new ManagedOptionalConfiguration.ServerInstrumentationSettings(
Optional.of("\u0000"), Optional.empty()))
void rejectsControlOnlyManagedPublicAddress() {
assertThatThrownBy(() -> new ManagedOptionalConfiguration.PublicAccessSettings(
Optional.of("\u0000"), Optional.empty(), Optional.empty()))
.isInstanceOf(IllegalArgumentException.class);
}
@@ -107,8 +129,8 @@ class ManagedOptionalConfigurationPersistenceTest {
void rejectsBlankServerEndpointInManagedDocument() throws Exception {
ManagedApplicationConfig application = required().application();
ManagedOptionalConfiguration options = new ManagedOptionalConfiguration(
Optional.of(new ManagedOptionalConfiguration.ServerInstrumentationSettings(
Optional.of("https://hertzbeat.example/otlp"), Optional.empty())),
Optional.of(new ManagedOptionalConfiguration.PublicAccessSettings(
Optional.empty(), Optional.of("https://hertzbeat.example/otlp"), Optional.empty())),
Optional.empty(), Optional.empty());
application = new ManagedApplicationConfig(
application.metadataDatabase(), application.telemetryStore(), options);
@@ -124,6 +146,27 @@ class ManagedOptionalConfigurationPersistenceTest {
.isInstanceOf(ManagedDocumentCodec.DocumentException.class);
}
@Test
void rejectsInvalidPublicAddressInManagedDocument() throws Exception {
ManagedApplicationConfig application = required().application();
ManagedOptionalConfiguration options = new ManagedOptionalConfiguration(
Optional.of(new ManagedOptionalConfiguration.PublicAccessSettings(
Optional.of("https://hertzbeat.example.test"), Optional.empty(), Optional.empty())),
Optional.empty(), Optional.empty());
application = new ManagedApplicationConfig(
application.metadataDatabase(), application.telemetryStore(), options);
ApplicationConfigDocumentCodec codec = new ApplicationConfigDocumentCodec();
ManagedDocumentCodec.Integrity.VerifiedBody encoded = ManagedDocumentCodec.Integrity.extract(
codec.encode(application, "generation"));
String invalid = encoded.content().replace(
"hertzbeat.setup.public-base-url: 'https://hertzbeat.example.test'",
"hertzbeat.setup.public-base-url: 'http://0.0.0.0:1157'");
byte[] document = ManagedDocumentCodec.Integrity.envelope(invalid, encoded.generation());
assertThatThrownBy(() -> codec.decode(document))
.isInstanceOf(ManagedDocumentCodec.DocumentException.class);
}
private static ManagedConfigurationBundle required() {
ManagedApplicationConfig application = new ManagedApplicationConfig(
new MetadataDatabaseSettings(MetadataDatabaseKind.H2, "jdbc:h2:./data/hertzbeat", "sa"),
@@ -109,13 +109,13 @@ class UnattendedSetupInitializerTest {
MockEnvironment environment = new MockEnvironment()
.withProperty(UnattendedSetupInitializer.ENABLED_PROPERTY, "true")
.withProperty("hertzbeat.setup.unattended.acknowledged-warnings",
"h2_non_production, server_otlp_plaintext");
"h2_non_production, public_address_plaintext");
new UnattendedSetupInitializer(workflow, environment, new SetupPasswordFileLoader(), transitions)
.initialize();
verify(workflow).complete(java.util.List.of(
SetupWarningCode.H2_NON_PRODUCTION, SetupWarningCode.SERVER_OTLP_PLAINTEXT));
SetupWarningCode.H2_NON_PRODUCTION, SetupWarningCode.PUBLIC_ADDRESS_PLAINTEXT));
verify(transitions).installationCompleted();
}
@@ -180,6 +180,6 @@ class UnattendedSetupInitializerTest {
new ManagementDatabaseSummary(MetadataDatabaseKind.H2, true, ConfigSource.UI_MANAGED, false),
new TelemetryStoreSummary(TelemetryStoreKind.GREPTIME, true, ConfigSource.UI_MANAGED, false),
phase != SetupPhase.ADMINISTRATOR_REQUIRED,
new OptionalConfigurationSummary(false, false, false, false));
new OptionalConfigurationSummary(false, false, false, false, false));
}
}
@@ -46,7 +46,7 @@ import org.apache.hertzbeat.manager.setup.api.SetupApiContract.ConfigurationResp
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.OptionsRequest;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.ServerInstrumentationConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.PublicAccessConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupAccess;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupOperationState;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupPhase;
@@ -77,13 +77,16 @@ class DefaultSetupWorkflowTest {
Optional.of(mock(SetupCompletionCoordinator.class)), mock(SetupOptionsCoordinator.class),
Clock.systemUTC(), new SetupMutationSerializer());
OptionsRequest request = new OptionsRequest(
new ServerInstrumentationConfiguration("https://server.example.test:4318", "\u0000"),
new PublicAccessConfiguration("https://hertzbeat.example.test",
"https://server.example.test:4318", "\u0000"),
null, null);
var response = workflow.configureOptions(request);
assertTrue(response.publicBaseUrlConfigured());
assertTrue(response.serverOtlpHttpConfigured());
assertFalse(response.serverOtlpGrpcConfigured());
assertTrue(state.status().optional().publicBaseUrlConfigured());
assertTrue(state.status().optional().serverOtlpHttpConfigured());
assertFalse(state.status().optional().serverOtlpGrpcConfigured());
}
@@ -156,7 +159,7 @@ class DefaultSetupWorkflowTest {
mock(SetupConfigurationCoordinator.class), capability,
Optional.of(mock(IdentityInitializationService.class)), Optional.of(completion), mutations);
OptionsRequest request = new OptionsRequest(
new ServerInstrumentationConfiguration("http://localhost:4318", null), null, null);
new PublicAccessConfiguration(null, "http://collector.example.test:4318", null), null, null);
try (var executor = Executors.newFixedThreadPool(2)) {
var optionsResult = executor.submit(() -> workflow.configureOptions(request));
@@ -40,9 +40,9 @@ class HeadlessSetupCoordinatorTest {
ManagedConfigCapability capability = mock(ManagedConfigCapability.class);
SetupRuntimeState state = new SetupRuntimeState(Clock.systemUTC(), capability,
SetupPhase.OPTIONAL_CONFIGURATION, SetupAccess.LOCAL, true, "operator");
state.optionsConfigured(new OptionalConfigurationSummary(true, false, false, false),
state.optionsConfigured(new OptionalConfigurationSummary(false, true, false, false, false),
List.of(SetupWarningCode.H2_NON_PRODUCTION,
SetupWarningCode.SERVER_OTLP_PLAINTEXT));
SetupWarningCode.PUBLIC_ADDRESS_PLAINTEXT));
SetupCompletionCoordinator completion = mock(SetupCompletionCoordinator.class);
SetupTransitionService transitions = new SetupTransitionService(state,
mock(SetupRequestValidator.class), mock(SetupConfigurationCoordinator.class), capability,
@@ -56,7 +56,7 @@ class HeadlessSetupCoordinatorTest {
verifyNoInteractions(completion);
coordinator.complete(List.of(SetupWarningCode.H2_NON_PRODUCTION,
SetupWarningCode.SERVER_OTLP_PLAINTEXT));
SetupWarningCode.PUBLIC_ADDRESS_PLAINTEXT));
verify(completion).completeInstallation();
}
}
@@ -26,7 +26,7 @@ import java.time.Instant;
import java.time.ZoneOffset;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.ServerInstrumentationConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.PublicAccessConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupErrorCode;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupWarningCode;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.ValidateRequest;
@@ -50,60 +50,155 @@ class SetupRequestValidatorTest {
}
@Test
void serverInstrumentationValidatorProducesStablePlaintextWarning() {
var response = validator.validate(new ValidateRequest(ValidationSection.SERVER_INSTRUMENTATION,
null, null, new ServerInstrumentationConfiguration("http://monitor.example.test", null), null));
void publicAccessValidatorProducesStablePlaintextWarningForPublicHttp() {
var response = validator.validate(new ValidateRequest(ValidationSection.PUBLIC_ACCESS,
null, null, new PublicAccessConfiguration(
"http://monitor.example.test", null, null), null));
assertTrue(response.valid());
assertEquals(1, response.warnings().size());
assertEquals(java.util.List.of(SetupWarningCode.PUBLIC_ADDRESS_PLAINTEXT), response.warnings());
}
@Test
void internalHttpAddressIsAllowedWithoutPublicPlaintextWarning() {
var response = validator.validate(new ValidateRequest(ValidationSection.PUBLIC_ACCESS,
null, null, new PublicAccessConfiguration("http://192.168.10.5:1157", null, null), null));
assertTrue(response.valid());
assertTrue(response.warnings().isEmpty());
}
@Test
void internalIpv6HttpAddressesDoNotProducePublicPlaintextWarning() {
for (String address : java.util.List.of(
"http://[::1]:1157", "http://[fd00::1]:1157",
"http://[::ffff:192.168.10.5]:1157", "http://[::ffff:127.0.0.1]:1157",
"http://[0:0:0:0::ffff:192.168.10.5]:1157", "http://[::ffff:c0a8:0a05]:1157")) {
var response = validator.validate(new ValidateRequest(ValidationSection.PUBLIC_ACCESS,
null, null, new PublicAccessConfiguration(address, null, null), null));
assertTrue(response.valid());
assertTrue(response.warnings().isEmpty());
}
}
@Test
void publicBaseUrlMustBeAnExplicitAbsoluteHttpOrHttpsAddress() {
var relative = validator.validate(new ValidateRequest(ValidationSection.PUBLIC_ACCESS,
null, null, new PublicAccessConfiguration("/from-browser-origin", null, null), null));
var https = validator.validate(new ValidateRequest(ValidationSection.PUBLIC_ACCESS,
null, null, new PublicAccessConfiguration("https://hertzbeat.example.test", null, null), null));
assertFalse(relative.valid());
assertEquals(SetupErrorCode.PUBLIC_ADDRESS_INVALID, relative.errorCode());
assertTrue(https.valid());
assertTrue(https.warnings().isEmpty());
}
@Test
void serverGrpcEndpointMustBeAnExplicitHttpUrl() {
var response = validator.validate(new ValidateRequest(ValidationSection.SERVER_INSTRUMENTATION,
null, null, new ServerInstrumentationConfiguration(null, "collector.example.test:4317"), null));
var response = validator.validate(new ValidateRequest(ValidationSection.PUBLIC_ACCESS,
null, null, new PublicAccessConfiguration(null, null, "collector.example.test:4317"), null));
assertFalse(response.valid());
assertEquals(SetupErrorCode.SERVER_INSTRUMENTATION_INVALID, response.errorCode());
assertEquals(SetupErrorCode.PUBLIC_ADDRESS_INVALID, response.errorCode());
}
@Test
void serverEndpointsRejectPortsOutsideTheTcpRange() {
for (PublicAccessConfiguration configuration : java.util.List.of(
new PublicAccessConfiguration("http://hertzbeat.example.test:0", null, null),
new PublicAccessConfiguration("http://hertzbeat.example.test:70000", null, null),
new PublicAccessConfiguration(null, "http://collector.example.test:0", null),
new PublicAccessConfiguration(null, "http://collector.example.test:70000", null),
new PublicAccessConfiguration(null, null, "http://collector.example.test:0"),
new PublicAccessConfiguration(null, null, "http://collector.example.test:70000"))) {
var response = validator.validate(new ValidateRequest(ValidationSection.PUBLIC_ACCESS,
null, null, configuration, null));
assertFalse(response.valid());
assertEquals(SetupErrorCode.PUBLIC_ADDRESS_INVALID, response.errorCode());
}
}
@Test
void advertisedAddressesRejectWildcardHosts() {
for (PublicAccessConfiguration configuration : java.util.List.of(
new PublicAccessConfiguration("http://0.0.0.0:1157", null, null),
new PublicAccessConfiguration("http://[::]:1157", null, null),
new PublicAccessConfiguration("http://[::ffff:0.0.0.0]:1157", null, null),
new PublicAccessConfiguration("http://[::ffff:0:0]:1157", null, null),
new PublicAccessConfiguration(null, "http://0.0.0.0:4318", null),
new PublicAccessConfiguration(null, null, "http://[0:0:0:0:0:0:0:0]:4317"))) {
var response = validator.validate(new ValidateRequest(ValidationSection.PUBLIC_ACCESS,
null, null, configuration, null));
assertFalse(response.valid());
assertEquals(SetupErrorCode.PUBLIC_ADDRESS_INVALID, response.errorCode());
}
}
@Test
void advertisedIpv6AddressesRejectZoneIdentifiers() {
for (PublicAccessConfiguration configuration : java.util.List.of(
new PublicAccessConfiguration("http://[::%25eth0]:4318", null, null),
new PublicAccessConfiguration("http://[::%eth0]:4318", null, null),
new PublicAccessConfiguration(null, "http://[fe80::1%25eth0]:4318", null),
new PublicAccessConfiguration(null, "http://[fe80::1%eth0]:4318", null))) {
var response = validator.validate(new ValidateRequest(ValidationSection.PUBLIC_ACCESS,
null, null, configuration, null));
assertFalse(response.valid());
assertEquals(SetupErrorCode.PUBLIC_ADDRESS_INVALID, response.errorCode());
}
}
@Test
void publicIpv4MappedIpv6AddressStillProducesPlaintextWarning() {
var response = validator.validate(new ValidateRequest(ValidationSection.PUBLIC_ACCESS,
null, null, new PublicAccessConfiguration("http://[::ffff:0808:0808]:1157", null, null), null));
assertTrue(response.valid());
assertEquals(java.util.List.of(SetupWarningCode.PUBLIC_ADDRESS_PLAINTEXT), response.warnings());
}
@Test
void serverEndpointRejectsUrlCredentialsAndQuery() {
var response = validator.validate(new ValidateRequest(ValidationSection.SERVER_INSTRUMENTATION,
null, null, new ServerInstrumentationConfiguration(
var response = validator.validate(new ValidateRequest(ValidationSection.PUBLIC_ACCESS,
null, null, new PublicAccessConfiguration(null,
"https://user:secret@collector.example.test:4318?token=secret", null), null));
assertFalse(response.valid());
assertEquals(SetupErrorCode.SERVER_INSTRUMENTATION_INVALID, response.errorCode());
assertEquals(SetupErrorCode.PUBLIC_ADDRESS_INVALID, response.errorCode());
}
@Test
void grpcOnlyPlaintextEndpointProducesWarning() {
var response = validator.validate(new ValidateRequest(ValidationSection.SERVER_INSTRUMENTATION,
null, null, new ServerInstrumentationConfiguration(null, "http://collector.example.test:4317"),
null));
var response = validator.validate(new ValidateRequest(ValidationSection.PUBLIC_ACCESS,
null, null, new PublicAccessConfiguration(
null, null, "http://collector.example.test:4317"), null));
assertTrue(response.valid());
assertEquals(java.util.List.of(SetupWarningCode.SERVER_OTLP_PLAINTEXT), response.warnings());
assertEquals(java.util.List.of(SetupWarningCode.PUBLIC_ADDRESS_PLAINTEXT), response.warnings());
}
@Test
void serverInstrumentationSectionRequiresAtLeastOneEndpoint() {
var response = validator.validate(new ValidateRequest(ValidationSection.SERVER_INSTRUMENTATION,
null, null, new ServerInstrumentationConfiguration(" ", null), null));
void publicAccessSectionRequiresAtLeastOneExplicitAddress() {
var response = validator.validate(new ValidateRequest(ValidationSection.PUBLIC_ACCESS,
null, null, new PublicAccessConfiguration(" ", null, null), null));
assertFalse(response.valid());
assertEquals(SetupErrorCode.SERVER_INSTRUMENTATION_INVALID, response.errorCode());
assertEquals(SetupErrorCode.PUBLIC_ADDRESS_INVALID, response.errorCode());
}
@Test
void endpointWhitespaceIsNormalizedBeforeValidationAndWarnings() {
var response = validator.validate(new ValidateRequest(ValidationSection.SERVER_INSTRUMENTATION,
null, null, new ServerInstrumentationConfiguration(
var response = validator.validate(new ValidateRequest(ValidationSection.PUBLIC_ACCESS,
null, null, new PublicAccessConfiguration(null,
" https://collector.example.test:4318/otlp ",
" http://collector.example.test:4317 "), null));
assertTrue(response.valid());
assertEquals(java.util.List.of(SetupWarningCode.SERVER_OTLP_PLAINTEXT), response.warnings());
assertEquals(java.util.List.of(SetupWarningCode.PUBLIC_ADDRESS_PLAINTEXT), response.warnings());
}
}
@@ -36,7 +36,7 @@ import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseC
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.OptionalConfigurationSummary;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.OptionsRequest;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.ServerInstrumentationConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.PublicAccessConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupAccess;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupErrorCode;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupOperationState;
@@ -226,10 +226,10 @@ class SetupTransitionServiceTest {
}
@Test
void serverInstrumentationValidationFailureDoesNotPersistOrPublishState() {
void publicAccessValidationFailureDoesNotPersistOrPublishState() {
assertOptionsValidationFailure(new OptionsRequest(
new ServerInstrumentationConfiguration("not-an-endpoint", null), null, null),
ValidationSection.SERVER_INSTRUMENTATION, SetupErrorCode.SERVER_INSTRUMENTATION_INVALID);
new PublicAccessConfiguration("not-an-address", null, null), null, null),
ValidationSection.PUBLIC_ACCESS, SetupErrorCode.PUBLIC_ADDRESS_INVALID);
}
@Test
@@ -270,7 +270,7 @@ class SetupTransitionServiceTest {
ConfigSource.UI_MANAGED, false),
new TelemetryStoreSummary(TelemetryStoreKind.GREPTIME, true,
ConfigSource.UI_MANAGED, false),
new OptionalConfigurationSummary(false, false, false, false), List.of());
new OptionalConfigurationSummary(false, false, false, false, false), List.of());
SetupRuntimeState state = new SetupRuntimeState(CLOCK, capability,
SetupPhase.OPTIONAL_CONFIGURATION, SetupAccess.LOCAL, true, "operator", projection);
SetupRequestValidator validator = mock(SetupRequestValidator.class);
@@ -281,16 +281,18 @@ class SetupTransitionServiceTest {
mock(SetupConfigurationCoordinator.class), capability, options,
Optional.empty(), Optional.empty());
OptionsRequest request = new OptionsRequest(
new ServerInstrumentationConfiguration(" ", "https://server.example.test:4317"), null,
new PublicAccessConfiguration("https://hertzbeat.example.test", " ",
"https://server.example.test:4317"), null,
new MailConfiguration("mail.example.test", 25, MailSecurity.NONE,
null, null, "alerts@example.test"));
var response = transitions.configureOptions(request);
assertThat(response.publicBaseUrlConfigured()).isTrue();
assertThat(response.serverOtlpHttpConfigured()).isFalse();
assertThat(response.serverOtlpGrpcConfigured()).isTrue();
assertThat(state.status().optional()).isEqualTo(
new OptionalConfigurationSummary(false, true, false, true));
new OptionalConfigurationSummary(true, false, true, false, true));
assertThat(state.pendingWarnings()).containsExactly(SetupWarningCode.MAIL_SECURITY_NONE);
verify(options).persist(request);
}
@@ -382,7 +384,7 @@ class SetupTransitionServiceTest {
private static OptionsRequest optionsRequest() {
return new OptionsRequest(
new ServerInstrumentationConfiguration("https://server.example.test:4318", null), null, null);
new PublicAccessConfiguration(null, "https://server.example.test:4318", null), null, null);
}
private static HeadlessSetupWorkflow.RequiredConfiguration headlessConfiguration(SecretValue password) {
@@ -23,7 +23,7 @@ import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MailConfiguration
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MailSecurity;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.OptionsRequest;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.ServerInstrumentationConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.PublicAccessConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupWarningCode;
import org.junit.jupiter.api.Test;
@@ -31,19 +31,20 @@ class SetupWarningPolicyTest {
@Test
void liveAndRestartInputsProduceTheSameWarnings() {
var options = new OptionsRequest(
new ServerInstrumentationConfiguration("http://localhost:4318", null), null,
new PublicAccessConfiguration("http://localhost:1157", "http://localhost:4318", null), null,
new MailConfiguration("localhost", 25, MailSecurity.NONE, null, null, "ops@example.test"));
assertThat(SetupWarningPolicy.INSTANCE.evaluate(MetadataDatabaseKind.H2, options))
.containsExactlyElementsOf(SetupWarningPolicy.INSTANCE.evaluate(
MetadataDatabaseKind.H2, "http://localhost:4318", null, MailSecurity.NONE));
MetadataDatabaseKind.H2, "http://localhost:1157", "http://localhost:4318", null,
MailSecurity.NONE));
}
@Test
void whitespaceWrappedGrpcEndpointStillProducesPlaintextWarning() {
var options = new OptionsRequest(
new ServerInstrumentationConfiguration(null, " http://localhost:4317 "), null, null);
new PublicAccessConfiguration(null, null, " http://collector.example.test:4317 "), null, null);
assertThat(SetupWarningPolicy.INSTANCE.evaluate(MetadataDatabaseKind.MYSQL, options))
.containsExactly(SetupWarningCode.SERVER_OTLP_PLAINTEXT);
.containsExactly(SetupWarningCode.PUBLIC_ADDRESS_PLAINTEXT);
}
}