Skip to content

Commit

Permalink
refactor: properly extract resource metadata from URI on HTTP request…
Browse files Browse the repository at this point in the history
… failures

Signed-off-by: Marc Nuri <marc@marcnuri.com>
  • Loading branch information
manusa committed Jul 27, 2022
1 parent 19a7d39 commit fc76146
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 152 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,54 +17,70 @@

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Status;
import io.fabric8.kubernetes.client.http.HttpRequest;
import io.fabric8.kubernetes.client.utils.Utils;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;

import java.io.Serializable;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class KubernetesClientException extends RuntimeException {

private int code;
private Status status;
private String group;
private String version;
private String resourcePlural;
private String namespace;
private String name;
private final int code;
private final Status status;
private final RequestMetadata requestMetadata;

public KubernetesClientException(String message) {
super(message);
this(message, null);
}

public KubernetesClientException(String message, Throwable t) {
super(message, t);
this(message, t, -1, null, (RequestMetadata) null);
}

public KubernetesClientException(Status status) {
this(status.getMessage(), status.getCode(), status);
}

public KubernetesClientException(String message, int code, Status status) {
this(message, code, status, null, null, null, null, null);
this(message, null, code, status, (RequestMetadata) null);
}

public KubernetesClientException(String message, int code, Status status, String group, String version, String resourcePlural,
String namespace, String name) {
super(message);
public KubernetesClientException(String message, Throwable t, int code, Status status, HttpRequest httpRequest) {
this(message, t, code, status, RequestMetadata.from(httpRequest));
}

private KubernetesClientException(String message, Throwable t, int code, Status status, RequestMetadata requestMetadata) {
super(message, t);
this.code = code;
this.status = status;
this.group = group;
this.version = version;
this.resourcePlural = resourcePlural;
this.namespace = namespace;
this.name = name;
this.requestMetadata = requestMetadata;
}

/**
* @deprecated use {@link #KubernetesClientException(String, Throwable, int, Status, HttpRequest)} instead
*/
@Deprecated
public KubernetesClientException(String message, int code, Status status, String group, String version, String resourcePlural,
String namespace) {
this(message, null, code, status,
RequestMetadata.builder().group(group).version(version).plural(resourcePlural).namespace(namespace).build());
}

/**
* @deprecated use {@link #KubernetesClientException(String, Throwable, int, Status, HttpRequest)} instead
*/
@Deprecated
public KubernetesClientException(String message, Throwable t, String group, String version, String resourcePlural,
String namespace, String name) {
super(message, t);
this.group = group;
this.version = version;
this.resourcePlural = resourcePlural;
this.namespace = namespace;
this.name = name;
String namespace) {
this(message, t, -1, null,
RequestMetadata.builder().group(group).version(version).plural(resourcePlural).namespace(namespace).build());
}

public Status getStatus() {
Expand All @@ -76,28 +92,28 @@ public int getCode() {
}

public String getGroup() {
return group;
return requestMetadata.group;
}

public String getVersion() {
return version;
return requestMetadata.version;
}

public String getResourcePlural() {
return resourcePlural;
return requestMetadata.plural;
}

public String getNamespace() {
return namespace;
return requestMetadata.namespace;
}

public String getName() {
return name;
return requestMetadata.name;
}

public String getFullResourceName() {
if (resourcePlural != null && group != null) {
return HasMetadata.getFullResourceName(resourcePlural, group);
if (requestMetadata.plural != null && requestMetadata.group != null) {
return HasMetadata.getFullResourceName(requestMetadata.plural, requestMetadata.group);
}
return null;
}
Expand Down Expand Up @@ -138,8 +154,9 @@ public static RuntimeException launderThrowable(OperationInfo spec, Throwable ca
}
}

throw new KubernetesClientException(sb.toString(), cause, spec.getGroup(), spec.getVersion(), spec.getPlural(),
spec.getNamespace(), spec.getName());
throw new KubernetesClientException(sb.toString(), cause, -1, null, RequestMetadata.builder()
.group(spec.getGroup()).version(spec.getVersion()).plural(spec.getPlural())
.namespace(spec.getNamespace()).name(spec.getName()).build());
}

/**
Expand Down Expand Up @@ -167,4 +184,50 @@ private static String describeOperation(OperationInfo operation) {
sb.append(" in namespace: [").append(operation.getNamespace()).append("] ");
return sb.toString();
}

@Builder
@EqualsAndHashCode
@AllArgsConstructor(access = AccessLevel.PRIVATE)
private static class RequestMetadata implements Serializable {

private static final RequestMetadata EMPTY = new RequestMetadata(null, null, null, null, null);

final String group;
final String version;
final String plural;
final String namespace;
final String name;

static RequestMetadata from(HttpRequest request) {
return from(request.uri());
}

static RequestMetadata from(URI request) {
final List<String> segments = Arrays.stream(request.getRawPath().split("/"))
.filter(s -> !s.isEmpty()).collect(Collectors.toList());
switch (segments.size()) {
case 3:
// cluster URL for historic resources
return RequestMetadata.builder().group("").version(segments.get(1)).plural(segments.get(2)).build();
case 4:
// cluster URL
return RequestMetadata.builder().group(segments.get(1)).version(segments.get(2)).plural(segments.get(3)).build();
case 6:
// namespaced URL with potential name
final String root = segments.get(0);
if ("api".equals(root)) {
return RequestMetadata.builder().group("").version(segments.get(1)).plural(segments.get(4))
.namespace(segments.get(3)).name(segments.get(5)).build();
}
return RequestMetadata.builder().group(segments.get(1)).version(segments.get(2)).plural(segments.get(5))
.namespace(segments.get(4)).build();
case 7:
// namespaced URL with name
return RequestMetadata.builder().group(segments.get(1)).version(segments.get(2)).plural(segments.get(5))
.namespace(segments.get(4)).name(segments.get(6)).build();
default:
return EMPTY;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Copyright (C) 2015 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.kubernetes.client;

import io.fabric8.kubernetes.client.http.TestHttpRequest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.params.provider.Arguments.arguments;

class KubernetesClientExceptionTest {

@DisplayName("Exception from HttpRequest contains resource metadata")
@ParameterizedTest(name = "{index} ''{0}'': group: ''{1}'', version: ''{2}'', plural: ''{3}'', namespace: ''{4}'', name: ''{5}''")
@MethodSource("exceptionFromHttpRequestContainsExpectedMetadataInput")
void exceptionFromHttpRequestContainsExpectedMetadata(String url, String expectedGroup, String expectedVersion,
String expectedPlural, String expectedNamespace, String expectedName) {
final KubernetesClientException result = new KubernetesClientException(
null, null, -1, null, new TestHttpRequest().withUri(url));
assertThat(result)
.hasFieldOrPropertyWithValue("group", expectedGroup)
.hasFieldOrPropertyWithValue("version", expectedVersion)
.hasFieldOrPropertyWithValue("resourcePlural", expectedPlural)
.hasFieldOrPropertyWithValue("namespace", expectedNamespace)
.hasFieldOrPropertyWithValue("name", expectedName);
}

static Stream<Arguments> exceptionFromHttpRequestContainsExpectedMetadataInput() {
return Stream.of(
arguments("https://localhost:8080/apis/apps/v1/deployments", "apps", "v1", "deployments", null, null),
arguments("https://localhost:8080/api/v1/pods", "", "v1", "pods", null, null),
arguments("https://localhost:8080/apis/apps/v1/namespaces/foo/deployments",
"apps", "v1", "deployments", "foo", null),
arguments("https://localhost:8080/api/v1/namespaces/kube-system/pods/coredns-78fcd69978-7ls8f",
"", "v1", "pods", "kube-system", "coredns-78fcd69978-7ls8f"),
arguments("https://localhost:8080/apis/apps/v1/namespaces/foo/deployments/bar-78fcd69978-7ls8f",
"apps", "v1", "deployments", "foo", "bar-78fcd69978-7ls8f"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,8 @@
import java.io.InterruptedIOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand All @@ -63,8 +61,6 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class OperationSupport {

Expand Down Expand Up @@ -714,9 +710,7 @@ public static KubernetesClientException requestFailure(HttpRequest request, Stat
sb.append(" Received status: ").append(status).append(".");
}

final RequestMetadata metadata = RequestMetadata.from(request);
return new KubernetesClientException(sb.toString(), status.getCode(), status, metadata.group, metadata.version,
metadata.plural, metadata.namespace, metadata.name);
return new KubernetesClientException(sb.toString(), null, status.getCode(), status, request);
}

public static KubernetesClientException requestException(HttpRequest request, Throwable e, String message) {
Expand All @@ -729,85 +723,13 @@ public static KubernetesClientException requestException(HttpRequest request, Th
.append(" at: ").append(request.uri())
.append(". Cause: ").append(e.getMessage());

final RequestMetadata metadata = RequestMetadata.from(request);
return new KubernetesClientException(sb.toString(), e, metadata.group, metadata.version, metadata.plural,
metadata.namespace, metadata.name);
return new KubernetesClientException(sb.toString(), e, -1, null, request);
}

public static KubernetesClientException requestException(HttpRequest request, Exception e) {
return requestException(request, e, null);
}

static class RequestMetadata {
final String group;
final String version;
final String plural;
final String namespace;
final String name;
private static final RequestMetadata EMPTY = new RequestMetadata(null, null, null, null, null);

RequestMetadata(String group, String version, String plural, String namespace, String name) {
this.group = group;
this.version = version;
this.plural = plural;
this.namespace = namespace;
this.name = name;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RequestMetadata that = (RequestMetadata) o;
return Objects.equals(group, that.group) && Objects.equals(version,
that.version) && Objects.equals(plural, that.plural)
&& Objects.equals(
namespace, that.namespace)
&& Objects.equals(name, that.name);
}

@Override
public int hashCode() {
return Objects.hash(group, version, plural, namespace, name);
}

static RequestMetadata from(HttpRequest request) {
return from(request.uri());
}

static RequestMetadata from(URI request) {
final List<String> segments = Arrays.stream(request.getRawPath().split("/"))
.filter(s -> !s.isEmpty()).collect(Collectors.toList());
switch (segments.size()) {
case 3:
// cluster URL for historic resources
return new RequestMetadata("", segments.get(1), segments.get(2), null, null);
case 4:
// cluster URL
return new RequestMetadata(segments.get(1), segments.get(2), segments.get(3), null, null);
case 6:
// namespaced URL with potential name
final String root = segments.get(0);
if ("api".equals(root)) {
return new RequestMetadata("", segments.get(1), segments.get(4), segments.get(3),
segments.get(5));
}
return new RequestMetadata(segments.get(1), segments.get(2), segments.get(5),
segments.get(4), null);
case 7:
// namespaced URL with name
return new RequestMetadata(segments.get(1), segments.get(2), segments.get(5),
segments.get(4), segments.get(6));
default:
return EMPTY;
}
}
}

protected static <T> T unmarshal(InputStream is) {
return Serialization.unmarshal(is);
}
Expand Down
Loading

0 comments on commit fc76146

Please sign in to comment.