Skip to content

Commit

Permalink
fix fabric8io#887: adding visiting logic for walking api resources
Browse files Browse the repository at this point in the history
  • Loading branch information
shawkins committed Jul 19, 2022
1 parent 11d20aa commit 58df6d6
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#### Improvements
* Fix #4041: adding Quantity.getNumericalAmount with an explanation about bytes and cores.
* Fix #4241: added more context to informer logs with the endpoint path
* Fix #887: added KubernetesClient.visitResources to search and perform other operations across all resources.

#### Dependency Upgrade

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.fabric8.kubernetes.client;

import io.fabric8.kubernetes.api.model.APIResource;
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import io.fabric8.kubernetes.api.model.GenericKubernetesResourceList;
import io.fabric8.kubernetes.client.dsl.MixedOperation;
import io.fabric8.kubernetes.client.dsl.Resource;
import io.fabric8.kubernetes.client.utils.ApiVersionUtil;

/**
* Used to perform operations across all resources known to the api server.
*/
public interface ApiVisitor {

public enum ApiVisitResult {
/**
* If returned, stop the visiting process immediately
*/
TERMINATE,
/**
* Continue visiting
*/
CONTINUE,
/**
* Skip any visiting that would occur under this point
*/
SKIP
}

/**
* Visit the api. The core/legacy group will be an empty string.
*
* @param api the group name
* @return the result to control future actions. {@link ApiVisitResult#SKIP} will skip visiting all versions of this api.
*/
default ApiVisitResult visitApi(String api) {
return ApiVisitResult.CONTINUE;
}

/**
* Visit the apiVersion, which will be in the form of api/version or just v1 for the core.
* Use {@link ApiVersionUtil} to separate components if needed.
*
* @return the result to control future actions. {@link ApiVisitResult#SKIP} will skip visiting all resources under this
* apiVersion.
*/
default ApiVisitResult visitApiVersion(String apiVersion) {
return ApiVisitResult.CONTINUE;
}

/**
* Visit the resource.
*
* @param apiVersion in the form of api/version or just v1 for the core
* @param apiResource can be used to look at applicable verbs and other resource information
* @param operation the {@link GenericKubernetesResource} operation for the current resource. Will be in the namespace of the
* client by default.
* @return {@link ApiVisitResult#TERMINATE} to terminate. SKIP and CONTINUE will both continue visiting.
*/
ApiVisitResult visitResources(String apiVersion, APIResource apiResource,
MixedOperation<GenericKubernetesResource, GenericKubernetesResourceList, Resource<GenericKubernetesResource>> operation);

}
Original file line number Diff line number Diff line change
Expand Up @@ -511,4 +511,11 @@ NamespaceListVisitFromServerGetDeleteRecreateWaitApplicable<HasMetadata> resourc
* @return {@link NonNamespaceOperation} for RuntimeClass
*/
NonNamespaceOperation<RuntimeClass, RuntimeClassList, Resource<RuntimeClass>> runtimeClasses();

/**
* Visit all resources with the given {@link ApiVisitor}.
*
* @param visitor
*/
void visitResources(ApiVisitor visitor);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@
*/
package io.fabric8.kubernetes.client.dsl.base;

import io.fabric8.kubernetes.api.model.APIResource;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.KubernetesResource;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.utils.ApiVersionUtil;
import io.fabric8.kubernetes.client.utils.Utils;

import java.util.Optional;

public class ResourceDefinitionContext {
protected String group;
protected boolean namespaced;
Expand Down Expand Up @@ -59,18 +63,28 @@ protected void validate() {
public static ResourceDefinitionContext fromResourceType(Class<? extends KubernetesResource> resource) {
try {
return new Builder()
.withGroup(HasMetadata.getGroup(resource))
.withVersion(HasMetadata.getVersion(resource))
.withNamespaced(Utils.isResourceNamespaced(resource))
.withPlural(HasMetadata.getPlural(resource))
.withKind(HasMetadata.getKind(resource))
.build();
.withGroup(HasMetadata.getGroup(resource))
.withVersion(HasMetadata.getVersion(resource))
.withNamespaced(Utils.isResourceNamespaced(resource))
.withPlural(HasMetadata.getPlural(resource))
.withKind(HasMetadata.getKind(resource))
.build();
} catch (IllegalArgumentException e) {
throw new KubernetesClientException(
String.format("%s is not annotated appropriately: %s", resource.getName(), e.getMessage()), e);
}
}

public static ResourceDefinitionContext fromApiResource(String apiVersion, APIResource resource) {
return new Builder()
.withGroup(Optional.ofNullable(ApiVersionUtil.trimGroupOrNull(apiVersion)).orElse(""))
.withVersion(ApiVersionUtil.trimVersion(apiVersion))
.withNamespaced(Boolean.TRUE.equals(resource.getNamespaced()))
.withPlural(resource.getName())
.withKind(resource.getKind())
.build();
}

public static class Builder {
private final ResourceDefinitionContext resourceDefinitionContext;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@

package io.fabric8.kubernetes.client.dsl.base;

import io.fabric8.kubernetes.api.model.APIResourceBuilder;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

class ResourceDefinitionContextTest {

Expand All @@ -31,5 +33,17 @@ void missingRequiredKindShouldFail() {
ResourceDefinitionContext context = builder.build();
assertEquals("kinds", context.getPlural());
}


@Test
void fromCoreApiResource() {
ResourceDefinitionContext context = ResourceDefinitionContext.fromApiResource("v1",
new APIResourceBuilder().withKind("builtin").withName("builtins").withNamespaced(true).build());

assertEquals("builtins", context.getPlural());
assertEquals("builtin", context.getKind());
assertTrue(context.isNamespaceScoped());
assertEquals("", context.getGroup());
assertEquals("v1", context.getVersion());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.type.TypeFactory;
import io.fabric8.kubernetes.api.model.APIGroup;
import io.fabric8.kubernetes.api.model.APIGroupBuilder;
import io.fabric8.kubernetes.api.model.APIResource;
import io.fabric8.kubernetes.api.model.APIService;
import io.fabric8.kubernetes.api.model.APIServiceList;
import io.fabric8.kubernetes.api.model.Binding;
Expand All @@ -28,6 +31,8 @@
import io.fabric8.kubernetes.api.model.EndpointsList;
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import io.fabric8.kubernetes.api.model.GenericKubernetesResourceList;
import io.fabric8.kubernetes.api.model.GroupVersionForDiscovery;
import io.fabric8.kubernetes.api.model.GroupVersionForDiscoveryBuilder;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.KubernetesListBuilder;
import io.fabric8.kubernetes.api.model.KubernetesResourceList;
Expand Down Expand Up @@ -64,6 +69,7 @@
import io.fabric8.kubernetes.api.model.coordination.v1.LeaseList;
import io.fabric8.kubernetes.api.model.node.v1beta1.RuntimeClass;
import io.fabric8.kubernetes.api.model.node.v1beta1.RuntimeClassList;
import io.fabric8.kubernetes.client.ApiVisitor.ApiVisitResult;
import io.fabric8.kubernetes.client.KubernetesClientBuilder.ExecutorSupplier;
import io.fabric8.kubernetes.client.dsl.ApiextensionsAPIGroupDSL;
import io.fabric8.kubernetes.client.dsl.AppsAPIGroupDSL;
Expand Down Expand Up @@ -128,7 +134,9 @@

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

/**
* Class for Default Kubernetes Client implementing KubernetesClient interface.
Expand Down Expand Up @@ -683,4 +691,47 @@ public Client newClient(RequestConfig requestConfig) {
return result;
}

@Override
public void visitResources(ApiVisitor visitor) {
if (visitGroups(visitor, Arrays.asList(new APIGroupBuilder().withName("")
.withVersions(new GroupVersionForDiscoveryBuilder().withGroupVersion("v1").build()).build()))) {
return; // user terminated
}
visitGroups(visitor, getApiGroups().getGroups());
}

private boolean visitGroups(ApiVisitor visitor, List<APIGroup> groups) {
for (APIGroup group : groups) {
switch (visitor.visitApi(group.getName())) {
case TERMINATE:
return true;
case SKIP:
continue;
case CONTINUE:
for (GroupVersionForDiscovery groupForDiscovery : group.getVersions()) {
String groupVersion = groupForDiscovery.getGroupVersion();
ApiVisitResult versionResult = visitor.visitApiVersion(groupVersion);
switch (versionResult) {
case TERMINATE:
return true;
case SKIP:
continue;
case CONTINUE:
for (APIResource resource : this.getApiResources(groupVersion).getResources()) {
if (resource.getName().contains("/")) { // skip subresources
continue;
}
ApiVisitResult resourceResult = visitor.visitResources(groupVersion, resource,
this.genericKubernetesResources(ResourceDefinitionContext.fromApiResource(groupVersion, resource)));
if (resourceResult == ApiVisitResult.TERMINATE) {
return true;
}
}
}
}
}
}
return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,21 @@

import io.fabric8.kubernetes.api.model.APIGroup;
import io.fabric8.kubernetes.api.model.APIGroupList;
import io.fabric8.kubernetes.api.model.APIResource;
import io.fabric8.kubernetes.api.model.APIResourceList;
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import io.fabric8.kubernetes.api.model.GenericKubernetesResourceList;
import io.fabric8.kubernetes.client.ApiVisitor;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.dsl.MixedOperation;
import io.fabric8.kubernetes.client.dsl.Resource;
import org.junit.jupiter.api.Test;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand Down Expand Up @@ -58,4 +69,57 @@ void testApiResources() {

assertTrue(list.getResources().stream().anyMatch(r -> "configmaps".equals(r.getName())));
}

@Test
void testApiVisiting() {
APIGroupList list = client.getApiGroups();

AtomicInteger groupCount = new AtomicInteger();

client.visitResources(new ApiVisitor() {

@Override
public ApiVisitResult visitApi(String api) {
groupCount.incrementAndGet();
return ApiVisitResult.CONTINUE;
}

@Override
public ApiVisitResult visitResources(String apiVersion, APIResource apiResource,
MixedOperation<GenericKubernetesResource, GenericKubernetesResourceList, Resource<GenericKubernetesResource>> mixedOperation) {
return ApiVisitResult.CONTINUE;
}

});

// visit all groups + the core group
assertEquals(list.getGroups().size() + 1, groupCount.get());

// visit again to make sure we terminate as expected
CompletableFuture<Boolean> done = new CompletableFuture<>();
client.visitResources(new ApiVisitor() {

@Override
public ApiVisitResult visitApi(String api) {
if (api.isEmpty()) {
return ApiVisitResult.CONTINUE;
}
return ApiVisitResult.TERMINATE;
}

@Override
public ApiVisitResult visitResources(String apiVersion, APIResource apiResource,
MixedOperation<GenericKubernetesResource, GenericKubernetesResourceList, Resource<GenericKubernetesResource>> operation) {
assertFalse(done.isDone());
if (apiResource.getName().equals("configmaps")) {
done.complete(!operation.inAnyNamespace().list().getItems().isEmpty());
return ApiVisitResult.TERMINATE;
}
return ApiVisitResult.CONTINUE;
}

});

assertTrue(done.join());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ void log() {
assertNotNull(log);
}

@Test
public void execSomething() throws Exception {
client.pods().withName("pod-standard").waitUntilReady(POD_READY_WAIT_IN_SECONDS, TimeUnit.SECONDS);
ExecWatch exec = client
.pods()
.withName("pod-standard")
.exec("date");
}

@Test
void exec() throws Exception {
client.pods().withName("pod-standard").waitUntilReady(POD_READY_WAIT_IN_SECONDS, TimeUnit.SECONDS);
Expand Down

0 comments on commit 58df6d6

Please sign in to comment.