Skip to content

Commit

Permalink
test(flagd): use newest testbed launchpad
Browse files Browse the repository at this point in the history
Signed-off-by: Simon Schrottner <[email protected]>
  • Loading branch information
aepfli committed Jan 21, 2025
1 parent b4fe2f4 commit 0d5079b
Show file tree
Hide file tree
Showing 9 changed files with 60 additions and 141 deletions.
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[submodule "providers/flagd/test-harness"]
path = providers/flagd/test-harness
url = https://github.com/open-feature/test-harness.git
branch = v1.1.1
branch = v1.3.2
[submodule "providers/flagd/spec"]
path = providers/flagd/spec
url = https://github.com/open-feature/spec.git
11 changes: 5 additions & 6 deletions providers/flagd/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>toxiproxy</artifactId>
<version>1.20.4</version>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
<!-- uncomment for logoutput during test runs -->

<dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.16</version>
Expand Down Expand Up @@ -254,7 +253,7 @@
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.68.2:exe:${os.detected.classifier}</pluginArtifact>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.69.1:exe:${os.detected.classifier}</pluginArtifact>
<protoSourceRoot>${project.basedir}/schemas/protobuf/</protoSourceRoot>
</configuration>
<executions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ void observeEventStream(final BlockingQueue<QueuePayload> writeTo, final AtomicB
metadataException = e;
}

log.info("stream");
while (!shutdown.get()) {
final GrpcResponseModel response = streamReceiver.take();
if (response.isComplete()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package dev.openfeature.contrib.providers.flagd.e2e;

import dev.openfeature.contrib.providers.flagd.Config;
import java.io.File;
import java.nio.file.Files;
import java.util.List;
import org.apache.logging.log4j.util.Strings;
import org.jetbrains.annotations.NotNull;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
Expand All @@ -28,16 +28,25 @@ public class FlagdContainer extends GenericContainer<FlagdContainer> {
private String feature;

public FlagdContainer() {
this("");
super(generateContainerName());
this.feature = feature;
this.addExposedPorts(8013, 8014, 8015, 8080);
}

public FlagdContainer(String feature) {
super(generateContainerName(feature));
this.withReuse(true);
this.feature = feature;
if (!"socket".equals(this.feature)) this.addExposedPorts(8013, 8014, 8015, 8016);
public int getPort(Config.Resolver resolver) {
switch (resolver) {
case RPC:
return getMappedPort(8013);
case IN_PROCESS:
return getMappedPort(8015);
default:
return 0;
}
}

public String launchpad() {
return this.getHost() + ":" + this.getMappedPort(8080);
}
/**
* @return a {@link org.testcontainers.containers.GenericContainer} instance of envoy container using
* flagd sync service as backend expose on port 9211
Expand All @@ -52,11 +61,8 @@ public static GenericContainer envoy() {
.withNetworkAliases("envoy");
}

public static @NotNull String generateContainerName(String feature) {
public static @NotNull String generateContainerName() {
String container = "ghcr.io/open-feature/flagd-testbed";
if (!Strings.isBlank(feature)) {
container += "-" + feature;
}
container += ":v" + version;
return container;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ public void a_stale_event_handler(String eventType) {

@When("a {} event was fired")
public void eventWasFired(String eventType) throws InterruptedException {
eventHandlerShouldBeExecutedWithin(eventType, 10000);
eventHandlerShouldBeExecutedWithin(eventType, 8000);
// we might be too fast in the execution
Thread.sleep(500);
Thread.sleep(100);
}

@Then("the {} event handler should have been executed")
public void eventHandlerShouldBeExecuted(String eventType) {
eventHandlerShouldBeExecutedWithin(eventType, 30000);
eventHandlerShouldBeExecutedWithin(eventType, 10000);
}

@Then("the {} event handler should have been executed within {int}ms")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package dev.openfeature.contrib.providers.flagd.e2e.steps;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

import dev.openfeature.contrib.providers.flagd.e2e.State;
import dev.openfeature.sdk.FlagEvaluationDetails;
Expand Down Expand Up @@ -70,15 +68,9 @@ public void the_variant_should_be(String variant) {
}

@Then("the flag should be part of the event payload")
@Then("the flag was modified")
public void the_flag_was_modified() {
await().atMost(5000, MILLISECONDS).until(() -> state.events.stream()
.anyMatch(event -> event.type.equals("change")
&& event.details.getFlagsChanged().contains(state.flag.name)));
state.lastEvent = state.events.stream()
.filter(event -> event.type.equals("change")
&& event.details.getFlagsChanged().contains(state.flag.name))
.findFirst();
Event event = state.lastEvent.orElseThrow(AssertionError::new);
assertThat(event.details.getFlagsChanged()).contains(state.flag.name);
}

public class Flag {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
package dev.openfeature.contrib.providers.flagd.e2e.steps;

import static io.restassured.RestAssured.when;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import dev.openfeature.contrib.providers.flagd.Config;
import dev.openfeature.contrib.providers.flagd.FlagdProvider;
import dev.openfeature.contrib.providers.flagd.e2e.FlagdContainer;
import dev.openfeature.contrib.providers.flagd.e2e.State;
import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.OpenFeatureAPI;
import eu.rekawek.toxiproxy.Proxy;
import eu.rekawek.toxiproxy.ToxiproxyClient;
import eu.rekawek.toxiproxy.model.ToxicDirection;
import io.cucumber.java.After;
import io.cucumber.java.AfterAll;
import io.cucumber.java.Before;
Expand All @@ -22,126 +20,59 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.parallel.Isolated;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.ToxiproxyContainer;
import org.testcontainers.shaded.org.apache.commons.io.FileUtils;

@Isolated()
@Slf4j
public class ProviderSteps extends AbstractSteps {

public static final int UNAVAILABLE_PORT = 9999;
static Map<ProviderType, FlagdContainer> containers = new HashMap<>();
public static Network network = Network.newNetwork();
public static ToxiproxyContainer toxiproxy =
new ToxiproxyContainer("ghcr.io/shopify/toxiproxy:2.5.0").withNetwork(network);
public static ToxiproxyClient toxiproxyClient;
static FlagdContainer container;

static Path sharedTempDir;

public ProviderSteps(State state) {
super(state);
}

static String generateProxyName(Config.Resolver resolver, ProviderType providerType) {
return providerType + "-" + resolver;
}

@BeforeAll
public static void beforeAll() throws IOException {
toxiproxy.start();
toxiproxyClient = new ToxiproxyClient(toxiproxy.getHost(), toxiproxy.getControlPort());
toxiproxyClient.createProxy(
generateProxyName(Config.Resolver.RPC, ProviderType.DEFAULT), "0.0.0.0:8666", "default:8013");

toxiproxyClient.createProxy(
generateProxyName(Config.Resolver.IN_PROCESS, ProviderType.DEFAULT), "0.0.0.0:8667", "default:8015");
toxiproxyClient.createProxy(
generateProxyName(Config.Resolver.RPC, ProviderType.SSL), "0.0.0.0:8668", "ssl:8013");
toxiproxyClient.createProxy(
generateProxyName(Config.Resolver.IN_PROCESS, ProviderType.SSL), "0.0.0.0:8669", "ssl:8015");

containers.put(
ProviderType.DEFAULT, new FlagdContainer().withNetwork(network).withNetworkAliases("default"));
containers.put(
ProviderType.SSL, new FlagdContainer("ssl").withNetwork(network).withNetworkAliases("ssl"));

sharedTempDir = Files.createDirectories(
Paths.get("tmp/" + RandomStringUtils.randomAlphanumeric(8).toLowerCase() + "/"));
containers.put(
ProviderType.SOCKET,
new FlagdContainer("socket")
.withFileSystemBind(sharedTempDir.toAbsolutePath().toString(), "/tmp", BindMode.READ_WRITE));
container = new FlagdContainer()
.withFileSystemBind(sharedTempDir.toAbsolutePath().toString(), "/tmp", BindMode.READ_WRITE);
;
}

@AfterAll
public static void afterAll() throws IOException {

containers.forEach((name, container) -> container.stop());
container.stop();
FileUtils.deleteDirectory(sharedTempDir.toFile());
toxiproxyClient.reset();
toxiproxy.stop();
}

@Before
public void before() throws IOException {
if (!container.isRunning()) {
container.start();
}

toxiproxyClient.getProxies().forEach(proxy -> {
try {
proxy.toxics().getAll().forEach(toxic -> {
try {
toxic.remove();
} catch (IOException e) {
log.debug("Failed to remove timout", e);
}
});
} catch (IOException e) {
log.debug("Failed to remove timout", e);
}
});

containers.values().stream()
.filter(containers -> !containers.isRunning())
.forEach(FlagdContainer::start);
}

@After
public void tearDown() {
OpenFeatureAPI.getInstance().shutdown();
}

public int getPort(Config.Resolver resolver, ProviderType providerType) {
switch (resolver) {
case RPC:
switch (providerType) {
case DEFAULT:
return toxiproxy.getMappedPort(8666);
case SSL:
return toxiproxy.getMappedPort(8668);
}
case IN_PROCESS:
switch (providerType) {
case DEFAULT:
return toxiproxy.getMappedPort(8667);
case SSL:
return toxiproxy.getMappedPort(8669);
}
default:
throw new IllegalArgumentException("Unsupported resolver: " + resolver);
}
}

@Given("a {} flagd provider")
public void setupProvider(String providerType) throws IOException {
state.builder.deadline(500).keepAlive(0).retryGracePeriod(3);
public void setupProvider(String providerType) throws IOException, InterruptedException {
String flagdConfig = "default";
state.builder.deadline(500).keepAlive(0).retryGracePeriod(1);
boolean wait = true;
switch (providerType) {
case "unavailable":
Expand All @@ -163,9 +94,10 @@ public void setupProvider(String providerType) throws IOException {
String absolutePath = file.getAbsolutePath();
this.state.providerType = ProviderType.SSL;
state.builder
.port(getPort(State.resolverType, state.providerType))
.port(container.getPort(State.resolverType))
.tls(true)
.certPath(absolutePath);
flagdConfig = "ssl";
break;
case "offline":
File flags = new File("test-harness/flags");
Expand All @@ -185,9 +117,15 @@ public void setupProvider(String providerType) throws IOException {

default:
this.state.providerType = ProviderType.DEFAULT;
state.builder.port(getPort(State.resolverType, state.providerType));
state.builder.port(container.getPort(State.resolverType));
break;
}
when().post("http://" + container.launchpad() + "/start?config={config}", flagdConfig)
.then()
.statusCode(200);

// giving flagd a little time to start
Thread.sleep(100);
FeatureProvider provider =
new FlagdProvider(state.builder.resolverType(State.resolverType).build());

Expand All @@ -203,30 +141,15 @@ public void setupProvider(String providerType) throws IOException {
@When("the connection is lost for {int}s")
public void the_connection_is_lost_for(int seconds) throws InterruptedException, IOException {
log.info("Timeout and wait for {} seconds", seconds);
String randomizer = RandomStringUtils.randomAlphanumeric(5);
String timeoutUpName = "restart-up-" + randomizer;
String timeoutDownName = "restart-down-" + randomizer;
Proxy proxy = toxiproxyClient.getProxy(generateProxyName(State.resolverType, state.providerType));
proxy.toxics().timeout(timeoutDownName, ToxicDirection.DOWNSTREAM, seconds);
proxy.toxics().timeout(timeoutUpName, ToxicDirection.UPSTREAM, seconds);

TimerTask task = new TimerTask() {
public void run() {
try {
proxy.toxics().get(timeoutUpName).remove();
proxy.toxics().get(timeoutDownName).remove();
} catch (IOException e) {
log.debug("Failed to remove timeout", e);
}
}
};
Timer restartTimer = new Timer("Timer" + randomizer);

restartTimer.schedule(task, seconds * 1000L);
when().post("http://" + container.launchpad() + "/restart?seconds={seconds}", seconds)
.then()
.statusCode(200);
}

static FlagdContainer getContainer(ProviderType providerType) {
log.info("getting container for {}", providerType);
return containers.getOrDefault(providerType, containers.get(ProviderType.DEFAULT));
@When("the flag was modified")
public void the_flag_was_modded() {

when().post("http://" + container.launchpad() + "/change").then().statusCode(200);
}
}
2 changes: 1 addition & 1 deletion providers/flagd/test-harness

0 comments on commit 0d5079b

Please sign in to comment.