diff --git a/deployment/pom.xml b/deployment/pom.xml index ac16724..2758c2b 100644 --- a/deployment/pom.xml +++ b/deployment/pom.xml @@ -38,6 +38,11 @@ quarkus-tekton ${project.version} + + io.quarkus + quarkus-kubernetes-spi + ${project.version} + io.quarkus quarkus-junit5-internal diff --git a/deployment/src/main/java/io/quarkiverse/tekton/deployment/TektonConfiguration.java b/deployment/src/main/java/io/quarkiverse/tekton/deployment/TektonConfiguration.java index c589e8e..175515e 100644 --- a/deployment/src/main/java/io/quarkiverse/tekton/deployment/TektonConfiguration.java +++ b/deployment/src/main/java/io/quarkiverse/tekton/deployment/TektonConfiguration.java @@ -6,8 +6,8 @@ import io.smallrye.config.ConfigMapping; import io.smallrye.config.WithDefault; -@ConfigRoot(phase = BUILD_TIME) @ConfigMapping(prefix = "quarkus.tekton") +@ConfigRoot(phase = BUILD_TIME) public interface TektonConfiguration { /** @@ -21,6 +21,5 @@ interface Generation { */ @WithDefault("true") boolean enabled(); - } } diff --git a/deployment/src/main/java/io/quarkiverse/tekton/deployment/TektonProcessor.java b/deployment/src/main/java/io/quarkiverse/tekton/deployment/TektonProcessor.java index 1409b4a..f726926 100644 --- a/deployment/src/main/java/io/quarkiverse/tekton/deployment/TektonProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/tekton/deployment/TektonProcessor.java @@ -24,9 +24,9 @@ import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.kubernetes.spi.GeneratedKubernetesResourceBuildItem; -class TektonProcessor { +public class TektonProcessor { - private static final String FEATURE = "tekton"; + public static final String FEATURE = "tekton"; @BuildStep FeatureBuildItem feature() { diff --git a/deployment/src/main/java/io/quarkiverse/tekton/deployment/devservices/TektonDevServiceConfig.java b/deployment/src/main/java/io/quarkiverse/tekton/deployment/devservices/TektonDevServiceConfig.java new file mode 100644 index 0000000..79d888d --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/tekton/deployment/devservices/TektonDevServiceConfig.java @@ -0,0 +1,70 @@ +package io.quarkiverse.tekton.deployment.devservices; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@ConfigMapping(prefix = "quarkus.tekton.devservices") +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public interface TektonDevServiceConfig { + + /** + * Enable the Tekton DevService. + */ + @WithDefault("true") + boolean enabled(); + + /** + * Enable the debugging level. + */ + @WithDefault("false") + boolean debugEnabled(); + + /** + * If logs should be shown from the Tekton container. + */ + @WithDefault("false") + boolean showLogs(); + + /** + * The version of Tekton to be installed from the GitHub repository + * and which corresponds to a tagged release expressed as such: "v0.68.0" + */ + @WithDefault("v0.68.0") + String version(); + + /** + * The Tekton controller namespace where Tekton stuffs are deployed + * The default namespace is: tekton-pipelines + */ + @WithDefault("tekton-pipelines") + String controllerNamespace(); + + /** + * Time to wait till a resource is ready: pod, etc + * The default value is: 180 seconds + */ + @WithDefault("360") + long timeOut(); + + /** + * The cluster type to be used: kind or k3 + * The default value is: kind + */ + @WithDefault("kind") + String clusterType(); + + /** + * The hostname of the tekton ingress route + */ + @WithDefault("tekton.localtest.me") + String hostName(); + + /** + * The host port to be used on the host machine to access the dashboard + */ + @WithDefault("9097") + String hostPort(); + +} \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/tekton/deployment/devservices/TektonDevServiceInfoBuildItem.java b/deployment/src/main/java/io/quarkiverse/tekton/deployment/devservices/TektonDevServiceInfoBuildItem.java new file mode 100644 index 0000000..74e610a --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/tekton/deployment/devservices/TektonDevServiceInfoBuildItem.java @@ -0,0 +1,25 @@ +package io.quarkiverse.tekton.deployment.devservices; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * A build item that represents the information required to connect to an Tekton dev service. + */ +public final class TektonDevServiceInfoBuildItem extends SimpleBuildItem { + + private final String hostName; + private final int hostPort; + + public TektonDevServiceInfoBuildItem(String hostName, int hostPort) { + this.hostName = hostName; + this.hostPort = hostPort; + } + + public int hostPort() { + return hostPort; + } + + public String host() { + return hostName; + } +} diff --git a/deployment/src/main/java/io/quarkiverse/tekton/deployment/devservices/TektonExtensionProcessor.java b/deployment/src/main/java/io/quarkiverse/tekton/deployment/devservices/TektonExtensionProcessor.java new file mode 100644 index 0000000..134f019 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/tekton/deployment/devservices/TektonExtensionProcessor.java @@ -0,0 +1,221 @@ +package io.quarkiverse.tekton.deployment.devservices; + +import static io.quarkiverse.tekton.deployment.devservices.Utils.*; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.Closeable; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.jboss.logging.Logger; +import org.testcontainers.containers.GenericContainer; + +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.networking.v1.Ingress; +import io.fabric8.kubernetes.api.model.networking.v1.IngressBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.LocalPortForward; +import io.fabric8.kubernetes.client.internal.KubeConfigUtils; +import io.quarkiverse.tekton.deployment.TektonProcessor; +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.builditem.DevServicesResultBuildItem; +import io.quarkus.deployment.dev.devservices.DevServicesConfig; +import io.quarkus.devservices.IngressDevServiceConfig; +import io.quarkus.devservices.common.ContainerShutdownCloseable; +import io.quarkus.kubernetes.client.spi.KubernetesDevServiceInfoBuildItem; +import io.quarkus.kubernetes.client.spi.KubernetesDevServiceRequestBuildItem; + +@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { DevServicesConfig.Enabled.class }) +public class TektonExtensionProcessor { + private static final Logger LOG = Logger.getLogger(TektonExtensionProcessor.class); + + private static final String TEKTON_DASHBOARD_NAME = "tekton-dashboard"; + private static final String TEKTON_CONTROLLER_NAME = "tekton-pipelines"; + + static volatile DevServicesResultBuildItem.RunningDevService devService; + + @BuildStep(onlyIfNot = IsNormal.class, onlyIf = { DevServicesConfig.Enabled.class }) + void requestKube(TektonDevServiceConfig config, + BuildProducer kubeDevServiceRequest) { + if (config.enabled()) { + kubeDevServiceRequest.produce( + // Specify the type of the kind test container to launch and enable its launch + new KubernetesDevServiceRequestBuildItem(config.clusterType())); + } + } + + @BuildStep + public void deployTekton( + TektonDevServiceConfig tektonConfig, + IngressDevServiceConfig ingressConfig, + Optional kubeServiceInfo, + BuildProducer devServicesResultBuildItem, + BuildProducer tektonDevServiceInfoBuildItemBuildProducer) { + + if (devService != null) { + // only produce DevServicesResultBuildItem when the dev service first starts. + throw new RuntimeException("Dev services already started"); + } + + if (!tektonConfig.enabled() && !kubeServiceInfo.isPresent()) { + // Tekton Dev Service not enabled and Kubernetes test container has not been created ... + throw new RuntimeException( + "Dev services is not enabled for Tekton and Kubernetes test container has not been created ..."); + } + + // Convert the kube config yaml to its Java Class + Config kubeConfig = KubeConfigUtils.parseConfigFromString(kubeServiceInfo.get().getKubeConfig()); + + if (tektonConfig.debugEnabled()) { + LOG.info(">>> Cluster container name : " + kubeServiceInfo.get().getContainerId()); + kubeConfig.getClusters().stream().forEach(c -> { + LOG.debugf(">>> Cluster name: %s", c.getName()); + LOG.debugf(">>> API URL: %s", c.getCluster().getServer()); + }); + kubeConfig.getUsers().stream().forEach(u -> LOG.debugf(">>> User key: %s", u.getUser().getClientKeyData())); + kubeConfig.getContexts().stream().forEach(ctx -> LOG.debugf(">>> Context : %s", ctx.getContext().getUser())); + } + + // Create the Kubernetes client using the Kube YAML Config + KubernetesClient client = new KubernetesClientBuilder() + .withConfig(io.fabric8.kubernetes.client.Config.fromKubeconfig(kubeServiceInfo.get().getKubeConfig())) + .build(); + + // Pass the configuration parameters to the utility class + setConfig(tektonConfig); + setKubernetesClient(client); + + // TODO: To be removed when the issue https://github.com/dajudge/kindcontainer/issues/363 is fixed and released in 1.4.9 + // Patch the node created to add the ingress label + // ingress-ready: "true" + LOG.info("Patching the node's label to add: ingress-ready: true"); + client.nodes().withName("kind").edit( + n -> new NodeBuilder(n).editMetadata().addToLabels("ingress-ready", "true").endMetadata().build()); + + // Install the ingress controller + List items = client.load(fetchIngressResourcesFromURL(ingressConfig.version())).items(); + LOG.info("Deploying the ingress controller resources ..."); + for (HasMetadata item : items) { + var res = client.resource(item).create(); + assertNotNull(res); + } + + waitTillPodSelectedByLabelsIsReady( + Map.of( + "app.kubernetes.io/name", "ingress-nginx", + "app.kubernetes.io/component", "controller"), + "ingress-nginx"); + + var TEKTON_CONTROLLER_NAMESPACE = tektonConfig.controllerNamespace(); + // Install the Tekton resources from the YAML manifest file + items = client.load(fetchTektonResourcesFromURL(tektonConfig.version())).items(); + LOG.info("Deploying the tekton resources ..."); + for (HasMetadata item : items) { + var res = client.resource(item).create(); + assertNotNull(res); + } + + // Waiting till the Tekton pods are ready/running ... + waitTillPodSelectedByLabelsIsReady( + Map.of("app.kubernetes.io/name", "controller", + "app.kubernetes.io/part-of", "tekton-pipelines"), + TEKTON_CONTROLLER_NAMESPACE); + + // TODO + items = client.load(fetchTektonDashboardResourcesFromURL()).items(); + LOG.info("Deploying the tekton dashboard resources ..."); + for (HasMetadata item : items) { + var res = client.resource(item).inNamespace(TEKTON_CONTROLLER_NAMESPACE); + res.create(); + assertNotNull(res); + } + + // Waiting till the Tekton dashboard pod is ready/running ... + waitTillPodSelectedByLabelsIsReady( + Map.of("app.kubernetes.io/name", "dashboard", + "app.kubernetes.io/part-of", "tekton-dashboard"), + TEKTON_CONTROLLER_NAMESPACE); + + // Create the Tekton dashboard ingress route + LOG.info("Creating the ingress route for the tekton dashboard ..."); + Ingress tektonIngressRoute = new IngressBuilder() + // @formatter:off + .withNewMetadata() + .withName("tekton-ui") + .withNamespace(TEKTON_CONTROLLER_NAMESPACE) + .endMetadata() + .withNewSpec() + .addNewRule() + .withHost(tektonConfig.hostName()) + .withNewHttp() + .addNewPath() + .withPath("/") + .withPathType("Prefix") // This field is mandatory + .withNewBackend() + .withNewService() + .withName(TEKTON_DASHBOARD_NAME) + .withNewPort().withNumber(9097).endPort() + .endService() + .endBackend() + .endPath() + .endHttp() + .endRule() + .endSpec() + .build(); + // @formatter:on + client.resource(tektonIngressRoute).create(); + + // Port-forward the traffic from host port to pod's container's port + Pod tektonDashboardPod = client.pods() + .inNamespace(TEKTON_CONTROLLER_NAMESPACE) + .withLabels(Map.of("app.kubernetes.io/name", "dashboard", + "app.kubernetes.io/part-of", TEKTON_DASHBOARD_NAME)) + .list().getItems().get(0); + + LOG.info("Launch Port Forward ..."); + LocalPortForward portForward = client.pods() + .resource(tektonDashboardPod) + .portForward(9097, Integer.parseInt(tektonConfig.hostPort())); + LOG.infof("Pod's container port: %d forwarded to the host port: %d", 9097, portForward.getLocalPort()); + + if (tektonConfig.debugEnabled()) { + // List the pods running under the Tekton controller namespace + client.resources(Pod.class) + .inNamespace(TEKTON_CONTROLLER_NAMESPACE) + .list().getItems().stream().forEach(p -> { + LOG.infof("Pod : %, status: %s", p.getMetadata().getName(), + p.getStatus().getConditions().get(0).getStatus()); + }); + } + + // TODO: To be reviewed in order to pass tekton parameters for the service consuming the extension + Map configOverrides = Map.of( + "quarkus.tekton.devservices.controller-namespace", TEKTON_CONTROLLER_NAMESPACE, + "quarkus.tekton.devservices.kube-config", kubeServiceInfo.get().getKubeConfig()); + + tektonDevServiceInfoBuildItemBuildProducer.produce( + new TektonDevServiceInfoBuildItem( + tektonConfig.hostName(), + Integer.parseInt(tektonConfig.hostPort()))); + + devServicesResultBuildItem.produce(new DevServicesResultBuildItem.RunningDevService( + TektonProcessor.FEATURE, + kubeServiceInfo.get().getContainerId(), + new ContainerShutdownCloseable(new DummyContainer(), TektonProcessor.FEATURE), + configOverrides).toBuildItem()); + } + + private class DummyContainer extends GenericContainer implements Closeable { + private static final Logger LOG = Logger.getLogger(DummyContainer.class); + + @Override + public void close() { + LOG.info("Closing the tekton container ..."); + } + } +} diff --git a/deployment/src/main/java/io/quarkiverse/tekton/deployment/devservices/Utils.java b/deployment/src/main/java/io/quarkiverse/tekton/deployment/devservices/Utils.java new file mode 100644 index 0000000..4c4a181 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/tekton/deployment/devservices/Utils.java @@ -0,0 +1,78 @@ +package io.quarkiverse.tekton.deployment.devservices; + +import java.io.InputStream; +import java.net.URL; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.jboss.logging.Logger; + +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.client.KubernetesClient; + +public class Utils { + private static final Logger LOG = Logger.getLogger(Utils.class); + private static TektonDevServiceConfig config; + private static KubernetesClient client; + + protected static InputStream fetchTektonResourcesFromURL(String version) { + InputStream resourceAsStream = null; + try { + resourceAsStream = new URL( + "https://github.com/tektoncd/pipeline/releases/download/" + version + "/release.yaml") + .openStream(); + } catch (Exception e) { + LOG.error("The resources cannot be fetched from the tekton repository URL !"); + LOG.error(e); + } + return resourceAsStream; + } + + protected static InputStream fetchTektonDashboardResourcesFromURL() { + InputStream resourceAsStream = null; + try { + resourceAsStream = new URL( + "https://storage.googleapis.com/tekton-releases/dashboard/latest/release.yaml").openStream(); + } catch (Exception e) { + LOG.error("The resources cannot be fetched from the tekton dashboard repository URL !"); + LOG.error(e); + } + return resourceAsStream; + } + + protected static InputStream fetchIngressResourcesFromURL(String version) { + InputStream resourceAsStream = null; + try { + if (version == "latest") { + resourceAsStream = new URL( + "https://raw.githubusercontent.com/kubernetes/ingress-nginx/refs/heads/main/deploy/static/provider/kind/deploy.yaml") + .openStream(); + } else { + resourceAsStream = new URL( + "https://raw.githubusercontent.com/kubernetes/ingress-nginx/refs/tags/controller-" + version + + "/deploy/static/provider/kind/deploy.yaml") + .openStream(); + } + } catch (Exception e) { + LOG.error("The resources cannot be fetched from the ingress nginx repository URL !"); + LOG.error(e); + } + return resourceAsStream; + } + + protected static void waitTillPodSelectedByLabelsIsReady(Map labels, String ns) { + client.resources(Pod.class) + .inNamespace(ns) + .withLabels(labels) + .waitUntilReady(config.timeOut(), TimeUnit.SECONDS); + LOG.infof("Pod selected with labels: %s is ready", labels); + } + + public static void setKubernetesClient(KubernetesClient client) { + Utils.client = client; + } + + public static void setConfig(TektonDevServiceConfig devservices) { + Utils.config = devservices; + } +} diff --git a/deployment/src/main/java/io/quarkiverse/tekton/deployment/devui/TektonDevUIProcessor.java b/deployment/src/main/java/io/quarkiverse/tekton/deployment/devui/TektonDevUIProcessor.java new file mode 100644 index 0000000..51da485 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/tekton/deployment/devui/TektonDevUIProcessor.java @@ -0,0 +1,48 @@ +package io.quarkiverse.tekton.deployment.devui; + +import java.util.Optional; + +import org.jboss.logging.Logger; + +import io.quarkiverse.tekton.deployment.devservices.TektonDevServiceInfoBuildItem; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.ExternalPageBuilder; +import io.quarkus.devui.spi.page.Page; + +public class TektonDevUIProcessor { + + private static final Logger LOG = Logger.getLogger(TektonDevUIProcessor.class); + + @BuildStep(onlyIf = IsDevelopment.class) + void createCard(Optional info, BuildProducer cardPage) { + + CardPageBuildItem card = new CardPageBuildItem(); + LOG.debug("Creating card page"); + + info.ifPresent(i -> { + String url = String.format("http://%s:%s", i.host(), i.hostPort()); + LOG.debug("Creating an external link page for: Tekton UI"); + card.addPage(Page.externalPageBuilder("Tekton Dashboard") + .doNotEmbed() + .icon("font-awesome-solid:code-branch") + .url(url)); + }); + + LOG.debug("Creating an external link page for: Tekton project & version"); + final ExternalPageBuilder versionPage = Page.externalPageBuilder("Tekton project") + .icon("font-awesome-solid:tag") + .url("https://tekton.dev/") + .doNotEmbed() + .staticLabel("0.68.0"); + + LOG.debug("Add version page"); + card.addPage(versionPage); + LOG.debug("Set custom car with js"); + card.setCustomCard("qwc-tekton-card.js"); + LOG.debug("Produce ..."); + cardPage.produce(card); + } +} diff --git a/deployment/src/main/java/io/quarkus/devservices/IngressDevServiceConfig.java b/deployment/src/main/java/io/quarkus/devservices/IngressDevServiceConfig.java new file mode 100644 index 0000000..396f700 --- /dev/null +++ b/deployment/src/main/java/io/quarkus/devservices/IngressDevServiceConfig.java @@ -0,0 +1,18 @@ +package io.quarkus.devservices; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@ConfigMapping(prefix = "quarkus.ingress.devservices") +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public interface IngressDevServiceConfig { + /** + * The version of the Ingress controller to be installed from the GitHub repository + * If not specified, it will use the resources published on main branch + * The version to be used should be specified using the tagged release: v1.12.0, etc + */ + @WithDefault("latest") + String version(); +} diff --git a/integration-tests/src/main/resources/application.properties b/deployment/src/main/resources/application.properties similarity index 100% rename from integration-tests/src/main/resources/application.properties rename to deployment/src/main/resources/application.properties diff --git a/deployment/src/main/resources/dev-ui/qwc-tekton-card.js b/deployment/src/main/resources/dev-ui/qwc-tekton-card.js new file mode 100644 index 0000000..e190e28 --- /dev/null +++ b/deployment/src/main/resources/dev-ui/qwc-tekton-card.js @@ -0,0 +1,86 @@ +import { LitElement, html, css} from 'lit'; +import { pages } from 'build-time-data'; +import 'qwc/qwc-extension-link.js'; + +const NAME = "Tekton"; +export class QwcTektonCard extends LitElement { + + static styles = css` + .identity { + display: flex; + justify-content: flex-start; + } + + .description { + padding-bottom: 10px; + } + + .logo { + padding-bottom: 10px; + margin-right: 5px; + } + + .card-content { + color: var(--lumo-contrast-90pct); + display: flex; + flex-direction: column; + justify-content: flex-start; + padding: 2px 2px; + height: 100%; + } + + .card-content slot { + display: flex; + flex-flow: column wrap; + padding-top: 5px; + } + `; + + static properties = { + description: {type: String} + }; + + constructor() { + super(); + } + + connectedCallback() { + super.connectedCallback(); + } + + render() { + return html` + + + + + ${this.description} + + ${this._renderCardLinks()} + + `; + } + + _renderCardLinks(){ + return html`${pages.map(page => html` + + + `)}`; + } + +} +customElements.define('qwc-tekton-card', QwcTektonCard); \ No newline at end of file diff --git a/docs/modules/ROOT/pages/includes/quarkus-tekton_quarkus.ingress.adoc b/docs/modules/ROOT/pages/includes/quarkus-tekton_quarkus.ingress.adoc new file mode 100644 index 0000000..a315051 --- /dev/null +++ b/docs/modules/ROOT/pages/includes/quarkus-tekton_quarkus.ingress.adoc @@ -0,0 +1,32 @@ +[.configuration-legend] +icon:lock[title=Fixed at build time] Configuration property fixed at build time - All other configuration properties are overridable at runtime +[.configuration-reference.searchable, cols="80,.^10,.^10"] +|=== + +h|[.header-title]##Configuration property## +h|Type +h|Default + +a|icon:lock[title=Fixed at build time] [[quarkus-tekton_quarkus-ingress-devservices-version]] [.property-path]##link:#quarkus-tekton_quarkus-ingress-devservices-version[`quarkus.ingress.devservices.version`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.ingress.devservices.version+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +The version of the Ingress controller to be installed from the GitHub repository If not specified, it will use the resources published on main branch The version to be used should be specified using the tagged release: v1.12.0, etc + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_INGRESS_DEVSERVICES_VERSION+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_INGRESS_DEVSERVICES_VERSION+++` +endif::add-copy-button-to-env-var[] +-- +|string +|`latest` + +|=== + diff --git a/docs/modules/ROOT/pages/includes/quarkus-tekton_quarkus.tekton.adoc b/docs/modules/ROOT/pages/includes/quarkus-tekton_quarkus.tekton.adoc index 3907241..dabab6f 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-tekton_quarkus.tekton.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-tekton_quarkus.tekton.adoc @@ -7,6 +7,195 @@ h|[.header-title]##Configuration property## h|Type h|Default +a|icon:lock[title=Fixed at build time] [[quarkus-tekton_quarkus-tekton-devservices-enabled]] [.property-path]##link:#quarkus-tekton_quarkus-tekton-devservices-enabled[`quarkus.tekton.devservices.enabled`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.tekton.devservices.enabled+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Enable the Tekton DevService. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEKTON_DEVSERVICES_ENABLED+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEKTON_DEVSERVICES_ENABLED+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +|`true` + +a|icon:lock[title=Fixed at build time] [[quarkus-tekton_quarkus-tekton-devservices-debug-enabled]] [.property-path]##link:#quarkus-tekton_quarkus-tekton-devservices-debug-enabled[`quarkus.tekton.devservices.debug-enabled`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.tekton.devservices.debug-enabled+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Enable the debugging level. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEKTON_DEVSERVICES_DEBUG_ENABLED+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEKTON_DEVSERVICES_DEBUG_ENABLED+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +|`false` + +a|icon:lock[title=Fixed at build time] [[quarkus-tekton_quarkus-tekton-devservices-show-logs]] [.property-path]##link:#quarkus-tekton_quarkus-tekton-devservices-show-logs[`quarkus.tekton.devservices.show-logs`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.tekton.devservices.show-logs+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +If logs should be shown from the Tekton container. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEKTON_DEVSERVICES_SHOW_LOGS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEKTON_DEVSERVICES_SHOW_LOGS+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +|`false` + +a|icon:lock[title=Fixed at build time] [[quarkus-tekton_quarkus-tekton-devservices-version]] [.property-path]##link:#quarkus-tekton_quarkus-tekton-devservices-version[`quarkus.tekton.devservices.version`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.tekton.devservices.version+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +The version of Tekton to be installed from the GitHub repository and which corresponds to a tagged release expressed as such: "v0.68.0" + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEKTON_DEVSERVICES_VERSION+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEKTON_DEVSERVICES_VERSION+++` +endif::add-copy-button-to-env-var[] +-- +|string +|`v0.68.0` + +a|icon:lock[title=Fixed at build time] [[quarkus-tekton_quarkus-tekton-devservices-controller-namespace]] [.property-path]##link:#quarkus-tekton_quarkus-tekton-devservices-controller-namespace[`quarkus.tekton.devservices.controller-namespace`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.tekton.devservices.controller-namespace+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +The Tekton controller namespace where Tekton stuffs are deployed The default namespace is: tekton-pipelines + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEKTON_DEVSERVICES_CONTROLLER_NAMESPACE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEKTON_DEVSERVICES_CONTROLLER_NAMESPACE+++` +endif::add-copy-button-to-env-var[] +-- +|string +|`tekton-pipelines` + +a|icon:lock[title=Fixed at build time] [[quarkus-tekton_quarkus-tekton-devservices-time-out]] [.property-path]##link:#quarkus-tekton_quarkus-tekton-devservices-time-out[`quarkus.tekton.devservices.time-out`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.tekton.devservices.time-out+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Time to wait till a resource is ready: pod, etc The default value is: 180 seconds + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEKTON_DEVSERVICES_TIME_OUT+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEKTON_DEVSERVICES_TIME_OUT+++` +endif::add-copy-button-to-env-var[] +-- +|long +|`360` + +a|icon:lock[title=Fixed at build time] [[quarkus-tekton_quarkus-tekton-devservices-cluster-type]] [.property-path]##link:#quarkus-tekton_quarkus-tekton-devservices-cluster-type[`quarkus.tekton.devservices.cluster-type`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.tekton.devservices.cluster-type+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +The cluster type to be used: kind or k3 The default value is: kind + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEKTON_DEVSERVICES_CLUSTER_TYPE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEKTON_DEVSERVICES_CLUSTER_TYPE+++` +endif::add-copy-button-to-env-var[] +-- +|string +|`kind` + +a|icon:lock[title=Fixed at build time] [[quarkus-tekton_quarkus-tekton-devservices-host-name]] [.property-path]##link:#quarkus-tekton_quarkus-tekton-devservices-host-name[`quarkus.tekton.devservices.host-name`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.tekton.devservices.host-name+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +The hostname of the tekton ingress route + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEKTON_DEVSERVICES_HOST_NAME+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEKTON_DEVSERVICES_HOST_NAME+++` +endif::add-copy-button-to-env-var[] +-- +|string +|`tekton.localtest.me` + +a|icon:lock[title=Fixed at build time] [[quarkus-tekton_quarkus-tekton-devservices-host-port]] [.property-path]##link:#quarkus-tekton_quarkus-tekton-devservices-host-port[`quarkus.tekton.devservices.host-port`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.tekton.devservices.host-port+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +The host port to be used on the host machine to access the dashboard + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEKTON_DEVSERVICES_HOST_PORT+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEKTON_DEVSERVICES_HOST_PORT+++` +endif::add-copy-button-to-env-var[] +-- +|string +|`9097` + a|icon:lock[title=Fixed at build time] [[quarkus-tekton_quarkus-tekton-generation-enabled]] [.property-path]##link:#quarkus-tekton_quarkus-tekton-generation-enabled[`quarkus.tekton.generation.enabled`]## ifdef::add-copy-button-to-config-props[] config_property_copy_button:+++quarkus.tekton.generation.enabled+++[] diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index b3ba065..2c050c5 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -24,6 +24,14 @@ quarkus-tekton ${project.version} + + + + io.quarkiverse.tekton + quarkus-tekton-cli + 999-SNAPSHOT + test + io.quarkus quarkus-junit5 @@ -34,6 +42,17 @@ rest-assured test + + io.fabric8 + tekton-model + test + + + org.projectlombok + lombok + ${lombok.version} + test + diff --git a/integration-tests/src/main/java/io/quarkiverse/quarkus/tekton/it/TektonResource.java b/integration-tests/src/main/java/io/quarkiverse/tekton/it/TektonResource.java similarity index 96% rename from integration-tests/src/main/java/io/quarkiverse/quarkus/tekton/it/TektonResource.java rename to integration-tests/src/main/java/io/quarkiverse/tekton/it/TektonResource.java index 329b7f3..288094d 100644 --- a/integration-tests/src/main/java/io/quarkiverse/quarkus/tekton/it/TektonResource.java +++ b/integration-tests/src/main/java/io/quarkiverse/tekton/it/TektonResource.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.quarkiverse.quarkus.tekton.it; +package io.quarkiverse.tekton.it; import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.GET; diff --git a/integration-tests/src/test/java/io/quarkiverse/tekton/it/ObjectMetaMixin.java b/integration-tests/src/test/java/io/quarkiverse/tekton/it/ObjectMetaMixin.java new file mode 100644 index 0000000..a341e47 --- /dev/null +++ b/integration-tests/src/test/java/io/quarkiverse/tekton/it/ObjectMetaMixin.java @@ -0,0 +1,19 @@ +package io.quarkiverse.tekton.it; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import io.fabric8.kubernetes.api.model.ManagedFieldsEntry; +import io.fabric8.kubernetes.api.model.ObjectMeta; + +@SuppressWarnings("unused") +public abstract class ObjectMetaMixin extends ObjectMeta { + + @JsonIgnore + private List managedFields; + + @JsonIgnore + public abstract List getManagedFields(); + +} diff --git a/integration-tests/src/test/java/io/quarkiverse/tekton/it/TektonExtensionDevModeTest.java b/integration-tests/src/test/java/io/quarkiverse/tekton/it/TektonExtensionDevModeTest.java new file mode 100644 index 0000000..8a3a84e --- /dev/null +++ b/integration-tests/src/test/java/io/quarkiverse/tekton/it/TektonExtensionDevModeTest.java @@ -0,0 +1,96 @@ +package io.quarkiverse.tekton.it; + +import static io.quarkiverse.tekton.it.TektonResourceGenerator.*; + +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.TimeUnit; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logging.Logger; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.utils.KubernetesSerialization; +import io.fabric8.tekton.v1.PipelineRun; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class TektonExtensionDevModeTest { + + private static final Logger LOG = Logger.getLogger(TektonExtensionDevModeTest.class); + private static KubernetesClient client; + private static String TEKTON_NAMESPACE; + private static final long TIMEOUT = 180; + + @BeforeAll + public static void setup() { + var objectMapper = new ObjectMapper(); + objectMapper.addMixIn(ObjectMeta.class, ObjectMetaMixin.class); + + var kubernetesSerialization = new KubernetesSerialization(objectMapper, true); + client = new KubernetesClientBuilder() + .withConfig(Config.fromKubeconfig( + ConfigProvider.getConfig().getValue("quarkus.tekton.devservices.kube-config", String.class))) + .withKubernetesSerialization(kubernetesSerialization) + .build(); + + TEKTON_NAMESPACE = ConfigProvider.getConfig().getValue("quarkus.tekton.devservices.controller-namespace", String.class); + } + + /* + * TODO + */ + @Test + @Order(1) + public void testCase() throws NoSuchAlgorithmException, KeyManagementException, JsonProcessingException { + LOG.info(">>> Running the test case"); + + // Deploy the Tasks: hello, goodbye and Pipeline hello-goodbye + client.resource(populateHelloTask()) + .inNamespace(TEKTON_NAMESPACE) + .create(); + + client.resource(populateGoodbyeTask()) + .inNamespace(TEKTON_NAMESPACE) + .create(); + + client.resource(populateHelloGoodbyePipeline()) + .inNamespace(TEKTON_NAMESPACE) + .create(); + + // Run the Tekton pipeline + client.resource(populatePipelineRun()) + .inNamespace(TEKTON_NAMESPACE) + .create(); + + LOG.info("Checking when Tekton Pipeline will end"); + try { + client.resources(PipelineRun.class) + .inNamespace(TEKTON_NAMESPACE) + .withName("hello-goodbye-run") + .waitUntilCondition(a -> a != null && + a.getStatus() != null && + a.getStatus().getConditions() != null && + a.getStatus().getConditions().get(0).getType().equals("Succeeded"), TIMEOUT, TimeUnit.SECONDS); + } catch (Exception e) { + LOG.error(client.getKubernetesSerialization() + .asYaml(client.genericKubernetesResources("apiVersion", "tekton.dev/v1") + .inNamespace(TEKTON_NAMESPACE) + .withName("hello-goodbye-run").get())); + } + LOG.infof("Tekton PipelineRun status"); + PipelineRun pipelineRun = client.resources(PipelineRun.class) + .inNamespace(TEKTON_NAMESPACE) + .withName("hello-goodbye-run").get(); + LOG.warn(client.getKubernetesSerialization().asYaml(pipelineRun)); + } +} diff --git a/integration-tests/src/test/java/io/quarkiverse/tekton/it/TektonResourceGenerator.java b/integration-tests/src/test/java/io/quarkiverse/tekton/it/TektonResourceGenerator.java new file mode 100644 index 0000000..988084a --- /dev/null +++ b/integration-tests/src/test/java/io/quarkiverse/tekton/it/TektonResourceGenerator.java @@ -0,0 +1,160 @@ +package io.quarkiverse.tekton.it; + +import io.fabric8.tekton.v1.*; + +public class TektonResourceGenerator { + public static Pipeline populateHelloGoodbyePipeline() { + /* + * apiVersion: tekton.dev/v1 + * kind: Pipeline + * metadata: + * name: hello + * spec: + * params: + * - name: username + * type: string + * tasks: + * - name: hello + * taskRef: + * name: hello + * - name: goodbye + * runAfter: + * - hello + * taskRef: + * name: goodbye + * params: + * - name: username + * value: $(params.username) + */ + // @formatter:off + return new PipelineBuilder() + .withNewMetadata() + .withName("hello-goodbye") + .endMetadata() + .withNewSpec() + .withParams(new ParamSpecBuilder() + .withName("username") + .withType("string") + .build()) + .withTasks( + new PipelineTaskBuilder() + .withName("hello") + .withTaskRef(new TaskRefBuilder() + .withName("hello") + .build()) + .build(), + new PipelineTaskBuilder() + .withName("goodbye") + .withRunAfter("hello") + .withTaskRef(new TaskRefBuilder() + .withName("goodbye") + .build()) + .withParams(new ParamBuilder() + .withName("username") + .withValue(new ParamValue("$(params.username)")) + .build()) + .build() + + ) + .endSpec() + .build(); + // @formatter:on + } + + public static Task populateGoodbyeTask() { + /* + * apiVersion: tekton.dev/v1beta1 + * kind: Task + * metadata: + * name: goodbye + * spec: + * params: + * - name: username + * type: string + * steps: + * - name: goodbye + * image: ubuntu + * script: | + * #!/bin/bash + * echo "Goodbye $(params.username)!" + */ + // @formatter:off + return new TaskBuilder() + .withNewMetadata().withName("goodbye") + .endMetadata() + .withNewSpec() + .withParams(new ParamSpecBuilder() + .withName("username") + .withType("string") + .build()) + .withSteps(new StepBuilder() + .withName("goodbye") + .withImage("ubuntu") + .withScript("|" + + "#!/bin/bash" + + "echo \"Goodbye $(params.username)!\"") + .build()) + .endSpec() + .build(); + // @formatter:on + } + + public static Task populateHelloTask() { + /* + * apiVersion: tekton.dev/v1 + * kind: Task + * metadata: + * name: hello + * spec: + * steps: + * - name: echo + * image: alpine + * script: | + * #!/bin/sh + * echo "Hello World" + */ + // @formatter:off + return new TaskBuilder() + .withNewMetadata().withName("hello") + .endMetadata() + .withNewSpec() + .withSteps(new StepBuilder() + .withName("echo") + .withImage("alpine") + .withScript("|" + + "#!/bin/bash" + + "echo \"Hello Worl\"") + .build()) + .endSpec() + .build(); + // @formatter:on + } + + public static PipelineRun populatePipelineRun() { + /* + * apiVersion: tekton.dev/v1 + * kind: PipelineRun + * metadata: + * name: hello-goodbye-run + * spec: + * pipelineRef: + * name: hello-goodbye + * params: + * - name: username + * value: "Tekton" + */ + // @formatter:off + return new PipelineRunBuilder() + .withNewMetadata() + .withName("hello-goodbye-run") + .endMetadata() + .withNewSpec() + .withPipelineRef(new PipelineRefBuilder().withName("hello-goodbye").build()) + .withParams( + new ParamBuilder().withName("username").withValue(new ParamValue("tekton")).build() + ) + .endSpec() + .build(); + // @formatter:on + } +} diff --git a/integration-tests/src/test/resources/application.properties b/integration-tests/src/test/resources/application.properties new file mode 100644 index 0000000..157c2c2 --- /dev/null +++ b/integration-tests/src/test/resources/application.properties @@ -0,0 +1,9 @@ +#quarkus.tekton.devservices.enabled=true +#quarkus.tekton.devservices.cluster-type=kind + +quarkus.tekton.devservices.version=v0.68.0 +quarkus.tekton.devservices.debug-enabled=false + +quarkus.log.category."io.quarkiverse.tekton".level=INFO +quarkus.log.category."io.fabric8.kubernetes".level=INFO +quarkus.log.category."org.testcontainers.containers".level=INFO diff --git a/pom.xml b/pom.xml index 482566d..7e8dc5c 100644 --- a/pom.xml +++ b/pom.xml @@ -34,8 +34,10 @@ UTF-8 3.18.1 + 1.1.0 7.1.0 + 1.18.30 3.2.5