mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 09:40:58 +00:00
Add operator-owned setup public addresses
This commit is contained in:
+19
-9
@@ -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
-7
@@ -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) {
|
||||
|
||||
+20
-14
@@ -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) {
|
||||
|
||||
+1
@@ -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";
|
||||
|
||||
+22
-19
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+159
@@ -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].*");
|
||||
}
|
||||
}
|
||||
+12
-9
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+46
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-59
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+11
-8
@@ -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())),
|
||||
|
||||
+3
-4
@@ -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);
|
||||
}
|
||||
|
||||
+3
-3
@@ -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,
|
||||
|
||||
+15
-16
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+29
-12
@@ -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"));
|
||||
|
||||
+1
-1
@@ -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())
|
||||
|
||||
+22
-1
@@ -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
|
||||
|
||||
+57
-14
@@ -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"),
|
||||
|
||||
+3
-3
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -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));
|
||||
|
||||
+3
-3
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+117
-22
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+10
-8
@@ -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) {
|
||||
|
||||
+6
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user