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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.micrometer.core.instrument.MeterRegistry;
import io.opentelemetry.api.trace.Tracer;
import java.util.Optional;
import org.jspecify.annotations.Nullable;
import ru.tinkoff.kora.application.graph.All;
import ru.tinkoff.kora.application.graph.PromiseOf;
Expand All @@ -13,6 +14,7 @@
import ru.tinkoff.kora.config.common.Config;
import ru.tinkoff.kora.config.common.extractor.ConfigValueExtractionException;
import ru.tinkoff.kora.config.common.extractor.ConfigValueExtractor;
import ru.tinkoff.kora.http.server.common.annotation.InternalApi;
import ru.tinkoff.kora.http.server.common.annotation.PrivateApi;
import ru.tinkoff.kora.http.server.common.handler.HttpServerRequestHandler;
import ru.tinkoff.kora.http.server.common.privateapi.LivenessHandler;
Expand All @@ -23,8 +25,6 @@
import ru.tinkoff.kora.http.server.common.telemetry.impl.DefaultHttpServerTelemetryFactory;
import ru.tinkoff.kora.telemetry.common.MetricsScraper;

import java.util.Optional;

public interface HttpServerModule extends StringParameterReadersModule, HttpServerRequestMapperModule, HttpServerResponseMapperModule {

default HttpServerConfig httpServerConfig(Config config, ConfigValueExtractor<HttpServerConfig> configValueExtractor) {
Expand Down Expand Up @@ -77,4 +77,19 @@ default HttpServerHandler privateApiHandler(@Tag(PrivateApi.class) All<HttpServe
return new HttpServerHandler(handlers, interceptors, config);
}

@InternalApi
default InternalHttpServerConfig internalApiHttpServerConfig(Config config, ConfigValueExtractor<InternalHttpServerConfig> configValueExtractor) {
var value = config.get("internalHttpServer");
var parsed = configValueExtractor.extract(value);
if (parsed == null) {
throw ConfigValueExtractionException.missingValueAfterParse(value);
}
return parsed;
}

@InternalApi
default HttpServerHandler internalApiHandler(@Tag(InternalApi.class) All<HttpServerRequestHandler> handlers, @Tag(InternalApi.class) All<HttpServerInterceptor> interceptors, HttpServerConfig config) {
return new HttpServerHandler(handlers, interceptors, config);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ru.tinkoff.kora.http.server.common;

import ru.tinkoff.kora.config.common.annotation.ConfigValueExtractor;

@ConfigValueExtractor
public interface InternalHttpServerConfig extends HttpServerConfig {

@Override
default int port() {
return 8090;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ru.tinkoff.kora.http.server.common;

public enum NoopHttpServer implements HttpServer {

INSTANCE;

@Override
public int port() {
return -1;
}

@Override
public void init() {
// no-op
}

@Override
public void release() {
// no-op
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ru.tinkoff.kora.http.server.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
import ru.tinkoff.kora.common.Tag;

@Tag(InternalApi.class)
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface InternalApi {
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
package ru.tinkoff.kora.http.server.common;

import static java.time.Instant.now;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static ru.tinkoff.kora.http.common.HttpMethod.GET;
import static ru.tinkoff.kora.http.common.HttpMethod.POST;

import io.opentelemetry.api.trace.Span;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okio.BufferedSink;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.mockito.AdditionalAnswers;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
Expand All @@ -32,28 +63,15 @@
import ru.tinkoff.kora.http.server.common.privateapi.MetricsHandler;
import ru.tinkoff.kora.http.server.common.privateapi.ReadinessHandler;
import ru.tinkoff.kora.http.server.common.router.HttpServerHandler;
import ru.tinkoff.kora.http.server.common.telemetry.*;
import ru.tinkoff.kora.http.server.common.telemetry.$HttpServerTelemetryConfig_ConfigValueExtractor;
import ru.tinkoff.kora.http.server.common.telemetry.$HttpServerTelemetryConfig_HttpServerLoggingConfig_ConfigValueExtractor;
import ru.tinkoff.kora.http.server.common.telemetry.$HttpServerTelemetryConfig_HttpServerMetricsConfig_ConfigValueExtractor;
import ru.tinkoff.kora.http.server.common.telemetry.$HttpServerTelemetryConfig_HttpServerTracingConfig_ConfigValueExtractor;
import ru.tinkoff.kora.http.server.common.telemetry.HttpServerObservation;
import ru.tinkoff.kora.http.server.common.telemetry.HttpServerTelemetry;
import ru.tinkoff.kora.http.server.common.telemetry.NoopHttpServerTelemetry;
import ru.tinkoff.kora.telemetry.common.MetricsScraper;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.function.Supplier;

import static java.time.Instant.now;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.*;
import static ru.tinkoff.kora.http.common.HttpMethod.GET;
import static ru.tinkoff.kora.http.common.HttpMethod.POST;

@TestInstance(TestInstance.Lifecycle.PER_METHOD)
public abstract class HttpServerTestKit {
protected static MetricsScraper registry = Mockito.mock(MetricsScraper.class);
Expand All @@ -74,6 +92,7 @@ public abstract class HttpServerTestKit {

private volatile HttpServer httpServer = null;
private volatile HttpServer privateHttpServer = null;
private volatile HttpServer internalHttpServer = null;

protected final OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(0, 1, TimeUnit.MICROSECONDS))
Expand Down Expand Up @@ -190,6 +209,84 @@ void testReadinessFailureOnUninitializedProbe() throws IOException {
}


@Nested
public class InternalApiTest {
@Test
void testNoopHttpServerWhenNoHandlers() {
var noopServer = NoopHttpServer.INSTANCE;
assertThat(noopServer.port()).isEqualTo(-1);
Assertions.assertDoesNotThrow(noopServer::init);
Assertions.assertDoesNotThrow(noopServer::release);
}

@Test
void testInternalApiHelloWorld() throws IOException {
var httpResponse = HttpServerResponse.of(200, HttpBody.plaintext("internal hello"));
var handler = handler(GET, "/internal", (_) -> httpResponse);
startInternalHttpServer(handler);

var request = internalApiRequest("/internal")
.get()
.build();

try (var response = client.newCall(request).execute()) {
assertThat(response.code()).isEqualTo(200);
assertThat(response.body().string()).isEqualTo("internal hello");
}
}

@Test
void testInternalApiUnknownPath() throws IOException {
var handler = handler(GET, "/internal", (_) -> HttpServerResponse.of(200));
startInternalHttpServer(handler);

var request = internalApiRequest("/unknown")
.get()
.build();

try (var response = client.newCall(request).execute()) {
assertThat(response.code()).isEqualTo(404);
}
}

@Test
void testInternalApiWithInterceptor() throws IOException {
var httpResponse = HttpServerResponse.of(200, HttpBody.plaintext("internal hello"));
var handler = handler(GET, "/internal", (_) -> httpResponse);
var interceptor = new HttpServerInterceptor() {
@Override
public HttpServerResponse intercept(HttpServerRequest request, InterceptChain chain) throws Exception {
var header = request.headers().getFirst("x-internal-block");
if (header != null) {
request.body().close();
return HttpServerResponse.of(403, HttpBody.plaintext("blocked"));
}
return chain.process(request);
}
};
startInternalHttpServer(List.of(interceptor), handler);

var request = internalApiRequest("/internal")
.get()
.build();

try (var response = client.newCall(request).execute()) {
assertThat(response.code()).isEqualTo(200);
assertThat(response.body().string()).isEqualTo("internal hello");
}

var blockedRequest = internalApiRequest("/internal")
.header("x-internal-block", "true")
.get()
.build();

try (var response = client.newCall(blockedRequest).execute()) {
assertThat(response.code()).isEqualTo(403);
assertThat(response.body().string()).isEqualTo("blocked");
}
}
}

@Nested
public class PublicApiTest {
@Test
Expand Down Expand Up @@ -990,6 +1087,34 @@ protected void startPrivateHttpServer() {
}
}

protected void startInternalHttpServer(HttpServerRequestHandler... handlers) {
startInternalHttpServer(List.of(), handlers);
}

protected void startInternalHttpServer(List<HttpServerInterceptor> interceptors, HttpServerRequestHandler... handlers) {
var config = new HttpServerConfig_Impl(
0,
false,
Duration.ofSeconds(1),
Duration.ofSeconds(1),
false,
Duration.ofMillis(1),
new $HttpServerTelemetryConfig_ConfigValueExtractor.HttpServerTelemetryConfig_Impl(
new $HttpServerTelemetryConfig_HttpServerLoggingConfig_ConfigValueExtractor.HttpServerLoggingConfig_Defaults(),
new $HttpServerTelemetryConfig_HttpServerMetricsConfig_ConfigValueExtractor.HttpServerMetricsConfig_Defaults(),
new $HttpServerTelemetryConfig_HttpServerTracingConfig_ConfigValueExtractor.HttpServerTracingConfig_Defaults()
),
Size.of(1, Size.Type.GiB)
);
var internalApiHandler = new HttpServerHandler(List.of(handlers), interceptors, config);
this.internalHttpServer = this.httpServer(valueOf(config), internalApiHandler, this.telemetry);
try {
this.internalHttpServer.init();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

@AfterEach
void tearDown() throws Exception {
if (this.httpServer != null) {
Expand All @@ -1000,6 +1125,10 @@ void tearDown() throws Exception {
this.privateHttpServer.release();
this.privateHttpServer = null;
}
if (this.internalHttpServer != null) {
this.internalHttpServer.release();
this.internalHttpServer = null;
}
this.readinessProbePromise.setValue(readinessProbe);
this.livenessProbePromise.setValue(livenessProbe);
}
Expand All @@ -1021,6 +1150,10 @@ protected Request.Builder privateApiRequest(String path) {
return request(this.privateHttpServer.port(), path);
}

protected Request.Builder internalApiRequest(String path) {
return request(this.internalHttpServer.port(), path);
}

protected Request.Builder request(String path) {
return request(this.httpServer.port(), path);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
import io.undertow.Undertow;
import org.jspecify.annotations.Nullable;
import org.xnio.XnioWorker;
import ru.tinkoff.kora.application.graph.All;
import ru.tinkoff.kora.application.graph.ValueOf;
import ru.tinkoff.kora.common.Tag;
import ru.tinkoff.kora.common.annotation.Root;
import ru.tinkoff.kora.common.util.Configurer;
import ru.tinkoff.kora.http.server.common.HttpServer;
import ru.tinkoff.kora.http.server.common.HttpServerConfig;
import ru.tinkoff.kora.http.server.common.InternalHttpServerConfig;
import ru.tinkoff.kora.http.server.common.NoopHttpServer;
import ru.tinkoff.kora.http.server.common.annotation.InternalApi;
import ru.tinkoff.kora.http.server.common.handler.HttpServerRequestHandler;
import ru.tinkoff.kora.http.server.common.router.HttpServerHandler;
import ru.tinkoff.kora.http.server.common.telemetry.HttpServerTelemetryFactory;

Expand All @@ -20,4 +27,19 @@ default UndertowHttpServer undertowHttpServer(ValueOf<HttpServerConfig> config,
var telemetry = telemetryFactory.get(config.get().telemetry());
return new UndertowHttpServer(config, handler, "kora-undertow", telemetry, worker, configurer);
}

@Root
@InternalApi
default HttpServer internalApiUndertowHttpServer(@Tag(InternalApi.class) All<HttpServerRequestHandler> handlers,
@InternalApi ValueOf<InternalHttpServerConfig> config,
@InternalApi ValueOf<HttpServerHandler> handler,
HttpServerTelemetryFactory telemetryFactory,
XnioWorker worker,
@Nullable @InternalApi Configurer<Undertow.Builder> configurer) {
if (handlers.isEmpty()) {
return NoopHttpServer.INSTANCE;
}
var telemetry = telemetryFactory.get(config.get().telemetry());
return new UndertowHttpServer(config, handler, "kora-undertow-internal", telemetry, worker, configurer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ default UndertowConfig undertowHttpServerConfig(Config config, ConfigValueExtrac
}
return parsed;
}

}