Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Creating a Tekton DevServices and UI #5

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
<artifactId>quarkus-tekton</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-kubernetes-spi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

/**
Expand All @@ -21,6 +21,5 @@ interface Generation {
*/
@WithDefault("true")
boolean enabled();

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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();

}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<KubernetesDevServiceRequestBuildItem> 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<KubernetesDevServiceInfoBuildItem> kubeServiceInfo,
BuildProducer<DevServicesResultBuildItem> devServicesResultBuildItem,
BuildProducer<TektonDevServiceInfoBuildItem> 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<HasMetadata> 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<String, String> 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<DummyContainer> implements Closeable {
private static final Logger LOG = Logger.getLogger(DummyContainer.class);

@Override
public void close() {
LOG.info("Closing the tekton container ...");
}
}
}
Loading
Loading