diff --git a/examples/database-postgresql/pom.xml b/examples/database-postgresql/pom.xml index c6632a662..c8487a3cf 100644 --- a/examples/database-postgresql/pom.xml +++ b/examples/database-postgresql/pom.xml @@ -46,6 +46,11 @@ quarkus-test-openshift test + + io.quarkus.qe + quarkus-test-kubernetes + test + diff --git a/examples/database-postgresql/src/test/java/io/quarkus/qe/database/postgresql/KubernetesPostgresqlIT.java b/examples/database-postgresql/src/test/java/io/quarkus/qe/database/postgresql/KubernetesPostgresqlIT.java new file mode 100644 index 000000000..14d437632 --- /dev/null +++ b/examples/database-postgresql/src/test/java/io/quarkus/qe/database/postgresql/KubernetesPostgresqlIT.java @@ -0,0 +1,28 @@ +package io.quarkus.qe.database.postgresql; + +import io.quarkus.test.bootstrap.PostgresqlService; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.KubernetesScenario; +import io.quarkus.test.services.Container; +import io.quarkus.test.services.QuarkusApplication; + +@KubernetesScenario +public class KubernetesPostgresqlIT extends AbstractSqlDatabaseIT { + + private static final int POSTGRESQL_PORT = 5432; + + @Container(image = "${postgresql.image}", port = POSTGRESQL_PORT, expectedLog = "is ready") + static PostgresqlService database = new PostgresqlService() + .withProperty("PGDATA", "/tmp/psql"); + + @QuarkusApplication + static RestService app = new RestService() + .withProperty("quarkus.datasource.username", database.getUser()) + .withProperty("quarkus.datasource.password", database.getPassword()) + .withProperty("quarkus.datasource.jdbc.url", database::getJdbcUrl); + + @Override + protected RestService getApp() { + return app; + } +} diff --git a/examples/pingpong/src/test/java/io/quarkus/qe/KubernetesUsingExtensionPingPongResourceIT.java b/examples/pingpong/src/test/java/io/quarkus/qe/KubernetesUsingExtensionPingPongResourceIT.java index 7de45a394..f76ebd9d9 100644 --- a/examples/pingpong/src/test/java/io/quarkus/qe/KubernetesUsingExtensionPingPongResourceIT.java +++ b/examples/pingpong/src/test/java/io/quarkus/qe/KubernetesUsingExtensionPingPongResourceIT.java @@ -11,8 +11,7 @@ public class KubernetesUsingExtensionPingPongResourceIT { @QuarkusApplication - static final RestService pingpong = new RestService() - .withProperty("quarkus.kubernetes.service-type", "LoadBalancer"); + static final RestService pingpong = new RestService(); @Test public void shouldPingPongIsUpAndRunning() { diff --git a/pom.xml b/pom.xml index 83c50e08c..a0be16f39 100644 --- a/pom.xml +++ b/pom.xml @@ -501,6 +501,23 @@ **/Kubernetes*IT.java no + + + + maven-failsafe-plugin + + + + + + true + + + + + + + validate-format diff --git a/quarkus-test-kubernetes/pom.xml b/quarkus-test-kubernetes/pom.xml index ce2c3d075..620de64c4 100644 --- a/quarkus-test-kubernetes/pom.xml +++ b/quarkus-test-kubernetes/pom.xml @@ -19,7 +19,14 @@ io.fabric8 - openshift-client + kubernetes-client + + + + com.squareup.okhttp3 + okhttp + + io.rest-assured diff --git a/quarkus-test-kubernetes/src/main/java/io/quarkus/test/bootstrap/inject/KubectlClient.java b/quarkus-test-kubernetes/src/main/java/io/quarkus/test/bootstrap/inject/KubectlClient.java index 7243229ac..ae6e38bca 100644 --- a/quarkus-test-kubernetes/src/main/java/io/quarkus/test/bootstrap/inject/KubectlClient.java +++ b/quarkus-test-kubernetes/src/main/java/io/quarkus/test/bootstrap/inject/KubectlClient.java @@ -12,8 +12,9 @@ import static org.junit.jupiter.api.Assertions.fail; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; @@ -24,6 +25,7 @@ import java.util.Objects; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; import java.util.function.UnaryOperator; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -40,11 +42,13 @@ import io.fabric8.kubernetes.api.model.VolumeMount; import io.fabric8.kubernetes.api.model.VolumeMountBuilder; 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.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.dsl.NamespaceListVisitFromServerGetDeleteRecreateWaitApplicable; +import io.fabric8.kubernetes.client.dsl.NonDeletingOperation; +import io.fabric8.kubernetes.client.impl.KubernetesClientImpl; import io.fabric8.kubernetes.client.utils.Serialization; -import io.fabric8.openshift.client.DefaultOpenShiftClient; -import io.fabric8.openshift.client.NamespacedOpenShiftClient; -import io.fabric8.openshift.client.OpenShiftConfig; -import io.fabric8.openshift.client.OpenShiftConfigBuilder; import io.quarkus.test.bootstrap.Service; import io.quarkus.test.configuration.PropertyLookup; import io.quarkus.test.logging.Log; @@ -58,29 +62,37 @@ public final class KubectlClient { public static final String LABEL_SCENARIO_ID = "scenarioId"; public static final PropertyLookup ENABLED_EPHEMERAL_NAMESPACES = new PropertyLookup( "ts.kubernetes.ephemeral.namespaces.enabled", Boolean.TRUE.toString()); - private static final String RESOURCE_MNT_FOLDER = "/resource"; private static final int NAMESPACE_NAME_SIZE = 10; private static final int NAMESPACE_CREATION_RETRIES = 5; + private static final int DEPLOYMENT_CREATION_TIMEOUT = 30; + private static final String KUBECTL = "kubectl"; private static final int HTTP_PORT_DEFAULT = 80; - private final String currentNamespace; - private final DefaultOpenShiftClient masterClient; - private final NamespacedOpenShiftClient client; + private final KubernetesClientImpl client; private final String scenarioId; private KubectlClient(String scenarioUniqueName) { this.scenarioId = scenarioUniqueName; - String activeNamespace = new DefaultOpenShiftClient().getNamespace(); - currentNamespace = ENABLED_EPHEMERAL_NAMESPACES.getAsBoolean() ? createNamespace() : activeNamespace; - OpenShiftConfig config = new OpenShiftConfigBuilder().withTrustCerts(true).withNamespace(currentNamespace).build(); - masterClient = new DefaultOpenShiftClient(config); - client = masterClient.inNamespace(currentNamespace); + if (ENABLED_EPHEMERAL_NAMESPACES.getAsBoolean()) { + currentNamespace = createNamespace(); + Config config = new ConfigBuilder().withTrustCerts(true).withNamespace(currentNamespace).build(); + client = createClient(config); + } else { + Config config = new ConfigBuilder().withTrustCerts(true).build(); + client = createClient(config); + currentNamespace = client.getNamespace(); + } setCurrentSessionNamespace(currentNamespace); } + private static KubernetesClientImpl createClient(Config config) { + return new KubernetesClientBuilder().withConfig(config).build() + .adapt(KubernetesClientImpl.class); + } + public static KubectlClient create(String scenarioName) { return new KubectlClient(scenarioName); } @@ -121,7 +133,8 @@ public void applyServicePropertiesUsingDeploymentConfig(Service service) { envVar -> container.getEnv().add(new EnvVar(envVar.getKey(), envVar.getValue(), null))); }); - client.apps().deployments().createOrReplace(deployment); + client.apps().deployments().withTimeout(DEPLOYMENT_CREATION_TIMEOUT, TimeUnit.SECONDS).delete(); + client.apps().deployments().resource(deployment).create(); } /** @@ -152,7 +165,7 @@ public void applyServiceProperties(Service service, String file, UnaryOperator logs(Service service) { } /** - * Resolve the url by the service. - * - * @param service - * @return + * Get node host IP. */ - public String host(Service service) { - String serviceName = service.getName(); - io.fabric8.kubernetes.api.model.Service serviceModel = client.services().withName(serviceName).get(); - if (serviceModel == null - || serviceModel.getStatus() == null - || serviceModel.getStatus().getLoadBalancer() == null - || serviceModel.getStatus().getLoadBalancer().getIngress() == null) { - printServiceInfo(service); - fail("Service " + serviceName + " not found"); - } - - // IP detection rules: - // 1.- Try Ingress IP - // 2.- Try Ingress Hostname - Optional ip = serviceModel.getStatus().getLoadBalancer().getIngress().stream() - .map(ingress -> StringUtils.defaultIfBlank(ingress.getIp(), ingress.getHostname())) - .filter(StringUtils::isNotEmpty) - .findFirst(); - - if (ip.isEmpty()) { - printServiceInfo(service); - fail("Service " + serviceName + " host not found"); + public String host() { + String nodeURL = client.network().getConfiguration().getMasterUrl(); + try { + URI uri = new URI(nodeURL); + return uri.getHost(); + } catch (URISyntaxException e) { + throw new IllegalStateException(e); } - - return ip.get(); } /** @@ -260,7 +253,7 @@ public int port(Service service) { } return serviceModel.getSpec().getPorts().stream() - .map(ServicePort::getPort) + .map(ServicePort::getNodePort) .filter(Objects::nonNull) .findFirst() .orElse(HTTP_PORT_DEFAULT); @@ -276,7 +269,7 @@ public void deleteNamespace() { } catch (Exception e) { fail("Project failed to be deleted. Caused by " + e.getMessage()); } finally { - masterClient.close(); + client.close(); } } else { deleteResourcesByLabel(LABEL_SCENARIO_ID, getScenarioId()); @@ -297,7 +290,7 @@ private void deleteResourcesByLabel(String labelName, String labelValue) { } catch (Exception e) { fail("Project failed to be deleted. Caused by " + e.getMessage()); } finally { - masterClient.close(); + client.close(); } } @@ -306,13 +299,14 @@ private boolean isPodRunning(Pod pod) { } private List loadYaml(String template) { - return client.load(new ByteArrayInputStream(template.getBytes())).get(); + NamespaceListVisitFromServerGetDeleteRecreateWaitApplicable load = client + .load(new ByteArrayInputStream(template.getBytes())); + return load.items(); } private String enrichTemplate(Service service, String template, Map extraTemplateProperties) { - List objs = loadYaml(template); - for (HasMetadata obj : objs) { - // set namespace + List objects = loadYaml(template); + for (HasMetadata obj : objects) { obj.getMetadata().setNamespace(namespace()); Map objMetadataLabels = Optional.ofNullable(obj.getMetadata().getLabels()) .orElse(new HashMap<>()); @@ -321,23 +315,23 @@ private String enrichTemplate(Service service, String template, Map templateMetadataLabels = d.getSpec().getTemplate().getMetadata().getLabels(); + Map templateMetadataLabels = deployment.getSpec().getTemplate().getMetadata().getLabels(); templateMetadataLabels.put(LABEL_TO_WATCH_FOR_LOGS, service.getName()); templateMetadataLabels.put(LABEL_SCENARIO_ID, getScenarioId()); // add env var properties - Map enrichProperties = enrichProperties(service.getProperties(), d); + Map enrichProperties = enrichProperties(service.getProperties(), deployment); enrichProperties.putAll(extraTemplateProperties); - d.getSpec().getTemplate().getSpec().getContainers() + deployment.getSpec().getTemplate().getSpec().getContainers() .forEach(container -> enrichProperties.entrySet().forEach(property -> { String key = property.getKey(); EnvVar envVar = getEnvVarByKey(key, container); @@ -351,16 +345,8 @@ private String enrichTemplate(Service service, String template, Map "\"" + cmd + "\"").collect(Collectors.joining(", ")); return content.replaceAll(quote("${IMAGE}"), model.getImage()) .replaceAll(quote("${SERVICE_NAME}"), model.getContext().getName()) .replaceAll(quote("${INTERNAL_PORT}"), "" + model.getPort()) - .replaceAll(quote("${ARGS}"), String.join(" ", model.getCommand())); + .replaceAll(quote("${ARGS}"), args); } private boolean useInternalServiceAsUrl() { diff --git a/quarkus-test-kubernetes/src/main/java/io/quarkus/test/services/quarkus/ContainerRegistryKubernetesQuarkusApplicationManagedResource.java b/quarkus-test-kubernetes/src/main/java/io/quarkus/test/services/quarkus/ContainerRegistryKubernetesQuarkusApplicationManagedResource.java index 21fc73004..e5cd32edf 100644 --- a/quarkus-test-kubernetes/src/main/java/io/quarkus/test/services/quarkus/ContainerRegistryKubernetesQuarkusApplicationManagedResource.java +++ b/quarkus-test-kubernetes/src/main/java/io/quarkus/test/services/quarkus/ContainerRegistryKubernetesQuarkusApplicationManagedResource.java @@ -64,7 +64,6 @@ private String replaceDeploymentContent(String content) { return content .replaceAll(quote("${IMAGE}"), image) .replaceAll(quote("${SERVICE_NAME}"), model.getContext().getName()) - .replaceAll(quote("${ARTIFACT}"), model.getArtifact().getFileName().toString()) .replaceAll(quote("${INTERNAL_PORT}"), model.getContext().getOwner().getProperty(QUARKUS_HTTP_PORT_PROPERTY, "" + HTTP_PORT_DEFAULT)); } diff --git a/quarkus-test-kubernetes/src/main/java/io/quarkus/test/services/quarkus/ExtensionKubernetesQuarkusApplicationManagedResource.java b/quarkus-test-kubernetes/src/main/java/io/quarkus/test/services/quarkus/ExtensionKubernetesQuarkusApplicationManagedResource.java index f4fe0edcb..76a5f1dd1 100644 --- a/quarkus-test-kubernetes/src/main/java/io/quarkus/test/services/quarkus/ExtensionKubernetesQuarkusApplicationManagedResource.java +++ b/quarkus-test-kubernetes/src/main/java/io/quarkus/test/services/quarkus/ExtensionKubernetesQuarkusApplicationManagedResource.java @@ -34,7 +34,7 @@ public class ExtensionKubernetesQuarkusApplicationManagedResource private static final String USING_EXTENSION_PROFILE = "-Pdeploy-to-kubernetes-using-extension"; private static final String QUARKUS_PLUGIN_DEPLOY = "-Dquarkus.kubernetes.deploy=true"; - private static final String QUARKUS_PLUGIN_INGRESS_EXPOSE = "-Dquarkus.kubernetes.ingress.expose=true"; + private static final String USING_SERVICE_TYPE_NODE_PORT = "-Dquarkus.kubernetes.service-type=NodePort"; private static final String QUARKUS_CONTAINER_NAME = "quarkus.application.name"; private static final String QUARKUS_CONTAINER_IMAGE_REGISTRY = "quarkus.container-image.registry"; private static final String QUARKUS_CONTAINER_IMAGE_GROUP = "quarkus.container-image.group"; @@ -100,7 +100,7 @@ private void deployProjectUsingMavenCommand() { List args = mvnCommand(model.getContext()); args.addAll(Arrays.asList(USING_EXTENSION_PROFILE, BATCH_MODE, DISPLAY_VERSION, PACKAGE_GOAL, - QUARKUS_PLUGIN_DEPLOY, QUARKUS_PLUGIN_INGRESS_EXPOSE, + QUARKUS_PLUGIN_DEPLOY, USING_SERVICE_TYPE_NODE_PORT, SKIP_TESTS, SKIP_ITS, SKIP_CHECKSTYLE, ENSURE_QUARKUS_BUILD)); propagateContainerRegistryIfSet(args); args.add(withContainerName()); diff --git a/quarkus-test-kubernetes/src/main/java/io/quarkus/test/services/quarkus/KubernetesQuarkusApplicationManagedResource.java b/quarkus-test-kubernetes/src/main/java/io/quarkus/test/services/quarkus/KubernetesQuarkusApplicationManagedResource.java index 2d1431839..0f250fdcd 100644 --- a/quarkus-test-kubernetes/src/main/java/io/quarkus/test/services/quarkus/KubernetesQuarkusApplicationManagedResource.java +++ b/quarkus-test-kubernetes/src/main/java/io/quarkus/test/services/quarkus/KubernetesQuarkusApplicationManagedResource.java @@ -71,13 +71,9 @@ public void stop() { @Override public URILike getURI(Protocol protocol) { - if (protocol == Protocol.HTTPS) { - fail("SSL is not supported for Kubernetes tests yet"); - } else if (protocol == Protocol.GRPC) { - fail("gRPC is not supported for Kubernetes tests yet"); - } + validateProtocol(protocol); return createURI(protocol.getValue(), - client.host(model.getContext().getOwner()), + client.host(), client.port(model.getContext().getOwner())); } diff --git a/quarkus-test-kubernetes/src/main/resources/quarkus-app-kubernetes-template.yml b/quarkus-test-kubernetes/src/main/resources/quarkus-app-kubernetes-template.yml index 723b2cbfa..4cd4246e0 100644 --- a/quarkus-test-kubernetes/src/main/resources/quarkus-app-kubernetes-template.yml +++ b/quarkus-test-kubernetes/src/main/resources/quarkus-app-kubernetes-template.yml @@ -2,40 +2,46 @@ apiVersion: "v1" kind: "List" items: -- apiVersion: "v1" - kind: "Service" - metadata: - name: "${SERVICE_NAME}" - spec: - type: LoadBalancer - ports: - - port: ${INTERNAL_PORT} - selector: - app.kubernetes.io/name: "${SERVICE_NAME}" -- apiVersion: "apps/v1" - kind: "Deployment" - metadata: - labels: - app.kubernetes.io/runtime: "quarkus" - app.kubernetes.io/name: "${SERVICE_NAME}" - name: "${SERVICE_NAME}" - spec: - replicas: 1 - selector: - matchLabels: + - apiVersion: "v1" + kind: "Service" + metadata: + name: "${SERVICE_NAME}" + labels: + app.kubernetes.io/runtime: "quarkus" app.kubernetes.io/name: "${SERVICE_NAME}" - template: - metadata: - labels: - app.kubernetes.io/runtime: "quarkus" + spec: + type: NodePort + ports: + - name: "http" + port: ${INTERNAL_PORT} + targetPort: 8080 + protocol: TCP + selector: + app.kubernetes.io/name: "${SERVICE_NAME}" + + - apiVersion: "apps/v1" + kind: "Deployment" + metadata: + labels: + app.kubernetes.io/runtime: "quarkus" + app.kubernetes.io/name: "${SERVICE_NAME}" + name: "${SERVICE_NAME}" + spec: + replicas: 1 + selector: + matchLabels: app.kubernetes.io/name: "${SERVICE_NAME}" - deployment: "${SERVICE_NAME}" - spec: - containers: - - image: "${IMAGE}" - name: "${SERVICE_NAME}" - ports: - - containerPort: ${INTERNAL_PORT} - name: "http" - protocol: "TCP" - \ No newline at end of file + template: + metadata: + labels: + app.kubernetes.io/runtime: "quarkus" + app.kubernetes.io/name: "${SERVICE_NAME}" + deployment: "${SERVICE_NAME}" + spec: + containers: + - image: "${IMAGE}" + name: "${SERVICE_NAME}" + ports: + - containerPort: ${INTERNAL_PORT} + name: "http" + protocol: "TCP" diff --git a/quarkus-test-service-keycloak/pom.xml b/quarkus-test-service-keycloak/pom.xml index ea47c636e..51be3d7ca 100644 --- a/quarkus-test-service-keycloak/pom.xml +++ b/quarkus-test-service-keycloak/pom.xml @@ -27,5 +27,11 @@ true provided + + io.quarkus.qe + quarkus-test-kubernetes + true + provided + diff --git a/quarkus-test-service-keycloak/src/main/java/io/quarkus/test/services/containers/KubernetesKeycloakContainerManagedResource.java b/quarkus-test-service-keycloak/src/main/java/io/quarkus/test/services/containers/KubernetesKeycloakContainerManagedResource.java new file mode 100644 index 000000000..80ae03fbf --- /dev/null +++ b/quarkus-test-service-keycloak/src/main/java/io/quarkus/test/services/containers/KubernetesKeycloakContainerManagedResource.java @@ -0,0 +1,11 @@ +package io.quarkus.test.services.containers; + +public class KubernetesKeycloakContainerManagedResource extends KubernetesContainerManagedResource { + + private final KeycloakContainerManagedResourceBuilder model; + + protected KubernetesKeycloakContainerManagedResource(KeycloakContainerManagedResourceBuilder model) { + super(model); + this.model = model; + } +} diff --git a/quarkus-test-service-keycloak/src/main/java/io/quarkus/test/services/containers/KubernetesKeycloakContainerManagedResourceBinding.java b/quarkus-test-service-keycloak/src/main/java/io/quarkus/test/services/containers/KubernetesKeycloakContainerManagedResourceBinding.java new file mode 100644 index 000000000..524659e62 --- /dev/null +++ b/quarkus-test-service-keycloak/src/main/java/io/quarkus/test/services/containers/KubernetesKeycloakContainerManagedResourceBinding.java @@ -0,0 +1,16 @@ +package io.quarkus.test.services.containers; + +import io.quarkus.test.bootstrap.ManagedResource; +import io.quarkus.test.scenarios.KubernetesScenario; + +public class KubernetesKeycloakContainerManagedResourceBinding implements KeycloakContainerManagedResourceBinding { + @Override + public boolean appliesFor(KeycloakContainerManagedResourceBuilder builder) { + return builder.getContext().getTestContext().getRequiredTestClass().isAnnotationPresent(KubernetesScenario.class); + } + + @Override + public ManagedResource init(KeycloakContainerManagedResourceBuilder builder) { + return new KubernetesKeycloakContainerManagedResource(builder); + } +} diff --git a/quarkus-test-service-keycloak/src/main/resources/META-INF/services/io.quarkus.test.services.containers.KeycloakContainerManagedResourceBinding b/quarkus-test-service-keycloak/src/main/resources/META-INF/services/io.quarkus.test.services.containers.KeycloakContainerManagedResourceBinding index 2f62a1c0f..814b2ddf2 100644 --- a/quarkus-test-service-keycloak/src/main/resources/META-INF/services/io.quarkus.test.services.containers.KeycloakContainerManagedResourceBinding +++ b/quarkus-test-service-keycloak/src/main/resources/META-INF/services/io.quarkus.test.services.containers.KeycloakContainerManagedResourceBinding @@ -1 +1,2 @@ -io.quarkus.test.services.containers.OpenShiftKeycloakContainerManagedResourceBinding \ No newline at end of file +io.quarkus.test.services.containers.OpenShiftKeycloakContainerManagedResourceBinding +io.quarkus.test.services.containers.KubernetesKeycloakContainerManagedResourceBinding \ No newline at end of file