Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
package com.github.streamshub.console;

import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import com.github.streamshub.console.api.v1alpha1.Console;
import com.github.streamshub.console.api.v1alpha1.status.Condition;
import com.github.streamshub.console.api.v1alpha1.status.ConditionBuilder;
Expand All @@ -22,6 +10,7 @@
import com.github.streamshub.console.dependents.ConsoleIngress;
import com.github.streamshub.console.dependents.ConsoleMonitoringClusterRoleBinding;
import com.github.streamshub.console.dependents.ConsoleResource;
import com.github.streamshub.console.dependents.ConsoleRoute;
import com.github.streamshub.console.dependents.ConsoleSecret;
import com.github.streamshub.console.dependents.ConsoleService;
import com.github.streamshub.console.dependents.ConsoleServiceAccount;
Expand All @@ -34,8 +23,8 @@
import com.github.streamshub.console.dependents.conditions.DeploymentReadyCondition;
import com.github.streamshub.console.dependents.conditions.IngressReadyCondition;
import com.github.streamshub.console.dependents.conditions.PrometheusPrecondition;
import com.github.streamshub.console.dependents.conditions.RouteReadyCondition;
import com.github.streamshub.console.support.RootCause;

import io.javaoperatorsdk.operator.AggregatedOperatorException;
import io.javaoperatorsdk.operator.api.reconciler.Cleaner;
import io.javaoperatorsdk.operator.api.reconciler.Context;
Expand All @@ -56,6 +45,18 @@
import io.quarkiverse.operatorsdk.annotations.CSVMetadata.Maintainer;
import io.quarkiverse.operatorsdk.annotations.CSVMetadata.Provider;

import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors;

@ControllerConfiguration(
maxReconciliationInterval = @MaxReconciliationInterval(
interval = 60,
Expand Down Expand Up @@ -140,17 +141,27 @@
@Dependent(
name = ConsoleIngress.NAME,
type = ConsoleIngress.class,
reconcilePrecondition = ConsoleIngress.Precondition.class,
dependsOn = {
ConsoleService.NAME
},
readyPostcondition = IngressReadyCondition.class),
@Dependent(
name = ConsoleRoute.NAME,
type = ConsoleRoute.class,
reconcilePrecondition = ConsoleRoute.Precondition.class,
dependsOn = {
ConsoleService.NAME
},
readyPostcondition = RouteReadyCondition.class),
@Dependent(
name = ConsoleDeployment.NAME,
type = ConsoleDeployment.class,
dependsOn = {
ConsoleClusterRoleBinding.NAME,
ConsoleSecret.NAME,
ConsoleIngress.NAME
ConsoleIngress.NAME,
ConsoleRoute.NAME
},
readyPostcondition = DeploymentReadyCondition.class),
})
Expand Down Expand Up @@ -349,4 +360,4 @@ private String getMessage(Throwable rootCause) {

return message;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import io.fabric8.openshift.api.model.Route;
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;

@ApplicationScoped
@KubernetesDependent(informer = @Informer(labelSelector = ConsoleResource.MANAGEMENT_SELECTOR))
Expand Down Expand Up @@ -43,7 +45,9 @@ protected Ingress desired(Console primary, Context<Console> context) {
.withLabels(commonLabels("console"))
.endMetadata()
.editSpec()
.withIngressClassName(getIngressClassName(context))
// Plain Kubernetes (non-OCP) clusters don't need a class name; the
// default ingress controller picks it up automatically.
.withIngressClassName(null)
.editDefaultBackend()
.editService()
.withName(service.instanceName(primary))
Expand All @@ -66,10 +70,15 @@ protected Ingress desired(Console primary, Context<Console> context) {
}

/**
* The class name is not required for functionality on OCP. However, monitoring
* will issue an alert if it is not present.
* Reconcile precondition: only create the plain Ingress on clusters that do
* NOT support OpenShift Routes. On OpenShift / MicroShift the Route-based
* dependent ({@code ConsoleRoute}) is used instead.
*/
private String getIngressClassName(Context<Console> context) {
return context.getClient().supports(Route.class) ? "openshift-default" : null;
@ApplicationScoped
public static class Precondition implements Condition<Ingress, Console> {
@Override
public boolean isMet(DependentResource<Ingress, Console> dependentResource, Console primary, Context<Console> context) {
return !context.getClient().supports(Route.class);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.github.streamshub.console.dependents;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import com.github.streamshub.console.api.v1alpha1.Console;

import io.fabric8.openshift.api.model.Route;
import io.fabric8.openshift.api.model.RouteBuilder;
import io.fabric8.openshift.api.model.RouteTargetReferenceBuilder;
import io.fabric8.openshift.api.model.TLSConfigBuilder;
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;

import java.util.Optional;

@ApplicationScoped
@KubernetesDependent(informer = @Informer(labelSelector = ConsoleResource.MANAGEMENT_SELECTOR))
public class ConsoleRoute extends CRUDKubernetesDependentResource<Route, Console>
implements ConsoleResource<Route> {

public static final String NAME = "console-route";

@Inject
ConsoleService service;

public ConsoleRoute() {
super(Route.class);
}

@Override
public String resourceName() {
return NAME;
}

@Override
public Optional<Route> getSecondaryResource(Console primary, Context<Console> context) {
return ConsoleResource.super.getSecondaryResource(primary, context);
}

@Override
protected Route desired(Console primary, Context<Console> context) {
String host = primary.getSpec().getHostname();
String serviceName = service.instanceName(primary);

// Store the URL so ConsoleDeployment can use it for NEXTAUTH_URL
setAttribute(context, INGRESS_URL_KEY, "https://" + host);

return new RouteBuilder()
.withNewMetadata()
.withName(instanceName(primary))
.withNamespace(primary.getMetadata().getNamespace())
.withLabels(commonLabels("console"))
.endMetadata()
.withNewSpec()
.withHost(host)
.withTo(new RouteTargetReferenceBuilder()
.withKind("Service")
.withName(serviceName)
.withWeight(100)
.build())
.withNewPort()
.withNewTargetPort("https")
.endPort()
.withTls(new TLSConfigBuilder()
.withTermination("edge")
.withInsecureEdgeTerminationPolicy("Redirect")
.build())
.endSpec()
.build();
}

/**
* Reconcile precondition: only create the Route when the cluster supports
* OpenShift Route resources (i.e. OpenShift / MicroShift).
*/
@ApplicationScoped
public static class Precondition implements Condition<Route, Console> {
@Override
public boolean isMet(DependentResource<Route, Console> dependentResource,
Console primary,
Context<Console> context) {
return context.getClient().supports(Route.class);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.github.streamshub.console.dependents.conditions;

import com.github.streamshub.console.api.v1alpha1.Console;
import io.fabric8.openshift.api.model.Route;
import io.fabric8.openshift.api.model.RouteIngress;
import io.fabric8.openshift.api.model.RouteIngressCondition;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
import org.jboss.logging.Logger;

import java.util.Optional;

public class RouteReadyCondition implements Condition<Route, Console> {

private static final Logger LOGGER = Logger.getLogger(RouteReadyCondition.class);

@Override
public boolean isMet(DependentResource<Route, Console> dependentResource,
Console primary,
Context<Console> context) {
return dependentResource.getSecondaryResource(primary, context)
.map(this::isReady)
.orElse(false);
}

private boolean isReady(Route route) {
// A Route is ready when at least one ingress entry carries an
// Admitted=True condition — this is how the OpenShift router signals
// that it has picked up the route (works on both full OCP and MicroShift).
boolean ready = Optional.ofNullable(route.getStatus())
.map(s -> s.getIngress())
.filter(list -> !list.isEmpty())
.map(ingresses -> ingresses.stream().anyMatch(this::isAdmitted))
.orElse(false);

LOGGER.debugf("Route %s ready: %s", route.getMetadata().getName(), ready);
return ready;
}

private boolean isAdmitted(RouteIngress ingress) {
return Optional.ofNullable(ingress.getConditions())
.map(conditions -> conditions.stream().anyMatch(this::admittedTrue))
.orElse(false);
}

private boolean admittedTrue(RouteIngressCondition condition) {
return "Admitted".equals(condition.getType()) && "True".equals(condition.getStatus());
}
}
Loading