Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public abstract class AbstractOperatorExtension
public static final int DEFAULT_NAMESPACE_DELETE_TIMEOUT = 90;

private final KubernetesClient kubernetesClient;
private final KubernetesClient infrastructureKubernetesClient;
protected final List<HasMetadata> infrastructure;
protected Duration infrastructureTimeout;
protected final boolean oneNamespacePerClass;
Expand All @@ -56,10 +57,15 @@ protected AbstractOperatorExtension(
boolean preserveNamespaceOnError,
boolean waitForNamespaceDeletion,
KubernetesClient kubernetesClient,
KubernetesClient infrastructureKubernetesClient,
Function<ExtensionContext, String> namespaceNameSupplier,
Function<ExtensionContext, String> perClassNamespaceNameSupplier) {
this.infrastructureKubernetesClient =
infrastructureKubernetesClient != null
? infrastructureKubernetesClient
: new KubernetesClientBuilder().build();
this.kubernetesClient =
kubernetesClient != null ? kubernetesClient : new KubernetesClientBuilder().build();
kubernetesClient != null ? kubernetesClient : this.infrastructureKubernetesClient;
this.infrastructure = infrastructure;
this.infrastructureTimeout = infrastructureTimeout;
this.oneNamespacePerClass = oneNamespacePerClass;
Expand Down Expand Up @@ -94,6 +100,11 @@ public KubernetesClient getKubernetesClient() {
return kubernetesClient;
}

@Override
public KubernetesClient getInfrastructureKubernetesClient() {
return infrastructureKubernetesClient;
}

public String getNamespace() {
return namespace;
}
Expand Down Expand Up @@ -141,16 +152,16 @@ protected void beforeEachImpl(ExtensionContext context) {
protected void before(ExtensionContext context) {
LOGGER.info("Initializing integration test in namespace {}", namespace);

kubernetesClient
infrastructureKubernetesClient
.namespaces()
.resource(
new NamespaceBuilder()
.withMetadata(new ObjectMetaBuilder().withName(namespace).build())
.build())
.serverSideApply();

kubernetesClient.resourceList(infrastructure).serverSideApply();
kubernetesClient
infrastructureKubernetesClient.resourceList(infrastructure).serverSideApply();
infrastructureKubernetesClient
.resourceList(infrastructure)
.waitUntilReady(infrastructureTimeout.toMillis(), TimeUnit.MILLISECONDS);
}
Expand All @@ -172,16 +183,19 @@ protected void after(ExtensionContext context) {
if (preserveNamespaceOnError && context.getExecutionException().isPresent()) {
LOGGER.info("Preserving namespace {}", namespace);
} else {
kubernetesClient.resourceList(infrastructure).delete();
infrastructureKubernetesClient.resourceList(infrastructure).delete();
deleteOperator();
LOGGER.info("Deleting namespace {} and stopping operator", namespace);
kubernetesClient.namespaces().withName(namespace).delete();
infrastructureKubernetesClient.namespaces().withName(namespace).delete();
if (waitForNamespaceDeletion) {
LOGGER.info("Waiting for namespace {} to be deleted", namespace);
Awaitility.await("namespace deleted")
.pollInterval(50, TimeUnit.MILLISECONDS)
.atMost(namespaceDeleteTimeout, TimeUnit.SECONDS)
.until(() -> kubernetesClient.namespaces().withName(namespace).get() == null);
.until(
() ->
infrastructureKubernetesClient.namespaces().withName(namespace).get()
== null);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ private ClusterDeployedOperatorExtension(
boolean waitForNamespaceDeletion,
boolean oneNamespacePerClass,
KubernetesClient kubernetesClient,
KubernetesClient infrastructureKubernetesClient,
Function<ExtensionContext, String> namespaceNameSupplier,
Function<ExtensionContext, String> perClassNamespaceNameSupplier) {
super(
Expand All @@ -48,6 +49,7 @@ private ClusterDeployedOperatorExtension(
preserveNamespaceOnError,
waitForNamespaceDeletion,
kubernetesClient,
infrastructureKubernetesClient,
namespaceNameSupplier,
perClassNamespaceNameSupplier);
this.operatorDeployment = operatorDeployment;
Expand All @@ -69,7 +71,7 @@ protected void before(ExtensionContext context) {
final var crdPath = "./target/classes/META-INF/fabric8/";
final var crdSuffix = "-v1.yml";

final var kubernetesClient = getKubernetesClient();
final var kubernetesClient = getInfrastructureKubernetesClient();
for (var crdFile :
Objects.requireNonNull(
new File(crdPath).listFiles((ignored, name) -> name.endsWith(crdSuffix)))) {
Expand Down Expand Up @@ -107,13 +109,17 @@ protected void before(ExtensionContext context) {

@Override
protected void deleteOperator() {
getKubernetesClient().resourceList(operatorDeployment).inNamespace(namespace).delete();
getInfrastructureKubernetesClient()
.resourceList(operatorDeployment)
.inNamespace(namespace)
.delete();
}

public static class Builder extends AbstractBuilder<Builder> {
private final List<HasMetadata> operatorDeployment;
private Duration deploymentTimeout;
private KubernetesClient kubernetesClient;
private KubernetesClient infrastructureKubernetesClient;
Copy link

Copilot AI Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The infrastructureKubernetesClient field is missing a corresponding setter method in the Builder class, while kubernetesClient has one. This creates an inconsistent API.

Copilot uses AI. Check for mistakes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xstefank this seems to be legit

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx, my eyes skipped this comment


protected Builder() {
super();
Expand Down Expand Up @@ -150,7 +156,18 @@ public Builder withKubernetesClient(KubernetesClient kubernetesClient) {
return this;
}

public Builder withInfrastructureKubernetesClient(KubernetesClient kubernetesClient) {
this.infrastructureKubernetesClient = kubernetesClient;
return this;
}

public ClusterDeployedOperatorExtension build() {
infrastructureKubernetesClient =
infrastructureKubernetesClient != null
? infrastructureKubernetesClient
: new KubernetesClientBuilder().build();
kubernetesClient =
kubernetesClient != null ? kubernetesClient : infrastructureKubernetesClient;
return new ClusterDeployedOperatorExtension(
operatorDeployment,
deploymentTimeout,
Expand All @@ -159,7 +176,8 @@ public ClusterDeployedOperatorExtension build() {
preserveNamespaceOnError,
waitForNamespaceDeletion,
oneNamespacePerClass,
kubernetesClient != null ? kubernetesClient : new KubernetesClientBuilder().build(),
kubernetesClient,
infrastructureKubernetesClient,
namespaceNameSupplier,
perClassNamespaceNameSupplier);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,20 @@
import io.fabric8.kubernetes.client.KubernetesClient;

public interface HasKubernetesClient {
/**
* Returns the main Kubernetes client that is used to deploy the operator to the cluster.
*
* @return the main Kubernetes client
*/
KubernetesClient getKubernetesClient();

/**
* Returns the Kubernetes client that is used to deploy infrastructure resources to the cluster
* such as clusterroles, clusterrolebindings, etc. This client can be different from the main
* client in case you need to test the operator with a different restrictions more closely
* resembling the real restrictions it will have in production.
*
* @return the infrastructure Kubernetes client
*/
KubernetesClient getInfrastructureKubernetesClient();
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ private LocallyRunOperatorExtension(
boolean waitForNamespaceDeletion,
boolean oneNamespacePerClass,
KubernetesClient kubernetesClient,
KubernetesClient infrastructureKubernetesClient,
Consumer<ConfigurationServiceOverrider> configurationServiceOverrider,
Function<ExtensionContext, String> namespaceNameSupplier,
Function<ExtensionContext, String> perClassNamespaceNameSupplier,
Expand All @@ -78,6 +79,7 @@ private LocallyRunOperatorExtension(
preserveNamespaceOnError,
waitForNamespaceDeletion,
kubernetesClient,
infrastructureKubernetesClient,
namespaceNameSupplier,
perClassNamespaceNameSupplier);
this.reconcilers = reconcilers;
Expand Down Expand Up @@ -240,7 +242,7 @@ public Operator getOperator() {
protected void before(ExtensionContext context) {
super.before(context);

final var kubernetesClient = getKubernetesClient();
final var kubernetesClient = getInfrastructureKubernetesClient();

for (var ref : portForwards) {
String podName =
Expand Down Expand Up @@ -313,7 +315,7 @@ protected void before(ExtensionContext context) {
protected void after(ExtensionContext context) {
super.after(context);

var kubernetesClient = getKubernetesClient();
var kubernetesClient = getInfrastructureKubernetesClient();

var iterator = appliedCRDs.iterator();
while (iterator.hasNext()) {
Expand Down Expand Up @@ -365,6 +367,7 @@ public static class Builder extends AbstractBuilder<Builder> {
private final List<String> additionalCRDs = new ArrayList<>();
private Consumer<LocallyRunOperatorExtension> beforeStartHook;
private KubernetesClient kubernetesClient;
private KubernetesClient infrastructureKubernetesClient;

protected Builder() {
super();
Expand Down Expand Up @@ -419,6 +422,12 @@ public Builder withKubernetesClient(KubernetesClient kubernetesClient) {
return this;
}

public Builder withInfrastructureKubernetesClient(
KubernetesClient infrastructureKubernetesClient) {
this.infrastructureKubernetesClient = infrastructureKubernetesClient;
return this;
}

public Builder withAdditionalCustomResourceDefinition(
Class<? extends CustomResource> customResource) {
additionalCustomResourceDefinitions.add(customResource);
Expand Down Expand Up @@ -452,6 +461,7 @@ public LocallyRunOperatorExtension build() {
waitForNamespaceDeletion,
oneNamespacePerClass,
kubernetesClient,
infrastructureKubernetesClient,
configurationServiceOverrider,
namespaceNameSupplier,
perClassNamespaceNameSupplier,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package io.javaoperatorsdk.operator.baseapi.infrastructureclient;

import java.util.concurrent.TimeUnit;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.rbac.ClusterRole;
import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBinding;
import io.fabric8.kubernetes.client.ConfigBuilder;
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.javaoperatorsdk.operator.ReconcilerUtils;
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.awaitility.Awaitility.await;

class InfrastructureClientIT {

private static final String RBAC_TEST_ROLE = "rbac-test-role.yaml";
private static final String RBAC_TEST_ROLE_BINDING = "rbac-test-role-binding.yaml";
private static final String RBAC_TEST_USER = "rbac-test-user";

@RegisterExtension
LocallyRunOperatorExtension operator =
LocallyRunOperatorExtension.builder()
.withReconciler(new InfrastructureClientTestReconciler())
.withKubernetesClient(
new KubernetesClientBuilder()
.withConfig(new ConfigBuilder().withImpersonateUsername(RBAC_TEST_USER).build())
.build())
.withInfrastructureKubernetesClient(
new KubernetesClientBuilder().build()) // no limitations
.build();

/**
* We need to apply the cluster role also before the CRD deployment so the rbac-test-user is
* permitted to deploy it
*/
public InfrastructureClientIT() {
applyClusterRole(RBAC_TEST_ROLE);
applyClusterRoleBinding(RBAC_TEST_ROLE_BINDING);
}

@BeforeEach
void setup() {
applyClusterRole(RBAC_TEST_ROLE);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are I guess redundant. The one from constructor might be an another hook I guess?!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the problem I mentioned to you on the call. If I removed this BeforeEach, the cluster roles wouldn't be applied for the second test.

applyClusterRoleBinding(RBAC_TEST_ROLE_BINDING);
}

@AfterEach
void cleanup() {
removeClusterRoleBinding(RBAC_TEST_ROLE_BINDING);
removeClusterRole(RBAC_TEST_ROLE);
}

@Test
void canCreateInfrastructure() {
var resource = new InfrastructureClientTestCustomResource();
resource.setMetadata(
new ObjectMetaBuilder().withName("infrastructure-client-resource").build());
operator.create(resource);

await()
.atMost(5, TimeUnit.SECONDS)
.untilAsserted(
() -> {
InfrastructureClientTestCustomResource r =
operator.get(
InfrastructureClientTestCustomResource.class,
"infrastructure-client-resource");
assertThat(r).isNotNull();
});

assertThat(
operator
.getReconcilerOfType(InfrastructureClientTestReconciler.class)
.getNumberOfExecutions())
.isEqualTo(1);
}

@Test
void shouldNotAccessNotPermittedResources() {
assertThatThrownBy(
() ->
operator
.getKubernetesClient()
.apiextensions()
.v1()
.customResourceDefinitions()
.list())
.isInstanceOf(KubernetesClientException.class)
.hasMessageContaining(
"User \"%s\" cannot list resource \"customresourcedefinitions\""
.formatted(RBAC_TEST_USER));

// but we should be able to access all resources with the infrastructure client
var deploymentList =
operator
.getInfrastructureKubernetesClient()
.apiextensions()
.v1()
.customResourceDefinitions()
.list();
assertThat(deploymentList).isNotNull();
}

private void applyClusterRoleBinding(String filename) {
var clusterRoleBinding =
ReconcilerUtils.loadYaml(ClusterRoleBinding.class, this.getClass(), filename);
operator.getInfrastructureKubernetesClient().resource(clusterRoleBinding).serverSideApply();
}

private void applyClusterRole(String filename) {
var clusterRole = ReconcilerUtils.loadYaml(ClusterRole.class, this.getClass(), filename);
operator.getInfrastructureKubernetesClient().resource(clusterRole).serverSideApply();
}

private void removeClusterRoleBinding(String filename) {
var clusterRoleBinding =
ReconcilerUtils.loadYaml(ClusterRoleBinding.class, this.getClass(), filename);
operator.getInfrastructureKubernetesClient().resource(clusterRoleBinding).delete();
}

private void removeClusterRole(String filename) {
var clusterRole = ReconcilerUtils.loadYaml(ClusterRole.class, this.getClass(), filename);
operator.getInfrastructureKubernetesClient().resource(clusterRole).delete();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.javaoperatorsdk.operator.baseapi.infrastructureclient;

import io.fabric8.kubernetes.api.model.Namespaced;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.model.annotation.Group;
import io.fabric8.kubernetes.model.annotation.ShortNames;
import io.fabric8.kubernetes.model.annotation.Version;

@Group("sample.javaoperatorsdk")
@Version("v1")
@ShortNames("ict")
public class InfrastructureClientTestCustomResource extends CustomResource<Void, Void>
implements Namespaced {}
Loading