Skip to content

Commit

Permalink
feat (jkube-kit/config/service) : Detect and warn user if Ingress con…
Browse files Browse the repository at this point in the history
…troller not available (eclipse-jkube#1478)

+ Add IngressControllerDetectorService which will try to detect whether
  any Ingress Controller is present in the cluster before applying
  Ingress. At the moment, it supports detecting these Ingress
  Controllers:
  - Contour
  - Ingress Kong
  - Ingress Nginx
  - OpenShift Ingress Controller

  This is based on Kind Ingress guide, we can also expand it if we want
  to.

Signed-off-by: Rohan Kumar <rohaan@redhat.com>
  • Loading branch information
rohanKanojia committed Feb 16, 2023
1 parent 7777376 commit 841dbbc
Show file tree
Hide file tree
Showing 21 changed files with 1,077 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Usage:
* Fix #1316: Add support for adding InitContainers via plugin configuration
* Fix #1439: Add hint to use jkube.domain if createExternalUrls is used without domain
* Fix #1459: Route Generation should support `8443` as default web port
* Fix #1478: Should detect and warn the user if creating ingress and ingress controller not available
* Fix #1546: Migrate to JUnit5 testing framework
* Fix #1829: Add trailing newline for labels/annotations for multiline values to avoid setting block chomping indicator
* Fix #1858: Properties in image name not replaced
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Copyright (c) 2019 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at:
*
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.jkube.kit.common;

import io.fabric8.kubernetes.client.KubernetesClientException;

public interface IngressControllerDetector {
/**
* Returns boolean value whether Ingress Controller is running in Kubernetes Cluster
* @return true if Ingress Controller is present, false otherwise
*/
default boolean isDetected() {
try {
return hasIngressClass() && isIngressControllerReady();
} catch (KubernetesClientException exception) {
return false;
}
}

/**
* Returns whether IngressClass for corresponding controller is installed in cluster
*
* @return true if IngressClass is installed, false otherwise
*/
boolean hasIngressClass();

/**
* Is Ingress Controller pod running in the cluster
*
* @return boolean value if Ingress Controller is running, false otherwise.
*/
boolean isIngressControllerReady();

/**
* Returns boolean value whether currently logged user has got enough permissions to
* check for current IngressController implementation
* @return true if it has permissions, false otherwise
*/
boolean hasPermissions();
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
import java.util.stream.Collectors;

import io.fabric8.kubernetes.api.model.HTTPHeader;
import io.fabric8.kubernetes.api.model.authorization.v1.ResourceAttributesBuilder;
import io.fabric8.kubernetes.api.model.authorization.v1.SelfSubjectAccessReview;
import io.fabric8.kubernetes.api.model.authorization.v1.SelfSubjectAccessReviewBuilder;
import io.fabric8.kubernetes.client.utils.ApiVersionUtil;
import org.eclipse.jkube.kit.common.KitLogger;

Expand Down Expand Up @@ -797,5 +800,31 @@ public static List<HTTPHeader> convertMapToHTTPHeaderList(Map<String, String> he
}
return httpHeaders;
}

public static SelfSubjectAccessReview createNewSelfSubjectAccessReview(String group, String resource, String namespace, String verb) {
SelfSubjectAccessReviewBuilder ssarBuilder = new SelfSubjectAccessReviewBuilder();
ResourceAttributesBuilder resourceAttributesBuilder = new ResourceAttributesBuilder();
if (StringUtils.isNotBlank(group)) {
resourceAttributesBuilder.withGroup(group);
}
if (StringUtils.isNotBlank(resource)) {
resourceAttributesBuilder.withResource(resource);
}
if (StringUtils.isNotBlank(namespace)) {
resourceAttributesBuilder.withNamespace(namespace);
}
if (StringUtils.isNotBlank(verb)) {
resourceAttributesBuilder.withVerb(verb);
}
return ssarBuilder.withNewSpec()
.withResourceAttributes(resourceAttributesBuilder.build())
.endSpec()
.build();
}

public static boolean hasAccessForAction(KubernetesClient client, SelfSubjectAccessReview accessReview) {
SelfSubjectAccessReview accessReviewFromServer = client.authorization().v1().selfSubjectAccessReview().create(accessReview);
return accessReviewFromServer.getStatus().getAllowed();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@
import java.util.List;
import java.util.Map;

import io.fabric8.kubernetes.api.model.authorization.v1.SelfSubjectAccessReview;
import io.fabric8.kubernetes.api.model.authorization.v1.SelfSubjectAccessReviewBuilder;
import io.fabric8.kubernetes.api.model.authorization.v1.SubjectAccessReviewStatus;
import io.fabric8.kubernetes.api.model.authorization.v1.SubjectAccessReviewStatusBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.V1AuthorizationAPIGroupDSL;
import io.fabric8.kubernetes.client.dsl.AuthorizationAPIGroupDSL;
import io.fabric8.kubernetes.client.dsl.InOutCreateable;
import org.eclipse.jkube.kit.common.KitLogger;

import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
Expand Down Expand Up @@ -54,6 +62,10 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.eclipse.jkube.kit.common.util.KubernetesHelper.hasAccessForAction;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class KubernetesHelperTest {

Expand Down Expand Up @@ -409,6 +421,53 @@ void extractPodLabelSelector_withJobWithNoSelector_shouldReturnTemplateLabels()
.containsEntry("template", "label");
}

@Test
void createNewSelfSubjectAccessReview_whenGroupResourceNamespaceVerbProvided_thenShouldCreateSelfSubjectAccessReview() {
// When
SelfSubjectAccessReview selfSubjectAccessReview = KubernetesHelper.createNewSelfSubjectAccessReview("example.com", "foos", "test-ns", "list");

// Then
assertThat(selfSubjectAccessReview)
.hasFieldOrPropertyWithValue("spec.resourceAttributes.group", "example.com")
.hasFieldOrPropertyWithValue("spec.resourceAttributes.resource", "foos")
.hasFieldOrPropertyWithValue("spec.resourceAttributes.namespace", "test-ns")
.hasFieldOrPropertyWithValue("spec.resourceAttributes.verb", "list");
}

@Test
void hasAccessForAction_whenApiServerReturnsAllowedTrue_thenReturnTrue() {
try (KubernetesClient kubernetesClient = mock(KubernetesClient.class)) {
// Given
mockKubernetesClientAccessReviewCallReturns(kubernetesClient, new SubjectAccessReviewStatusBuilder()
.withAllowed(true)
.build());
SelfSubjectAccessReview selfSubjectAccessReview = createNewTestSelfSubjectAccessReview();

// When
boolean result = hasAccessForAction(kubernetesClient, selfSubjectAccessReview);

// Then
assertThat(result).isTrue();
}
}

@Test
void hasAccessForAction_whenApiServerReturnsAllowedFalse_thenReturnFalse() {
try (KubernetesClient kubernetesClient = mock(KubernetesClient.class)) {
// Given
mockKubernetesClientAccessReviewCallReturns(kubernetesClient, new SubjectAccessReviewStatusBuilder()
.withAllowed(false)
.build());
SelfSubjectAccessReview selfSubjectAccessReview = createNewTestSelfSubjectAccessReview();

// When
boolean result = hasAccessForAction(kubernetesClient, selfSubjectAccessReview);

// Then
assertThat(result).isFalse();
}
}

private void assertLocalFragments(File[] fragments, int expectedSize) {
assertThat(fragments).hasSize(expectedSize);
assertThat(Arrays.stream(fragments).anyMatch( f -> f.getName().equals("deployment.yml"))).isTrue();
Expand All @@ -431,4 +490,24 @@ private List<String> getRemoteFragments(int port) {
remoteStrList.add(String.format("http://localhost:%d/sa.yml", port));
return remoteStrList;
}

private void mockKubernetesClientAccessReviewCallReturns(KubernetesClient kubernetesClient, SubjectAccessReviewStatus status) {
AuthorizationAPIGroupDSL authorizationAPIGroupDSL = mock(AuthorizationAPIGroupDSL.class);
V1AuthorizationAPIGroupDSL v1AuthorizationAPIGroupDSL = mock(V1AuthorizationAPIGroupDSL.class);
InOutCreateable<SelfSubjectAccessReview, SelfSubjectAccessReview> inOutCreateable = mock(InOutCreateable.class);
when(kubernetesClient.authorization()).thenReturn(authorizationAPIGroupDSL);
when(authorizationAPIGroupDSL.v1()).thenReturn(v1AuthorizationAPIGroupDSL);
when(v1AuthorizationAPIGroupDSL.selfSubjectAccessReview()).thenReturn(inOutCreateable);
when(inOutCreateable.create(any())).thenReturn(new SelfSubjectAccessReviewBuilder().withStatus(status).build());
}

private SelfSubjectAccessReview createNewTestSelfSubjectAccessReview() {
return new SelfSubjectAccessReviewBuilder()
.withNewSpec()
.withNewResourceAttributes()
.withGroup("foo")
.endResourceAttributes()
.endSpec()
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.eclipse.jkube.kit.common.util.OpenshiftHelper;
import org.eclipse.jkube.kit.common.util.ResourceUtil;
import org.eclipse.jkube.kit.common.util.UserConfigurationCompare;
import org.eclipse.jkube.kit.config.service.ingresscontroller.IngressControllerDetectorService;
import org.eclipse.jkube.kit.config.service.kubernetes.KubernetesClientUtil;

import com.fasterxml.jackson.core.JsonProcessingException;
Expand Down Expand Up @@ -112,12 +113,14 @@ public class ApplyService {
private boolean rollingUpgradePreserveScale = true;
private boolean recreateMode;
private final PatchService patchService;
private final IngressControllerDetectorService ingressControllerDetectorService;
// This map is to track projects created.
private static final Set<String> projectsCreated = new HashSet<>();

public ApplyService(KubernetesClient kubernetesClient, KitLogger log) {
public ApplyService(KubernetesClient kubernetesClient, IngressControllerDetectorService ingressControllerDetectorService, KitLogger log) {
this.kubernetesClient = kubernetesClient;
this.patchService = new PatchService(kubernetesClient, log);
this.ingressControllerDetectorService = ingressControllerDetectorService;
this.log = log;
}

Expand Down Expand Up @@ -192,7 +195,11 @@ private void applyEntity(Object dto, String sourceName) {
} else if (dto instanceof StatefulSet) {
applyResource((StatefulSet) dto, sourceName, kubernetesClient.apps().statefulSets());
} else if (dto instanceof Ingress) {
ingressControllerDetectorService.detect();
applyResource((Ingress) dto, sourceName, kubernetesClient.extensions().ingresses());
}else if (dto instanceof io.fabric8.kubernetes.api.model.networking.v1.Ingress) {
ingressControllerDetectorService.detect();
applyResource((io.fabric8.kubernetes.api.model.networking.v1.Ingress) dto, sourceName, kubernetesClient.network().v1().ingresses());
} else if (dto instanceof PersistentVolumeClaim) {
applyPersistentVolumeClaim((PersistentVolumeClaim) dto, sourceName);
} else if (dto instanceof CustomResourceDefinition) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package org.eclipse.jkube.kit.config.service;

import java.io.Closeable;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

Expand All @@ -22,15 +23,18 @@
import lombok.Getter;
import lombok.Setter;
import org.eclipse.jkube.kit.build.service.docker.access.DockerAccess;
import org.eclipse.jkube.kit.common.IngressControllerDetector;
import org.eclipse.jkube.kit.common.service.MigrateService;
import org.eclipse.jkube.kit.build.service.docker.DockerServiceHub;
import org.eclipse.jkube.kit.common.KitLogger;
import org.eclipse.jkube.kit.common.util.LazyBuilder;
import org.eclipse.jkube.kit.common.util.PluginServiceFactory;
import org.eclipse.jkube.kit.config.access.ClusterAccess;
import org.eclipse.jkube.kit.config.access.ClusterConfiguration;
import org.eclipse.jkube.kit.common.JKubeConfiguration;
import org.eclipse.jkube.kit.config.resource.ResourceService;
import org.eclipse.jkube.kit.config.resource.RuntimeMode;
import org.eclipse.jkube.kit.config.service.ingresscontroller.IngressControllerDetectorService;
import org.eclipse.jkube.kit.config.service.kubernetes.KubernetesUndeployService;
import org.eclipse.jkube.kit.config.service.openshift.OpenshiftUndeployService;
import org.eclipse.jkube.kit.config.service.plugins.PluginManager;
Expand Down Expand Up @@ -105,7 +109,7 @@ private void initLazyBuilders() {
kubernetesClientLazyBuilder = new LazyBuilder<>(() -> getClusterAccess().createDefaultClient());
buildServiceManager = new LazyBuilder<>(() -> new BuildServiceManager(this));
pluginManager = new LazyBuilder<>(() -> new PluginManager(this));
applyService = new LazyBuilder<>(() -> new ApplyService(getClient(), log));
applyService = new LazyBuilder<>(() -> new ApplyService(getClient(), createIngressControllerDetectorService(), log));
portForwardService = new LazyBuilder<>(() -> {
getClient();
return new PortForwardService(log);
Expand Down Expand Up @@ -181,4 +185,12 @@ public KubernetesClient getClient() {
public ClusterAccess getClusterAccess() {
return clusterAccessLazyBuilder.get();
}

private IngressControllerDetectorService createIngressControllerDetectorService() {
final PluginServiceFactory<KubernetesClient> pluginServiceFactory = new PluginServiceFactory<>(getClient());
List<IngressControllerDetector> ingressControllerDetectors = pluginServiceFactory.createServiceObjects("META-INF/jkube/ingress-detectors");
IngressControllerDetectorService ingressControllerDetectorService = new IngressControllerDetectorService(log);
ingressControllerDetectorService.setIngressControllerDetectors(ingressControllerDetectors);
return ingressControllerDetectorService;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Copyright (c) 2019 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at:
*
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.jkube.kit.config.service.ingresscontroller;

import io.fabric8.kubernetes.api.model.authorization.v1.SelfSubjectAccessReview;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.eclipse.jkube.kit.common.IngressControllerDetector;
import org.eclipse.jkube.kit.common.util.KubernetesHelper;

import static org.eclipse.jkube.kit.common.util.KubernetesHelper.createNewSelfSubjectAccessReview;
import static org.eclipse.jkube.kit.common.util.KubernetesHelper.hasAccessForAction;

public class ContourIngressControllerDetector implements IngressControllerDetector {
private final KubernetesClient client;
private static final String INGRESS_CONTOUR_NAMESPACE = "projectcontour";

public ContourIngressControllerDetector(KubernetesClient client) {
this.client = client;
}

@Override
public boolean hasIngressClass() {
return !client.network().v1().ingressClasses()
.withLabel("app.kubernetes.io/name", "contour")
.list()
.getItems()
.isEmpty();
}

@Override
public boolean isIngressControllerReady() {
return isComponentReady("envoy") &&
isComponentReady("contour");
}

@Override
public boolean hasPermissions() {
SelfSubjectAccessReview listIngressClassAccess = createNewSelfSubjectAccessReview("networking.k8s.io", "ingressclasses", null, "list");
SelfSubjectAccessReview listPodsAnyNamespaceAccess = createNewSelfSubjectAccessReview(null, "pods", INGRESS_CONTOUR_NAMESPACE, "list");

return hasAccessForAction(client, listIngressClassAccess) && hasAccessForAction(client, listPodsAnyNamespaceAccess);
}

private boolean isComponentReady(String componentName) {
return client.pods()
.inNamespace(INGRESS_CONTOUR_NAMESPACE)
.withLabel("app.kubernetes.io/component", componentName)
.list()
.getItems()
.stream()
.anyMatch(KubernetesHelper::isPodReady);
}
}
Loading

0 comments on commit 841dbbc

Please sign in to comment.