Test Baseline (#9625)
Closes #9174 Signed-off-by: jonathan <jvilalop@redhat.com>
This commit is contained in:
parent
a1f2f77b82
commit
3fd725a3f5
11 changed files with 294 additions and 34 deletions
16
.github/workflows/operator-ci.yml
vendored
16
.github/workflows/operator-ci.yml
vendored
|
@ -34,6 +34,18 @@ jobs:
|
|||
run: |
|
||||
mvn clean install -DskipTests -DskipExamples -DskipTestsuite
|
||||
|
||||
- name: Build the Keycloak Operator
|
||||
- name: Setup Minikube-Kubernetes
|
||||
uses: manusa/actions-setup-minikube@v2.4.3
|
||||
with:
|
||||
minikube version: v1.24.0
|
||||
kubernetes version: v1.22.3
|
||||
github token: ${{ secrets.GITHUB_TOKEN }}
|
||||
driver: docker
|
||||
- name: Build , deploy and test operator in minikube
|
||||
run: |
|
||||
mvn clean package -nsu -B -e -pl operator -Doperator -Dquarkus.container-image.build=true -Dquarkus.kubernetes.deployment-target=minikube
|
||||
cd operator
|
||||
eval $(minikube -p minikube docker-env)
|
||||
mvn clean verify \
|
||||
-Dquarkus.container-image.build=true -Dquarkus.container-image.tag=test \
|
||||
-Dquarkus.kubernetes.deployment-target=kubernetes \
|
||||
--no-transfer-progress -Dtest.operator.deployment=remote
|
|
@ -32,11 +32,12 @@
|
|||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<quarkus.operator.sdk.version>3.0.0-SNAPSHOT</quarkus.operator.sdk.version>
|
||||
<quarkus.operator.sdk.version>3.0.2</quarkus.operator.sdk.version>
|
||||
<quarkus.version>2.6.1.Final</quarkus.version>
|
||||
<quarkus.container-image.group>keycloak</quarkus.container-image.group>
|
||||
<quarkus.jib.base-jvm-image>eclipse-temurin:11</quarkus.jib.base-jvm-image>
|
||||
<quarkus.kubernetes.image-pull-policy>Never</quarkus.kubernetes.image-pull-policy>
|
||||
<maven-failsafe-plugin.version>2.22.0</maven-failsafe-plugin.version>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
|
@ -119,6 +120,30 @@
|
|||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-test-common</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>${assertj-core.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<version>${awaitility.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
@ -222,7 +247,18 @@
|
|||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>${maven-failsafe-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>integration-test</goal>
|
||||
<goal>verify</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
|
|
@ -33,6 +33,6 @@ public final class Constants {
|
|||
);
|
||||
|
||||
public static final Map<String, String> DEFAULT_DIST_CONFIG = Map.of(
|
||||
"KEYCLOAK_METRICS_ENABLED", "true"
|
||||
"KC_METRICS_ENABLED", "true"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ package org.keycloak.operator.v2alpha1;
|
|||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.OwnerReference;
|
||||
import io.fabric8.kubernetes.api.model.apps.Deployment;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.informers.SharedIndexInformer;
|
||||
|
@ -30,9 +29,9 @@ import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
|
|||
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.RetryInfo;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
|
||||
import io.javaoperatorsdk.operator.processing.event.ResourceID;
|
||||
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
|
||||
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
|
||||
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;
|
||||
import io.quarkus.logging.Log;
|
||||
import org.keycloak.operator.Config;
|
||||
import org.keycloak.operator.Constants;
|
||||
|
@ -43,11 +42,11 @@ import org.keycloak.operator.v2alpha1.crds.KeycloakStatusBuilder;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER;
|
||||
import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_CURRENT_NAMESPACE;
|
||||
|
||||
@ControllerConfiguration(namespaces = WATCH_CURRENT_NAMESPACE)
|
||||
@ControllerConfiguration(namespaces = WATCH_CURRENT_NAMESPACE, finalizerName = NO_FINALIZER)
|
||||
public class KeycloakController implements Reconciler<Keycloak>, EventSourceInitializer<Keycloak>, ErrorStatusHandler<Keycloak> {
|
||||
|
||||
@Inject
|
||||
|
@ -59,19 +58,11 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
|||
@Override
|
||||
public List<EventSource> prepareEventSources(EventSourceContext<Keycloak> context) {
|
||||
SharedIndexInformer<Deployment> deploymentInformer =
|
||||
client.apps().deployments().inAnyNamespace()
|
||||
client.apps().deployments().inNamespace(context.getConfigurationService().getClientConfiguration().getNamespace())
|
||||
.withLabels(Constants.DEFAULT_LABELS)
|
||||
.runnableInformer(0);
|
||||
|
||||
EventSource deploymentEvent = new InformerEventSource<>(
|
||||
deploymentInformer, d -> {
|
||||
List<OwnerReference> ownerReferences = d.getMetadata().getOwnerReferences();
|
||||
if (!ownerReferences.isEmpty()) {
|
||||
return Set.of(new ResourceID(ownerReferences.get(0).getName(), d.getMetadata().getNamespace()));
|
||||
} else {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
});
|
||||
EventSource deploymentEvent = new InformerEventSource<>(deploymentInformer, Mappers.fromOwnerReference());
|
||||
|
||||
return List.of(deploymentEvent);
|
||||
}
|
||||
|
|
|
@ -22,12 +22,20 @@ import io.fabric8.kubernetes.model.annotation.Group;
|
|||
import io.fabric8.kubernetes.model.annotation.Plural;
|
||||
import io.fabric8.kubernetes.model.annotation.ShortNames;
|
||||
import io.fabric8.kubernetes.model.annotation.Version;
|
||||
import io.sundr.builder.annotations.Buildable;
|
||||
import io.sundr.builder.annotations.BuildableReference;
|
||||
import org.keycloak.operator.Constants;
|
||||
|
||||
@Group(Constants.CRDS_GROUP)
|
||||
@Version(Constants.CRDS_VERSION)
|
||||
@ShortNames(Constants.SHORT_NAME)
|
||||
@Plural(Constants.PLURAL_NAME)
|
||||
@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder",
|
||||
lazyCollectionInitEnabled = false, refs = {
|
||||
@BuildableReference(io.fabric8.kubernetes.api.model.ObjectMeta.class),
|
||||
@BuildableReference(io.fabric8.kubernetes.client.CustomResource.class),
|
||||
@BuildableReference(org.keycloak.operator.v2alpha1.crds.KeycloakSpec.class)
|
||||
})
|
||||
public class Keycloak extends CustomResource<KeycloakSpec, KeycloakStatus> implements Namespaced {
|
||||
|
||||
}
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
*/
|
||||
package org.keycloak.operator.v2alpha1.crds;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.PodTemplate;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class KeycloakSpec {
|
||||
|
|
|
@ -27,16 +27,19 @@ spec:
|
|||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- curl --head --fail --silent http://127.0.0.1:8080/health/live
|
||||
periodSeconds: 1
|
||||
httpGet:
|
||||
path: /health/live
|
||||
port: 8080
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 2
|
||||
failureThreshold: 10
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- curl --head --fail --silent http://127.0.0.1:8080/health/ready
|
||||
periodSeconds: 1
|
||||
failureThreshold: 180
|
||||
httpGet:
|
||||
path: /health/ready
|
||||
port: 8080
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 2
|
||||
failureThreshold: 10
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
terminationGracePeriodSeconds: 30
|
||||
|
|
|
@ -4,7 +4,7 @@ metadata:
|
|||
name: example-kc
|
||||
spec:
|
||||
instances: 1
|
||||
distConfig:
|
||||
serverConfiguration:
|
||||
KC_DB: postgres
|
||||
KC_DB_URL_HOST: postgres-db
|
||||
# KC_DB_USERNAME: ${secret:keycloak-db-secret:username}
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
package org.keycloak.operator;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.HasMetadata;
|
||||
import io.fabric8.kubernetes.api.model.NamespaceBuilder;
|
||||
import io.fabric8.kubernetes.api.model.apps.Deployment;
|
||||
import io.fabric8.kubernetes.client.Config;
|
||||
import io.fabric8.kubernetes.client.ConfigBuilder;
|
||||
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.javaoperatorsdk.operator.Operator;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
|
||||
import io.quarkiverse.operatorsdk.runtime.OperatorProducer;
|
||||
import io.quarkiverse.operatorsdk.runtime.QuarkusConfigurationService;
|
||||
import io.quarkus.logging.Log;
|
||||
import org.eclipse.microprofile.config.ConfigProvider;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
|
||||
import javax.enterprise.inject.Instance;
|
||||
import javax.enterprise.inject.spi.CDI;
|
||||
import javax.enterprise.util.TypeLiteral;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public abstract class ClusterOperatorTest {
|
||||
|
||||
public static final String QUARKUS_KUBERNETES_DEPLOYMENT_TARGET = "quarkus.kubernetes.deployment-target";
|
||||
public static final String OPERATOR_DEPLOYMENT_PROP = "test.operator.deployment";
|
||||
public static final String TARGET_KUBERNETES_GENERATED_YML_FOLDER = "target/kubernetes/";
|
||||
|
||||
public enum OperatorDeployment {local,remote}
|
||||
|
||||
protected static OperatorDeployment operatorDeployment;
|
||||
protected static Instance<Reconciler<? extends HasMetadata>> reconcilers;
|
||||
protected static QuarkusConfigurationService configuration;
|
||||
protected static KubernetesClient k8sclient;
|
||||
protected static String namespace;
|
||||
protected static String deploymentTarget;
|
||||
private static Operator operator;
|
||||
|
||||
|
||||
@BeforeAll
|
||||
public static void before() throws FileNotFoundException {
|
||||
configuration = CDI.current().select(QuarkusConfigurationService.class).get();
|
||||
reconcilers = CDI.current().select(new TypeLiteral<>() {});
|
||||
operatorDeployment = ConfigProvider.getConfig().getOptionalValue(OPERATOR_DEPLOYMENT_PROP, OperatorDeployment.class).orElse(OperatorDeployment.local);
|
||||
deploymentTarget = ConfigProvider.getConfig().getOptionalValue(QUARKUS_KUBERNETES_DEPLOYMENT_TARGET, String.class).orElse("kubernetes");
|
||||
|
||||
calculateNamespace();
|
||||
createK8sClient();
|
||||
createNamespace();
|
||||
|
||||
if (operatorDeployment == OperatorDeployment.remote) {
|
||||
createCRD();
|
||||
createRBACresourcesAndOperatorDeployment();
|
||||
} else {
|
||||
createOperator();
|
||||
registerReconcilers();
|
||||
operator.start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void createK8sClient() {
|
||||
k8sclient = new DefaultKubernetesClient(new ConfigBuilder(Config.autoConfigure(null)).withNamespace(namespace).build());
|
||||
}
|
||||
|
||||
private static void createRBACresourcesAndOperatorDeployment() throws FileNotFoundException {
|
||||
Log.info("Creating RBAC into Namespace " + namespace);
|
||||
List<HasMetadata> hasMetadata = k8sclient.load(new FileInputStream(TARGET_KUBERNETES_GENERATED_YML_FOLDER + deploymentTarget + ".yml"))
|
||||
.inNamespace(namespace).get();
|
||||
hasMetadata.stream()
|
||||
.map(b -> {
|
||||
if ("Deployment".equalsIgnoreCase(b.getKind()) && b.getMetadata().getName().contains("operator")) {
|
||||
((Deployment) b).getSpec().getTemplate().getSpec().getContainers().get(0).setImagePullPolicy("Never");
|
||||
}
|
||||
return b;
|
||||
}).forEach(c -> {
|
||||
Log.info("processing part : " + c.getKind() + "--" + c.getMetadata().getName() + " -- " + namespace);
|
||||
k8sclient.resource(c).inNamespace(namespace).createOrReplace();
|
||||
});
|
||||
}
|
||||
|
||||
private static void cleanRBACresourcesAndOperatorDeployment() throws FileNotFoundException {
|
||||
Log.info("Deleting RBAC from Namespace " + namespace);
|
||||
k8sclient.load(new FileInputStream(TARGET_KUBERNETES_GENERATED_YML_FOLDER +deploymentTarget+".yml"))
|
||||
.inNamespace(namespace).delete();
|
||||
}
|
||||
private static void createCRD() throws FileNotFoundException {
|
||||
Log.info("Creating CRD ");
|
||||
k8sclient.load(new FileInputStream(TARGET_KUBERNETES_GENERATED_YML_FOLDER + "keycloaks.keycloak.org-v1.yml")).createOrReplace();
|
||||
}
|
||||
|
||||
private static void registerReconcilers() {
|
||||
Log.info("Registering reconcilers for operator : " + operator + " [" + operatorDeployment + "]");
|
||||
|
||||
for (Reconciler reconciler : reconcilers) {
|
||||
final var config = configuration.getConfigurationFor(reconciler);
|
||||
if (!config.isRegistrationDelayed()) {
|
||||
Log.info("Register and apply : " + reconciler.getClass().getName());
|
||||
OperatorProducer.applyCRDIfNeededAndRegister(operator, reconciler, configuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void createOperator() {
|
||||
operator = new Operator(k8sclient, configuration);
|
||||
operator.getConfigurationService().getClientConfiguration().setNamespace(namespace);
|
||||
}
|
||||
|
||||
private static void createNamespace() {
|
||||
Log.info("Creating Namespace " + namespace);
|
||||
k8sclient.namespaces().create(new NamespaceBuilder().withNewMetadata().withName(namespace).endMetadata().build());
|
||||
}
|
||||
|
||||
private static void calculateNamespace() {
|
||||
namespace = "keycloak-test-" + UUID.randomUUID();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void after() throws FileNotFoundException {
|
||||
|
||||
if (operatorDeployment == OperatorDeployment.local) {
|
||||
Log.info("Stopping Operator");
|
||||
operator.stop();
|
||||
|
||||
Log.info("Creating new K8s Client");
|
||||
// create a new client bc operator has closed the old one
|
||||
createK8sClient();
|
||||
} else {
|
||||
cleanRBACresourcesAndOperatorDeployment();
|
||||
}
|
||||
|
||||
Log.info("Deleting namespace : " + namespace);
|
||||
assertThat(k8sclient.namespaces().withName(namespace).delete()).isTrue();
|
||||
k8sclient.close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package org.keycloak.operator;
|
||||
|
||||
import io.quarkus.logging.Log;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.awaitility.core.ConditionTimeoutException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@QuarkusTest
|
||||
public class OperatorE2EIT extends ClusterOperatorTest {
|
||||
@Test
|
||||
public void given_ClusterAndOperatorRunning_when_KeycloakCRCreated_Then_KeycloakStructureIsDeployedAndStatusIsOK() throws IOException {
|
||||
Log.info(((operatorDeployment == OperatorDeployment.remote) ? "Remote " : "Local ") + "Run Test :" + namespace);
|
||||
|
||||
// DB
|
||||
Log.info("Creating new PostgreSQL deployment");
|
||||
k8sclient.load(OperatorE2EIT.class.getResourceAsStream("/example-postgres.yaml")).inNamespace(namespace).createOrReplace();
|
||||
|
||||
// Check DB has deployed and ready
|
||||
Log.info("Checking Postgres is running");
|
||||
Awaitility.await()
|
||||
.atMost(Duration.ofSeconds(60))
|
||||
.pollDelay(Duration.ofSeconds(2))
|
||||
.untilAsserted(() -> assertThat(k8sclient.apps().statefulSets().inNamespace(namespace).withName("postgresql-db").get().getStatus().getReadyReplicas()).isEqualTo(1));
|
||||
// CR
|
||||
Log.info("Creating new Keycloak CR example");
|
||||
k8sclient.load(OperatorE2EIT.class.getResourceAsStream("/example-keycloak.yml")).inNamespace(namespace).createOrReplace();
|
||||
|
||||
// Check Operator has deployed Keycloak
|
||||
Log.info("Checking Operator has deployed Keycloak deployment");
|
||||
Awaitility.await()
|
||||
.atMost(Duration.ofSeconds(60))
|
||||
.pollDelay(Duration.ofSeconds(2))
|
||||
.untilAsserted(() -> assertThat(k8sclient.apps().deployments().inNamespace(namespace).withName("example-kc").get()).isNotNull());
|
||||
|
||||
// Check Keycloak has status ready
|
||||
StringBuffer podlog = new StringBuffer();
|
||||
try {
|
||||
Log.info("Checking Keycloak pod has ready replicas == 1");
|
||||
Awaitility.await()
|
||||
.atMost(Duration.ofSeconds(180))
|
||||
.pollDelay(Duration.ofSeconds(5))
|
||||
.untilAsserted(() -> {
|
||||
podlog.delete(0, podlog.length());
|
||||
try {
|
||||
k8sclient.pods().inNamespace(namespace).list().getItems().stream()
|
||||
.filter(a -> a.getMetadata().getName().startsWith("example-kc"))
|
||||
.forEach(a -> podlog.append(a.getMetadata().getName()).append(" : ")
|
||||
.append(k8sclient.pods().inNamespace(namespace).withName(a.getMetadata().getName()).getLog(true)));
|
||||
} catch (Exception e) {
|
||||
// swallowing exception bc the pod is not ready to give logs yet
|
||||
}
|
||||
assertThat(k8sclient.apps().deployments().inNamespace(namespace).withName("example-kc").get().getStatus().getReadyReplicas()).isEqualTo(1);
|
||||
});
|
||||
} catch (ConditionTimeoutException e) {
|
||||
Log.error("On error POD LOG " + podlog, e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
2
pom.xml
2
pom.xml
|
@ -158,6 +158,8 @@
|
|||
<selenium.version>2.35.0</selenium.version>
|
||||
<xml-apis.version>1.4.01</xml-apis.version>
|
||||
<subethasmtp.version>3.1.7</subethasmtp.version>
|
||||
<awaitility.version>4.1.1</awaitility.version>
|
||||
<assertj-core.version>3.22.0</assertj-core.version>
|
||||
<!-- KEYCLOAK-17585 Prevent microprofile-metrics-api upgrades from version "2.3" due to:
|
||||
https://issues.redhat.com/browse/KEYCLOAK-17585?focusedCommentId=16002705&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-16002705
|
||||
-->
|
||||
|
|
Loading…
Reference in a new issue