From 00b78c7b93b74703889166fe0bc2ed6b53eddeed Mon Sep 17 00:00:00 2001 From: Anirudh Date: Fri, 3 Feb 2017 23:03:47 -0800 Subject: [PATCH 1/3] Allow watching resources over HTTP if watching using websockets fails --- .../client/dsl/base/BaseOperation.java | 17 ++ .../client/dsl/internal/WatchHTTPManager.java | 279 ++++++++++++++++++ 2 files changed, 296 insertions(+) create mode 100644 kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/WatchHTTPManager.java diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java index 156fd2c1e5c..6ca83f42799 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java @@ -40,6 +40,7 @@ import io.fabric8.kubernetes.client.dsl.Reaper; import io.fabric8.kubernetes.client.dsl.Watchable; import io.fabric8.kubernetes.client.dsl.internal.WatchConnectionManager; +import io.fabric8.kubernetes.client.dsl.internal.WatchHTTPManager; import io.fabric8.kubernetes.client.utils.URLUtils; import java.io.File; @@ -649,6 +650,22 @@ public Watch watch(String resourceVersion, final Watcher watcher) throws Kube return watch; } catch (MalformedURLException e) { throw KubernetesClientException.launderThrowable(e); + } catch (KubernetesClientException ke) { + WatchHTTPManager watch = null; + try { + watch = new WatchHTTPManager( + client, + this, + resourceVersion, + watcher, + config.getWatchReconnectInterval(), + config.getWatchReconnectLimit(), + config.getConnectionTimeout() + ); + } catch (MalformedURLException e) { + throw KubernetesClientException.launderThrowable(e); + } + return watch; } } diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/WatchHTTPManager.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/WatchHTTPManager.java new file mode 100644 index 00000000000..a3652c0b7fb --- /dev/null +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/WatchHTTPManager.java @@ -0,0 +1,279 @@ +/** + * Copyright (C) 2017 Google, 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.dsl.internal; + +import static io.fabric8.kubernetes.client.utils.Utils.isNotNullOrEmpty; +import static java.net.HttpURLConnection.HTTP_GONE; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.api.model.Status; +import io.fabric8.kubernetes.api.model.WatchEvent; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.Watch; +import io.fabric8.kubernetes.client.Watcher; +import io.fabric8.kubernetes.client.dsl.base.BaseOperation; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import okhttp3.HttpUrl; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.logging.HttpLoggingInterceptor; +import okio.BufferedSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WatchHTTPManager> implements + Watch { + private static final Logger logger = LoggerFactory.getLogger(WatchHTTPManager.class); + private static final ObjectMapper mapper = new ObjectMapper(); + + private final BaseOperation baseOperation; + private final Watcher watcher; + private final AtomicBoolean forceClosed = new AtomicBoolean(); + private final AtomicReference resourceVersion; + private final int reconnectLimit; + private final int reconnectInterval; + + private final AtomicBoolean reconnectPending = new AtomicBoolean(false); + private final static int maxIntervalExponent = 5; // max 32x slowdown from base interval + private final URL requestUrl; + private final AtomicInteger currentReconnectAttempt = new AtomicInteger(0); + private OkHttpClient clonedClient; + + private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread ret = new Thread(r, "Executor for Watch " + System.identityHashCode(WatchHTTPManager.this)); + ret.setDaemon(true); + return ret; + } + }); + + public WatchHTTPManager(final OkHttpClient client, + final BaseOperation baseOperation, + final String version, final Watcher watcher, final int reconnectInterval, + final int reconnectLimit, long connectTimeout) + throws MalformedURLException { + + if (version == null) { + L currentList = baseOperation.list(); + this.resourceVersion = new AtomicReference<>(currentList.getMetadata().getResourceVersion()); + } else { + this.resourceVersion = new AtomicReference<>(version); + } + this.baseOperation = baseOperation; + this.watcher = watcher; + this.reconnectInterval = reconnectInterval; + this.reconnectLimit = reconnectLimit; + + OkHttpClient clonedClient = client.newBuilder() + .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS) + .readTimeout(0,TimeUnit.MILLISECONDS) + .cache(null) + .build(); + + // If we set the HttpLoggingInterceptor's logging level to Body (as it is by default), it does + // not let us stream responses from the server. + for (Interceptor i : clonedClient.networkInterceptors()) { + if (i instanceof HttpLoggingInterceptor) { + HttpLoggingInterceptor interceptor = (HttpLoggingInterceptor) i; + interceptor.setLevel(HttpLoggingInterceptor.Level.BASIC); + } + } + + this.clonedClient = clonedClient; + requestUrl = baseOperation.getNamespacedUrl(); + scheduleReconnect(); + } + + private final void runWatch() { + logger.debug("Watching via HTTP GET ... {}", this); + + HttpUrl.Builder httpUrlBuilder = HttpUrl.get(requestUrl).newBuilder(); + String labelQueryParam = baseOperation.getLabelQueryParam(); + if (isNotNullOrEmpty(labelQueryParam)) { + httpUrlBuilder.addQueryParameter("labelSelector", labelQueryParam); + } + + String fieldQueryString = baseOperation.getFieldQueryParam(); + String name = baseOperation.getName(); + if (name != null && name.length() > 0) { + if (fieldQueryString.length() > 0) { + fieldQueryString += ","; + } + fieldQueryString += "metadata.name=" + name; + } + + if (isNotNullOrEmpty(fieldQueryString)) { + httpUrlBuilder.addQueryParameter("fieldSelector", fieldQueryString); + } + + httpUrlBuilder + .addQueryParameter("resourceVersion", this.resourceVersion.get()) + .addQueryParameter("watch", "true"); + + final Request request = new Request.Builder() + .get() + .url(httpUrlBuilder.build()) + .addHeader("Origin", requestUrl.getProtocol() + "://" + requestUrl.getHost() + ":" + requestUrl.getPort()) + .build(); + + Response response = null; + try { + response = clonedClient.newCall(request).execute(); + BufferedSource source = response.body().source(); + while (!source.exhausted()) { + String message = source.readUtf8LineStrict(); + onMessage(message); + } + } catch (IOException e) { + logger.info("Watch connection close received. reason: {}", e.getMessage()); + } finally { + if (forceClosed.get()) { + logger.warn("Ignoring onClose for already closed/closing connection"); + return; + } + if (currentReconnectAttempt.get() >= reconnectLimit && reconnectLimit >= 0) { + watcher.onClose(new KubernetesClientException("Connection unexpectedly closed")); + return; + } + + // if we get here, the source is exhausted, so, we have lost our "watch". + // we must reconnect. + if (response != null) { + response.body().close(); + } + scheduleReconnect(); + } + } + + private void scheduleReconnect() { + logger.debug("Submitting reconnect task to the executor"); + // make sure that whichever thread calls this method, the tasks are + // performed serially in the executor. + executor.submit(new Runnable() { + @Override + public void run() { + if (!reconnectPending.compareAndSet(false, true)) { + logger.debug("Reconnect already scheduled"); + return; + } + try { + // actual reconnect only after the back-off time has passed, without + // blocking the thread + logger.debug("Scheduling reconnect task"); + executor.schedule(new Runnable() { + @Override + public void run() { + try { + WatchHTTPManager.this.runWatch(); + reconnectPending.set(false); + } catch (Exception e) { + // An unexpected error occurred and we didn't even get an onFailure callback. + logger.error("Exception in reconnect", e); + close(); + watcher.onClose(new KubernetesClientException("Unhandled exception in reconnect attempt", e)); + } + } + }, nextReconnectInterval(), TimeUnit.MILLISECONDS); + } catch (RejectedExecutionException e) { + logger.error("Exception in reconnect", e); + reconnectPending.set(false); + } + } + }); + } + + public void onMessage(String messageSource) throws IOException { + try { + WatchEvent event = mapper.readValue(messageSource, WatchEvent.class); + if (event.getObject() instanceof HasMetadata) { + @SuppressWarnings("unchecked") + T obj = (T) event.getObject(); + // Dirty cast - should always be valid though + String currentResourceVersion = resourceVersion.get(); + String newResourceVersion = ((HasMetadata) obj).getMetadata().getResourceVersion(); + if (currentResourceVersion.compareTo(newResourceVersion) < 0) { + resourceVersion.compareAndSet(currentResourceVersion, newResourceVersion); + } + Watcher.Action action = Watcher.Action.valueOf(event.getType()); + watcher.eventReceived(action, obj); + } else if (event.getObject() instanceof Status) { + Status status = (Status) event.getObject(); + + // The resource version no longer exists - this has to be handled by the caller. + if (status.getCode() == HTTP_GONE) { + // exception + // shut down executor, etc. + close(); + watcher.onClose(new KubernetesClientException(status)); + return; + } + + logger.error("Error received: {}", status.toString()); + } else { + logger.error("Unknown message received: {}", messageSource); + } + } catch (IOException e) { + logger.error("Could not deserialize watch event: {}", messageSource, e); + } catch (ClassCastException e) { + logger.error("Received wrong type of object for watch", e); + } catch (IllegalArgumentException e) { + logger.error("Invalid event type", e); + } + } + + private long nextReconnectInterval() { + int exponentOfTwo = currentReconnectAttempt.getAndIncrement(); + if (exponentOfTwo > maxIntervalExponent) + exponentOfTwo = maxIntervalExponent; + long ret = reconnectInterval * (1 << exponentOfTwo); + logger.info("Current reconnect backoff is " + ret + " milliseconds (T" + exponentOfTwo + ")"); + return ret; + } + + @Override + public void close() { + logger.debug("Force closing the watch {}", this); + forceClosed.set(true); + if (!executor.isShutdown()) { + try { + executor.shutdown(); + if (!executor.awaitTermination(1, TimeUnit.SECONDS)) { + logger.warn("Executor didn't terminate in time after shutdown in close(), killing it in: {}", this); + executor.shutdownNow(); + } + } catch (Throwable t) { + throw KubernetesClientException.launderThrowable(t); + } + } + } +} From edd59d51290c34f5bd840306200e62e31ceee73a Mon Sep 17 00:00:00 2001 From: Anirudh Date: Tue, 7 Feb 2017 14:32:51 -0800 Subject: [PATCH 2/3] Added tests --- .../client/dsl/base/BaseOperation.java | 12 +- .../client/dsl/base/OperationSupport.java | 4 +- .../client/dsl/internal/WatchHTTPManager.java | 16 ++- .../kubernetes/client/WatchOverHTTP.java | 136 ++++++++++++++++++ 4 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 kubernetes-client/src/test/java/io/fabric8/kubernetes/client/WatchOverHTTP.java diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java index 6ca83f42799..4a69a5171d8 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java @@ -18,6 +18,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.internal.readiness.Readiness; import io.fabric8.kubernetes.client.utils.Utils; +import java.net.ProtocolException; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -651,9 +652,15 @@ public Watch watch(String resourceVersion, final Watcher watcher) throws Kube } catch (MalformedURLException e) { throw KubernetesClientException.launderThrowable(e); } catch (KubernetesClientException ke) { - WatchHTTPManager watch = null; + if (ke.getCode() != 200) { + throw ke; + } + + // If the HTTP return code is 200, we retry the watch again using a persistent hanging + // HTTP GET. This is meant to handle cases like kubectl local proxy which does not support + // websockets. Issue: https://github.com/kubernetes/kubernetes/issues/25126 try { - watch = new WatchHTTPManager( + return new WatchHTTPManager( client, this, resourceVersion, @@ -665,7 +672,6 @@ public Watch watch(String resourceVersion, final Watcher watcher) throws Kube } catch (MalformedURLException e) { throw KubernetesClientException.launderThrowable(e); } - return watch; } } diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/OperationSupport.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/OperationSupport.java index b6ec49cdb76..ddd86e086a6 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/OperationSupport.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/OperationSupport.java @@ -302,7 +302,7 @@ public static Status createStatus(int statusCode, String message) { return status; } - KubernetesClientException requestFailure(Request request, Status status) { + public static KubernetesClientException requestFailure(Request request, Status status) { StringBuilder sb = new StringBuilder(); sb.append("Failure executing: ").append(request.method()) .append(" at: ").append(request.url().toString()).append("."); @@ -318,7 +318,7 @@ KubernetesClientException requestFailure(Request request, Status status) { return new KubernetesClientException(sb.toString(), status.getCode(), status); } - KubernetesClientException requestException(Request request, Exception e) { + public static KubernetesClientException requestException(Request request, Exception e) { StringBuilder sb = new StringBuilder(); sb.append("Error executing: ").append(request.method()) .append(" at: ").append(request.url().toString()) diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/WatchHTTPManager.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/WatchHTTPManager.java index a3652c0b7fb..0ed851e5f80 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/WatchHTTPManager.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/WatchHTTPManager.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2017 Google, Inc. + * 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.fabric8.kubernetes.client.dsl.internal; import static io.fabric8.kubernetes.client.utils.Utils.isNotNullOrEmpty; @@ -24,11 +23,13 @@ import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.api.model.Status; import io.fabric8.kubernetes.api.model.WatchEvent; +import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.Watch; import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.dsl.base.BaseOperation; +import io.fabric8.kubernetes.client.dsl.base.OperationSupport; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; @@ -111,7 +112,7 @@ public WatchHTTPManager(final OkHttpClient client, this.clonedClient = clonedClient; requestUrl = baseOperation.getNamespacedUrl(); - scheduleReconnect(); + runWatch(); } private final void runWatch() { @@ -149,12 +150,17 @@ private final void runWatch() { Response response = null; try { response = clonedClient.newCall(request).execute(); + if(!response.isSuccessful()) { + throw OperationSupport.requestFailure(request, + OperationSupport.createStatus(response.code(), response.message())); + } + BufferedSource source = response.body().source(); while (!source.exhausted()) { String message = source.readUtf8LineStrict(); onMessage(message); } - } catch (IOException e) { + } catch (Exception e) { logger.info("Watch connection close received. reason: {}", e.getMessage()); } finally { if (forceClosed.get()) { @@ -166,6 +172,7 @@ private final void runWatch() { return; } + // if we get here, the source is exhausted, so, we have lost our "watch". // we must reconnect. if (response != null) { @@ -228,7 +235,6 @@ public void onMessage(String messageSource) throws IOException { watcher.eventReceived(action, obj); } else if (event.getObject() instanceof Status) { Status status = (Status) event.getObject(); - // The resource version no longer exists - this has to be handled by the caller. if (status.getCode() == HTTP_GONE) { // exception diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/WatchOverHTTP.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/WatchOverHTTP.java new file mode 100644 index 00000000000..b684d4987ae --- /dev/null +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/WatchOverHTTP.java @@ -0,0 +1,136 @@ +/** + * 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 static org.junit.Assert.assertTrue; + +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodBuilder; +import io.fabric8.kubernetes.api.model.Status; +import io.fabric8.kubernetes.api.model.StatusBuilder; +import io.fabric8.kubernetes.api.model.WatchEvent; +import io.fabric8.kubernetes.api.model.WatchEventBuilder; +import io.fabric8.kubernetes.server.mock.KubernetesServer; +import java.net.HttpURLConnection; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import junit.framework.AssertionFailedError; +import org.junit.Rule; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WatchOverHTTP { + static final Pod pod1 = new PodBuilder().withNewMetadata().withNamespace("test").withName("pod1") + .withResourceVersion("1").endMetadata().build(); + static final Status outdatedStatus = new StatusBuilder().withCode(HttpURLConnection.HTTP_GONE) + .withMessage( + "401: The event in requested index is outdated and cleared (the requested history has been cleared [3/1]) [2]") + .build(); + static final WatchEvent outdatedEvent = new WatchEventBuilder().withStatusObject(outdatedStatus).build(); + final String path = "/api/v1/namespaces/test/pods?fieldSelector=metadata.name%3Dpod1&resourceVersion=1&watch=true"; + @Rule + public KubernetesServer server = new KubernetesServer(false); + Logger logger = LoggerFactory.getLogger(WatchTest.class); + + @Test + public void testDeleted() throws InterruptedException { + logger.info("testDeleted"); + KubernetesClient client = server.getClient().inNamespace("test"); + + server.expect() + .withPath(path) + .andReturn(200, "Failed WebSocket Connection").once(); + server.expect().withPath(path).andReturnChunked(200, + new WatchEvent(pod1, "DELETED"), "\n", + new WatchEvent(pod1, "ADDED"), "\n").once(); + + final CountDownLatch addLatch = new CountDownLatch(1); + final CountDownLatch deleteLatch = new CountDownLatch(1); + try (Watch watch = client.pods().withName("pod1").withResourceVersion("1").watch(new Watcher() { + @Override + public void eventReceived(Action action, Pod resource) { + switch (action) { + case DELETED: + deleteLatch.countDown(); + break; + case ADDED: + addLatch.countDown(); + break; + default: + throw new AssertionFailedError(); + } + } + + @Override + public void onClose(KubernetesClientException cause) {} + })) /* autoclose */ { + assertTrue(addLatch.await(10, TimeUnit.SECONDS)); + assertTrue(deleteLatch.await(10, TimeUnit.SECONDS)); + } + } + + @Test + public void testOutdated() throws InterruptedException { + logger.info("testOutdated"); + KubernetesClient client = server.getClient().inNamespace("test"); + + server.expect() + .withPath(path) + .andReturn(200, "Failed WebSocket Connection").once(); + server.expect().withPath(path).andReturnChunked(200, outdatedEvent, "\n").once(); + try (Watch watch = client.pods().withName("pod1").withResourceVersion("1").watch(new Watcher() { + @Override + public void eventReceived(Action action, Pod resource) { + throw new AssertionFailedError(); + } + + @Override + public void onClose(KubernetesClientException cause) { + throw new AssertionFailedError(); + } + })){}; + } + + @Test + public void testHttpErrorReconnect() throws InterruptedException { + logger.info("testHttpErrorReconnect"); + KubernetesClient client = server.getClient().inNamespace("test"); + + server.expect() + .withPath(path) + .andReturn(200, "Failed WebSocket Connection").once(); + server.expect().withPath(path).andReturnChunked(503, new StatusBuilder().withCode(503).build()).times(6); + server.expect().withPath(path).andReturnChunked(200, outdatedEvent, "\n").once(); + + final CountDownLatch closeLatch = new CountDownLatch(1); + try (Watch watch = client.pods().withName("pod1").withResourceVersion("1").watch(new Watcher() { + @Override + public void eventReceived(Action action, Pod resource) { + throw new AssertionFailedError(); + } + + @Override + public void onClose(KubernetesClientException cause) { + logger.debug("onClose", cause); + closeLatch.countDown(); + } + })) /* autoclose */ { + assertTrue(closeLatch.await(3, TimeUnit.MINUTES)); + } + } +} From 17e350bc3bdd48c7830a9f46275746ec8bc38dd4 Mon Sep 17 00:00:00 2001 From: Anirudh Date: Tue, 7 Feb 2017 18:05:27 -0800 Subject: [PATCH 3/3] Remove unneeded import --- .../io/fabric8/kubernetes/client/dsl/base/BaseOperation.java | 1 - 1 file changed, 1 deletion(-) diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java index 4a69a5171d8..0aee8826b08 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java @@ -18,7 +18,6 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.internal.readiness.Readiness; import io.fabric8.kubernetes.client.utils.Utils; -import java.net.ProtocolException; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request;