Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

eureka: supports credentials in EUREKA_SERVICE_URL #3700

Merged
merged 2 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -91,7 +91,7 @@ class ServerIntegratedBenchmark {

@Test void elasticsearch() throws Exception {
GenericContainer<?> elasticsearch =
new GenericContainer<>(parse("ghcr.io/openzipkin/zipkin-elasticsearch7:3.0.1"))
new GenericContainer<>(parse("ghcr.io/openzipkin/zipkin-elasticsearch7:3.0.4"))
.withNetwork(Network.SHARED)
.withNetworkAliases("elasticsearch")
.withLabel("name", "elasticsearch")
Expand All @@ -105,7 +105,7 @@ class ServerIntegratedBenchmark {

@Test void cassandra3() throws Exception {
GenericContainer<?> cassandra =
new GenericContainer<>(parse("ghcr.io/openzipkin/zipkin-cassandra:3.0.1"))
new GenericContainer<>(parse("ghcr.io/openzipkin/zipkin-cassandra:3.0.4"))
.withNetwork(Network.SHARED)
.withNetworkAliases("cassandra")
.withLabel("name", "cassandra")
Expand All @@ -119,7 +119,7 @@ class ServerIntegratedBenchmark {

@Test void mysql() throws Exception {
GenericContainer<?> mysql =
new GenericContainer<>(parse("ghcr.io/openzipkin/zipkin-mysql:3.0.1"))
new GenericContainer<>(parse("ghcr.io/openzipkin/zipkin-mysql:3.0.4"))
.withNetwork(Network.SHARED)
.withNetworkAliases("mysql")
.withLabel("name", "mysql")
Expand Down
16 changes: 13 additions & 3 deletions docker/examples/docker-compose-eureka.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,22 @@ services:
eureka:
image: ghcr.io/openzipkin/zipkin-eureka:${TAG:-latest}
container_name: eureka
# Uncomment to expose the eureka port for testing
# ports:
# - 8761:8761
# Uncomment to require authentication
# environment:
# - EUREKA_USERNAME=username
# - EUREKA_PASSWORD=password
# Uncomment to expose the eureka port for testing
# ports:
# - 8761:8761

zipkin:
extends:
file: docker-compose.yml
service: zipkin
environment:
- EUREKA_SERVICE_URL=http://eureka:8761/eureka/v2
# Uncomment to authenticate eureka
#- EUREKA_SERVICE_URL=http://username:password@eureka:8761/eureka/v2
- EUREKA_HOSTNAME=zipkin
depends_on:
eureka:
Expand All @@ -46,6 +52,8 @@ services:
entrypoint: start-frontend
environment:
- EUREKA_SERVICE_URL=http://eureka:8761/eureka/v2
# Uncomment to authenticate eureka
#- EUREKA_SERVICE_URL=http://username:password@eureka:8761/eureka/v2
ports:
- 8081:8081
depends_on:
Expand All @@ -61,6 +69,8 @@ services:
entrypoint: start-backend
environment:
- EUREKA_SERVICE_URL=http://eureka:8761/eureka/v2
# Uncomment to authenticate eureka
#- EUREKA_SERVICE_URL=http://username:password@eureka:8761/eureka/v2
depends_on:
zipkin:
condition: service_healthy
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ String brokerURL() {
// mostly waiting for https://github.com/testcontainers/testcontainers-java/issues/3537
static final class ActiveMQContainer extends GenericContainer<ActiveMQContainer> {
ActiveMQContainer() {
super(parse("ghcr.io/openzipkin/zipkin-activemq:3.0.1"));
super(parse("ghcr.io/openzipkin/zipkin-activemq:3.0.4"));
withExposedPorts(ACTIVEMQ_PORT);
waitStrategy = Wait.forListeningPorts(ACTIVEMQ_PORT);
withStartupTimeout(Duration.ofSeconds(60));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ KafkaCollector.Builder newCollectorBuilder(String topic, int streams) {
// mostly waiting for https://github.com/testcontainers/testcontainers-java/issues/3537
static final class KafkaContainer extends GenericContainer<KafkaContainer> {
KafkaContainer() {
super(parse("ghcr.io/openzipkin/zipkin-kafka:3.0.1"));
super(parse("ghcr.io/openzipkin/zipkin-kafka:3.0.4"));
waitStrategy = Wait.forHealthcheck();
// 19092 is for connections from the Docker host and needs to be used as a fixed port.
// TODO: someone who knows Kafka well, make ^^ comment better!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ int port() {
// mostly waiting for https://github.com/testcontainers/testcontainers-java/issues/3537
static final class RabbitMQContainer extends GenericContainer<RabbitMQContainer> {
RabbitMQContainer() {
super(parse("ghcr.io/openzipkin/zipkin-rabbitmq:3.0.1"));
super(parse("ghcr.io/openzipkin/zipkin-rabbitmq:3.0.4"));
withExposedPorts(RABBIT_PORT);
waitStrategy = Wait.forLogMessage(".*Server startup complete.*", 1);
withStartupTimeout(Duration.ofSeconds(60));
Expand Down
5 changes: 5 additions & 0 deletions zipkin-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,11 @@ If you are using a containerized environment, you may need to set `EUREKA_HOSTNA
detecting the wrong hostname. For example, if using docker-compose, set `EUREKA_HOSTNAME` to
zipkin's `container_name`.

If your Eureka server requires authentication, adjust `EUREKA_SERVICE_URL` accordingly. If user info
is present, those credentials will be used for BASIC authentication. For example, if the URL is
`https://myuser:[email protected]/eureka/v2/`, requests to Eureka will authenticate with the user
"myuser" and password "mypassword".

Note: Eureka server registration only includes host and port details. Tracers need to resolve this
to the POST endpoint "/api/v2/spans".

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
*/
package zipkin2.server.internal.eureka;

import com.linecorp.armeria.common.auth.BasicToken;
import com.linecorp.armeria.server.eureka.EurekaUpdatingListener;
import com.linecorp.armeria.server.eureka.EurekaUpdatingListenerBuilder;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
Expand All @@ -30,21 +33,51 @@
*/
@ConfigurationProperties("zipkin.discovery.eureka")
class ZipkinEurekaDiscoveryProperties implements Serializable { // for Spark jobs
/** URL of the Eureka v2 endpoint. e.g. http://eureka:8761/eureka/v2 */
private String serviceUrl;
/**
* URL of the Eureka v2 endpoint. e.g. http://eureka:8761/eureka/v2
*
* <p>Note: When present, {@link URI#getUserInfo() userInfo} credentials will be used for BASIC
* authentication. For example, if the URL is "https://myuser:[email protected]/eureka/v2/",
* requests to Eureka will authenticate with the user "myuser" and password "mypassword".
*/
private URI serviceUrl;
/** The appName property of this instance of zipkin. */
private String appName;
/** The instanceId property of this instance of zipkin. */
private String instanceId;
/** The hostName property of this instance of zipkin. */
private String hostname;

public String getServiceUrl() {
private BasicToken auth;

public URI getServiceUrl() {
return serviceUrl;
}

public void setServiceUrl(String serviceUrl) {
this.serviceUrl = emptyToNull(serviceUrl);
public void setServiceUrl(URI serviceUrl) {
if (serviceUrl == null || serviceUrl.toString().isEmpty()) {
this.serviceUrl = null;
return;
}
if (serviceUrl.getUserInfo() != null) {
String[] ui = serviceUrl.getUserInfo().split(":");
if (ui.length == 2) {
auth = BasicToken.ofBasic(ui[0], ui[1]);
}
}
this.serviceUrl = stripBaseUrl(serviceUrl);
}

// Strip the credentials and any invalid query or fragment from the URI:
// The Eureka API doesn't define any global query params or fragment.
// See https://github.com/Netflix/eureka/wiki/Eureka-REST-operations
static URI stripBaseUrl(URI serviceUrl) {
try {
return new URI(serviceUrl.getScheme(), null, serviceUrl.getHost(), serviceUrl.getPort(),
serviceUrl.getPath(), null, null);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}

public String getAppName() {
Expand Down Expand Up @@ -72,7 +105,8 @@ public void setHostname(String hostname) {
}

EurekaUpdatingListenerBuilder toBuilder() {
final EurekaUpdatingListenerBuilder result = EurekaUpdatingListener.builder(serviceUrl);
EurekaUpdatingListenerBuilder result = EurekaUpdatingListener.builder(serviceUrl);
if (auth != null) result.auth(auth);
if (appName != null) result.appName(appName);
if (instanceId != null) result.instanceId(instanceId);
if (hostname != null) result.hostname(hostname);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* Copyright 2015-2024 The OpenZipkin Authors
*
* Licensed 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 zipkin2.server.internal.eureka;

import com.jayway.jsonpath.JsonPath;
import com.linecorp.armeria.server.Server;
import java.io.IOException;
import java.time.Duration;
import java.util.Map;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.junit.jupiter.Testcontainers;
import zipkin.server.ZipkinServer;

import static okhttp3.Credentials.basic;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.testcontainers.utility.DockerImageName.parse;

/**
* Integration test for Eureka, which validates with <a href="https://github.com/Netflix/eureka/wiki/Eureka-REST-operations">Eureka REST operations</a>
*
* <p>Note: We only validate that authentication works, as it is cheaper than also testing without
* it.
*/
@SpringBootTest(
classes = ZipkinServer.class,
webEnvironment = SpringBootTest.WebEnvironment.NONE, // RANDOM_PORT requires spring-web
properties = {
"server.port=0",
"spring.config.name=zipkin-server",
"spring.main.banner-mode=off",
"zipkin.storage.type=", // cheat and test empty storage type
"zipkin.collector.http.enabled=false",
"zipkin.query.enabled=false",
"zipkin.ui.enabled=false"
})
@Tag("docker")
@Testcontainers(disabledWithoutDocker = true)
@TestMethodOrder(OrderAnnotation.class) // so that deregistration tests don't flake the others.
abstract class BaseITZipkinEureka {
/**
* Path under the Eureka v2 endpoint for the app named "zipkin".
* Note that Eureka always coerces app names to uppercase.
*/
private static final String APPS_ZIPKIN = "apps/ZIPKIN";
private final OkHttpClient client = new OkHttpClient.Builder().followRedirects(false).build();

@Autowired Server zipkin;

final HttpUrl serviceUrl;

BaseITZipkinEureka(HttpUrl serviceUrl) {
this.serviceUrl = serviceUrl;
}

@BeforeEach void awaitRegistration() {
// The zipkin server may start before Eureka processes the registration
await().until(this::getEurekaZipkinAppAsString, (s) -> true);
}

@Test @Order(1) void registersInEureka() throws IOException {
String json = getEurekaZipkinAppAsString();

// Make sure the health status is OK
assertThat(readString(json, "$.application.instance[0].status"))
.isEqualTo("UP");

String zipkinHostname = zipkin.defaultHostname();
int zipkinPort = zipkin.activePort().localAddress().getPort();

// Note: Netflix/Eureka says use hostname, which can conflict on laptops.
// Armeria adopts the spring-cloud-netflix convention shown here.
assertThat(readString(json, "$.application.instance[0].instanceId"))
.isEqualTo(zipkinHostname + ":zipkin:" + zipkinPort);

// Make sure the vip address is relevant
assertThat(readString(json, "$.application.instance[0].vipAddress"))
.isEqualTo(zipkinHostname + ":" + zipkinPort);
}

@Test @Order(2) void deregistersOnClose() {
zipkin.close();
await().untilAsserted( // wait for deregistration
() -> {
try (Response response = getEurekaZipkinApp()) {
assertThat(response.code()).isEqualTo(404);
}
});
}

private String getEurekaZipkinAppAsString() throws IOException {
try (Response response = getEurekaZipkinApp(); ResponseBody body = response.body()) {
assertThat(response.isSuccessful()).withFailMessage(response.toString()).isTrue();
return body != null ? body.string() : "";
}
}

private Response getEurekaZipkinApp() throws IOException {
HttpUrl url = serviceUrl.newBuilder().addEncodedPathSegments(APPS_ZIPKIN).build();
Request.Builder rb = new Request.Builder().url(url)
.header("Accept", "application/json"); // XML is default
if (!url.username().isEmpty()) {
rb.header("Authorization", basic(url.username(), url.password()));
}
return client.newCall(rb.build()).execute();
}

static final class EurekaContainer extends GenericContainer<EurekaContainer> {
static final Logger LOGGER = LoggerFactory.getLogger(EurekaContainer.class);
static final int EUREKA_PORT = 8761;

EurekaContainer(Map<String, String> env) {
super(parse("ghcr.io/openzipkin/zipkin-eureka:3.0.4"));
withEnv(env);
withExposedPorts(EUREKA_PORT);
waitStrategy = Wait.forHealthcheck();
withStartupTimeout(Duration.ofSeconds(60));
withLogConsumer(new Slf4jLogConsumer(LOGGER));
}

HttpUrl serviceUrl() {
return new HttpUrl.Builder()
.scheme("http")
.host(getHost()).port(getMappedPort(EUREKA_PORT))
.addEncodedPathSegments("eureka/v2").build();
}
}

static String readString(String json, String jsonPath) {
return JsonPath.compile(jsonPath).read(json);
}
}
Loading
Loading