001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2017-2018 microBean. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 014 * implied. See the License for the specific language governing 015 * permissions and limitations under the License. 016 */ 017package org.microbean.kubernetes.controller; 018 019import java.io.Closeable; 020import java.io.IOException; 021 022import java.lang.reflect.Field; 023import java.lang.reflect.Method; 024 025import java.time.Duration; 026 027import java.time.temporal.ChronoUnit; 028 029import java.util.ArrayList; 030import java.util.Collection; 031import java.util.Collections; 032import java.util.Objects; 033import java.util.Map; 034 035import java.util.concurrent.Executors; 036import java.util.concurrent.ScheduledExecutorService; 037import java.util.concurrent.ScheduledFuture; 038import java.util.concurrent.TimeUnit; 039 040import java.util.logging.Level; 041import java.util.logging.Logger; 042 043import io.fabric8.kubernetes.client.DefaultKubernetesClient; // for javadoc only 044import io.fabric8.kubernetes.client.KubernetesClientException; 045import io.fabric8.kubernetes.client.Watcher; 046 047import io.fabric8.kubernetes.client.dsl.base.BaseOperation; 048import io.fabric8.kubernetes.client.dsl.base.OperationSupport; 049 050import io.fabric8.kubernetes.client.dsl.Listable; 051import io.fabric8.kubernetes.client.dsl.Versionable; 052import io.fabric8.kubernetes.client.dsl.VersionWatchable; 053import io.fabric8.kubernetes.client.dsl.Watchable; 054 055import io.fabric8.kubernetes.client.dsl.internal.CustomResourceOperationsImpl; 056 057import io.fabric8.kubernetes.api.model.HasMetadata; 058import io.fabric8.kubernetes.api.model.ObjectMeta; 059import io.fabric8.kubernetes.api.model.KubernetesResourceList; 060import io.fabric8.kubernetes.api.model.ListMeta; 061 062import net.jcip.annotations.GuardedBy; 063import net.jcip.annotations.ThreadSafe; 064 065import okhttp3.OkHttpClient; 066 067import org.microbean.development.annotation.Issue; 068import org.microbean.development.annotation.NonBlocking; 069 070/** 071 * A pump of sorts that continuously "pulls" logical events out of 072 * Kubernetes and {@linkplain EventCache#add(Object, AbstractEvent.Type, 073 * HasMetadata) adds them} to an {@link EventCache} so as to logically 074 * "reflect" the contents of Kubernetes into the cache. 075 * 076 * <h2>Thread Safety</h2> 077 * 078 * <p>Instances of this class are safe for concurrent use by multiple 079 * {@link Thread}s.</p> 080 * 081 * @param <T> a type of Kubernetes resource 082 * 083 * @author <a href="https://about.me/lairdnelson" 084 * target="_parent">Laird Nelson</a> 085 * 086 * @see EventCache 087 */ 088@ThreadSafe 089public class Reflector<T extends HasMetadata> implements Closeable { 090 091 092 /* 093 * Instance fields. 094 */ 095 096 097 /** 098 * The operation that was supplied at construction time. 099 * 100 * <p>This field is never {@code null}.</p> 101 * 102 * <p>It is guaranteed that the value of this field may be 103 * assignable to a reference of type {@link Listable Listable<? 104 * extends KubernetesResourceList>} or to a reference of type 105 * {@link VersionWatchable VersionWatchable<? extends Closeable, 106 * Watcher<T>>}.</p> 107 * 108 * @see Listable 109 * 110 * @see VersionWatchable 111 */ 112 private final Object operation; 113 114 /** 115 * The resource version 116 */ 117 private volatile Object lastSynchronizationResourceVersion; 118 119 private final ScheduledExecutorService synchronizationExecutorService; 120 121 @GuardedBy("this") 122 private ScheduledFuture<?> synchronizationTask; 123 124 private final boolean shutdownSynchronizationExecutorServiceOnClose; 125 126 private final long synchronizationIntervalInSeconds; 127 128 @GuardedBy("this") 129 private Closeable watch; 130 131 @GuardedBy("itself") 132 private final EventCache<T> eventCache; 133 134 /** 135 * A {@link Logger} for use by this {@link Reflector}. 136 * 137 * <p>This field is never {@code null}.</p> 138 * 139 * @see #createLogger() 140 */ 141 protected final Logger logger; 142 143 144 /* 145 * Constructors. 146 */ 147 148 149 /** 150 * Creates a new {@link Reflector}. 151 * 152 * @param <X> a type that is both an appropriate kind of {@link 153 * Listable} and {@link VersionWatchable}, such as the kind of 154 * operation returned by {@link 155 * DefaultKubernetesClient#configMaps()} and the like 156 * 157 * @param operation a {@link Listable} and a {@link 158 * VersionWatchable} that can report information from a Kubernetes 159 * cluster; must not be {@code null} 160 * 161 * @param eventCache an {@link EventCache} <strong>that will be 162 * synchronized on</strong> and into which {@link Event}s will be 163 * logically "reflected"; must not be {@code null} 164 * 165 * @exception NullPointerException if {@code operation} or {@code 166 * eventCache} is {@code null} 167 * 168 * @exception IllegalStateException if the {@link #createLogger()} 169 * method returns {@code null} 170 * 171 * @see #Reflector(Listable, EventCache, ScheduledExecutorService, 172 * Duration) 173 * 174 * @see #start() 175 */ 176 @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types 177 public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Reflector(final X operation, 178 final EventCache<T> eventCache) { 179 this(operation, eventCache, null, null); 180 } 181 182 /** 183 * Creates a new {@link Reflector}. 184 * 185 * @param <X> a type that is both an appropriate kind of {@link 186 * Listable} and {@link VersionWatchable}, such as the kind of 187 * operation returned by {@link 188 * DefaultKubernetesClient#configMaps()} and the like 189 * 190 * @param operation a {@link Listable} and a {@link 191 * VersionWatchable} that can report information from a Kubernetes 192 * cluster; must not be {@code null} 193 * 194 * @param eventCache an {@link EventCache} <strong>that will be 195 * synchronized on</strong> and into which {@link Event}s will be 196 * logically "reflected"; must not be {@code null} 197 * 198 * @param synchronizationInterval a {@link Duration} representing 199 * the time in between one {@linkplain EventCache#synchronize() 200 * synchronization operation} and another; interpreted with a 201 * granularity of seconds; may be {@code null} or semantically equal 202 * to {@code 0} seconds in which case no synchronization will occur 203 * 204 * @exception NullPointerException if {@code operation} or {@code 205 * eventCache} is {@code null} 206 * 207 * @exception IllegalStateException if the {@link #createLogger()} 208 * method returns {@code null} 209 * 210 * @see #Reflector(Listable, EventCache, ScheduledExecutorService, 211 * Duration) 212 * 213 * @see #start() 214 */ 215 @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types 216 public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Reflector(final X operation, 217 final EventCache<T> eventCache, 218 final Duration synchronizationInterval) { 219 this(operation, eventCache, null, synchronizationInterval); 220 } 221 222 /** 223 * Creates a new {@link Reflector}. 224 * 225 * @param <X> a type that is both an appropriate kind of {@link 226 * Listable} and {@link VersionWatchable}, such as the kind of 227 * operation returned by {@link 228 * DefaultKubernetesClient#configMaps()} and the like 229 * 230 * @param operation a {@link Listable} and a {@link 231 * VersionWatchable} that can report information from a Kubernetes 232 * cluster; must not be {@code null} 233 * 234 * @param eventCache an {@link EventCache} <strong>that will be 235 * synchronized on</strong> and into which {@link Event}s will be 236 * logically "reflected"; must not be {@code null} 237 * 238 * @param synchronizationExecutorService a {@link 239 * ScheduledExecutorService} to be used to tell the supplied {@link 240 * EventCache} to {@linkplain EventCache#synchronize() synchronize} 241 * on a schedule; may be {@code null} in which case no 242 * synchronization will occur 243 * 244 * @param synchronizationInterval a {@link Duration} representing 245 * the time in between one {@linkplain EventCache#synchronize() 246 * synchronization operation} and another; may be {@code null} in 247 * which case no synchronization will occur 248 * 249 * @exception NullPointerException if {@code operation} or {@code 250 * eventCache} is {@code null} 251 * 252 * @exception IllegalStateException if the {@link #createLogger()} 253 * method returns {@code null} 254 * 255 * @see #start() 256 */ 257 @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types 258 public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Reflector(final X operation, 259 final EventCache<T> eventCache, 260 final ScheduledExecutorService synchronizationExecutorService, 261 final Duration synchronizationInterval) { 262 super(); 263 this.logger = this.createLogger(); 264 if (this.logger == null) { 265 throw new IllegalStateException("createLogger() == null"); 266 } 267 final String cn = this.getClass().getName(); 268 final String mn = "<init>"; 269 if (this.logger.isLoggable(Level.FINER)) { 270 this.logger.entering(cn, mn, new Object[] { operation, eventCache, synchronizationExecutorService, synchronizationInterval }); 271 } 272 Objects.requireNonNull(operation); 273 this.eventCache = Objects.requireNonNull(eventCache); 274 // TODO: research: maybe: operation.withField("metadata.resourceVersion", "0")? 275 this.operation = withResourceVersion(operation, "0"); 276 277 if (synchronizationInterval == null) { 278 this.synchronizationIntervalInSeconds = 0L; 279 } else { 280 this.synchronizationIntervalInSeconds = synchronizationInterval.get(ChronoUnit.SECONDS); 281 } 282 if (this.synchronizationIntervalInSeconds <= 0L) { 283 this.synchronizationExecutorService = null; 284 this.shutdownSynchronizationExecutorServiceOnClose = false; 285 } else if (synchronizationExecutorService == null) { 286 this.synchronizationExecutorService = Executors.newScheduledThreadPool(1); 287 this.shutdownSynchronizationExecutorServiceOnClose = true; 288 } else { 289 this.synchronizationExecutorService = synchronizationExecutorService; 290 this.shutdownSynchronizationExecutorServiceOnClose = false; 291 } 292 if (this.logger.isLoggable(Level.FINER)) { 293 this.logger.exiting(cn, mn); 294 } 295 } 296 297 298 /* 299 * Instance methods. 300 */ 301 302 303 /** 304 * Returns a {@link Logger} that will be used for this {@link 305 * Reflector}. 306 * 307 * <p>This method never returns {@code null}.</p> 308 * 309 * <p>Overrides of this method must not return {@code null}.</p> 310 * 311 * @return a non-{@code null} {@link Logger} 312 */ 313 protected Logger createLogger() { 314 return Logger.getLogger(this.getClass().getName()); 315 } 316 317 /** 318 * Notionally closes this {@link Reflector} by terminating any 319 * {@link Thread}s that it has started and invoking the {@link 320 * #onClose()} method while holding this {@link Reflector}'s 321 * monitor. 322 * 323 * @exception IOException if an error occurs 324 * 325 * @see #onClose() 326 */ 327 @Override 328 public synchronized final void close() throws IOException { 329 final String cn = this.getClass().getName(); 330 final String mn = "close"; 331 if (this.logger.isLoggable(Level.FINER)) { 332 this.logger.entering(cn, mn); 333 } 334 try { 335 this.closeSynchronizationExecutorService(); 336 if (this.watch != null) { 337 this.watch.close(); 338 } 339 } finally { 340 this.onClose(); 341 } 342 if (this.logger.isLoggable(Level.FINER)) { 343 this.logger.exiting(cn, mn); 344 } 345 } 346 347 private synchronized final void closeSynchronizationExecutorService() { 348 final String cn = this.getClass().getName(); 349 final String mn = "closeSynchronizationExecutorService"; 350 if (this.logger.isLoggable(Level.FINER)) { 351 this.logger.entering(cn, mn); 352 } 353 if (this.synchronizationExecutorService != null && this.shutdownSynchronizationExecutorServiceOnClose) { 354 355 // Stop accepting new tasks. Not that any will be showing up 356 // anyway, but it's the right thing to do. 357 this.synchronizationExecutorService.shutdown(); 358 359 // Nicely cancel our task. 360 final ScheduledFuture<?> synchronizationTask = this.synchronizationTask; 361 if (synchronizationTask != null) { 362 synchronizationTask.cancel(true /* interrupt the task */); 363 } 364 365 try { 366 if (!this.synchronizationExecutorService.awaitTermination(60L, TimeUnit.SECONDS)) { 367 this.synchronizationExecutorService.shutdownNow(); 368 if (!this.synchronizationExecutorService.awaitTermination(60L, TimeUnit.SECONDS)) { 369 // TODO: log 370 } 371 } 372 } catch (final InterruptedException interruptedException) { 373 this.synchronizationExecutorService.shutdownNow(); 374 Thread.currentThread().interrupt(); 375 } 376 } 377 if (this.logger.isLoggable(Level.FINER)) { 378 this.logger.exiting(cn, mn); 379 } 380 } 381 382 private synchronized final void setUpSynchronization() { 383 final String cn = this.getClass().getName(); 384 final String mn = "setUpSynchronization"; 385 if (this.logger.isLoggable(Level.FINER)) { 386 this.logger.entering(cn, mn); 387 } 388 389 if (this.synchronizationExecutorService != null && this.synchronizationTask == null) { 390 assert this.synchronizationIntervalInSeconds > 0L; 391 392 if (this.logger.isLoggable(Level.INFO)) { 393 this.logger.logp(Level.INFO, cn, mn, "Scheduling downstream synchronization every {0} seconds", Long.valueOf(this.synchronizationIntervalInSeconds)); 394 } 395 final ScheduledFuture<?> job = this.synchronizationExecutorService.scheduleWithFixedDelay(() -> { 396 if (shouldSynchronize()) { 397 if (logger.isLoggable(Level.FINE)) { 398 logger.logp(Level.FINE, cn, mn, "Synchronizing event cache with its downstream consumers"); 399 } 400 synchronized (eventCache) { 401 eventCache.synchronize(); 402 } 403 } 404 }, 0L, this.synchronizationIntervalInSeconds, TimeUnit.SECONDS); 405 assert job != null; 406 this.synchronizationTask = job; 407 } 408 409 if (this.logger.isLoggable(Level.FINER)) { 410 this.logger.exiting(cn, mn); 411 } 412 } 413 414 /** 415 * Returns whether, at any given moment, this {@link Reflector} 416 * should cause its {@link EventCache} to {@linkplain 417 * EventCache#synchronize() synchronize}. 418 * 419 * <h2>Design Notes</h2> 420 * 421 * <p>This code follows the Go code in the Kubernetes {@code 422 * client-go/tools/cache} package. One thing that becomes clear 423 * when looking at all of this through an object-oriented lens is 424 * that it is the {@link EventCache} (the {@code delta_fifo}, in the 425 * Go code) that is ultimately in charge of synchronizing. It is 426 * not clear why this is a function of a reflector. In an 427 * object-oriented world, perhaps the {@link EventCache} itself 428 * should be in charge of resynchronization schedules.</p> 429 * 430 * @return {@code true} if this {@link Reflector} should cause its 431 * {@link EventCache} to {@linkplain EventCache#synchronize() 432 * synchronize}; {@code false} otherwise 433 */ 434 protected boolean shouldSynchronize() { 435 final String cn = this.getClass().getName(); 436 final String mn = "shouldSynchronize"; 437 if (this.logger.isLoggable(Level.FINER)) { 438 this.logger.entering(cn, mn); 439 } 440 final boolean returnValue = this.synchronizationExecutorService != null; 441 if (this.logger.isLoggable(Level.FINER)) { 442 this.logger.exiting(cn, mn, Boolean.valueOf(returnValue)); 443 } 444 return returnValue; 445 } 446 447 private final Object getLastSynchronizationResourceVersion() { 448 return this.lastSynchronizationResourceVersion; 449 } 450 451 private final void setLastSynchronizationResourceVersion(final Object resourceVersion) { 452 this.lastSynchronizationResourceVersion = resourceVersion; 453 } 454 455 /** 456 * Using the {@code operation} supplied at construction time, 457 * {@linkplain Listable#list() lists} appropriate Kubernetes 458 * resources, and then, on a separate {@link Thread}, {@linkplain 459 * VersionWatchable sets up a watch} on them, calling {@link 460 * EventCache#replace(Collection, Object)} and {@link 461 * EventCache#add(Object, AbstractEvent.Type, HasMetadata)} methods 462 * as appropriate. 463 * 464 * <p>The calling {@link Thread} is not blocked by invocations of 465 * this method.</p> 466 * 467 * @see #close() 468 */ 469 @NonBlocking 470 public final void start() { 471 final String cn = this.getClass().getName(); 472 final String mn = "start"; 473 if (this.logger.isLoggable(Level.FINER)) { 474 this.logger.entering(cn, mn); 475 } 476 477 synchronized (this) { 478 if (this.watch == null) { 479 480 // Run a list operation, and get the resourceVersion of that list. 481 @SuppressWarnings("unchecked") 482 final KubernetesResourceList<? extends T> list = ((Listable<? extends KubernetesResourceList<? extends T>>)this.operation).list(); 483 assert list != null; 484 485 final ListMeta metadata = list.getMetadata(); 486 assert metadata != null; 487 final String resourceVersion = metadata.getResourceVersion(); 488 assert resourceVersion != null; 489 490 // Using the results of that list operation, do a full replace 491 // on the EventCache with them. 492 final Collection<? extends T> replacementItems; 493 final Collection<? extends T> items = list.getItems(); 494 if (items == null || items.isEmpty()) { 495 replacementItems = Collections.emptySet(); 496 } else { 497 replacementItems = Collections.unmodifiableCollection(new ArrayList<>(items)); 498 } 499 synchronized (eventCache) { 500 this.eventCache.replace(replacementItems, resourceVersion); 501 } 502 503 // Record the resource version we captured during our list 504 // operation. 505 this.setLastSynchronizationResourceVersion(resourceVersion); 506 507 // Now that we've vetted that our list operation works (i.e. no 508 // syntax errors, no connectivity problems) we can schedule 509 // resynchronizations if necessary. 510 this.setUpSynchronization(); 511 512 // Now that we've taken care of our list() operation, set up our 513 // watch() operation. 514 515 @SuppressWarnings("unchecked") 516 final Versionable<? extends Watchable<? extends Closeable, Watcher<T>>> versionableOperation = (Versionable<? extends Watchable<? extends Closeable, Watcher<T>>>)this.operation; 517 this.watch = withResourceVersion(versionableOperation, resourceVersion).watch(new WatchHandler()); 518 519 } 520 } 521 522 if (this.logger.isLoggable(Level.FINER)) { 523 this.logger.exiting(cn, mn); 524 } 525 } 526 527 /** 528 * Invoked when {@link #close()} is invoked. 529 * 530 * <p>The default implementation of this method does nothing.</p> 531 */ 532 protected synchronized void onClose() { 533 534 } 535 536 537 /* 538 * Hacks. 539 */ 540 541 542 @Issue( 543 id = "kubernetes-client/1099", 544 uri = "https://github.com/fabric8io/kubernetes-client/issues/1099" 545 ) 546 @SuppressWarnings({"rawtypes", "unchecked"}) 547 private final Watchable<? extends Closeable, Watcher<T>> withResourceVersion(final Versionable<? extends Watchable<? extends Closeable, Watcher<T>>> operation, final String resourceVersion) { 548 Objects.requireNonNull(operation); 549 Objects.requireNonNull(resourceVersion); 550 Watchable<? extends Closeable, Watcher<T>> returnValue = null; 551 if (operation instanceof CustomResourceOperationsImpl) { 552 final CustomResourceOperationsImpl old = (CustomResourceOperationsImpl)operation; 553 try { 554 returnValue = 555 new CustomResourceOperationsImpl(getClient(old), 556 old.getConfig(), 557 getApiGroup(old), 558 old.getAPIVersion(), 559 getResourceT(old), 560 old.getNamespace(), 561 old.getName(), 562 old.isCascading(), 563 (T)old.getItem(), 564 resourceVersion, 565 old.isReloadingFromServer(), 566 old.getType(), 567 old.getListType(), 568 old.getDoneableType()); 569 } catch (final ReflectiveOperationException reflectiveOperationException) { 570 throw new KubernetesClientException(reflectiveOperationException.getMessage(), reflectiveOperationException); 571 } 572 } else { 573 returnValue = operation.withResourceVersion(resourceVersion); 574 } 575 assert returnValue != null; 576 return returnValue; 577 } 578 579 private static final OkHttpClient getClient(final OperationSupport operation) throws ReflectiveOperationException { 580 Objects.requireNonNull(operation); 581 final Field clientField = OperationSupport.class.getDeclaredField("client"); 582 assert !clientField.isAccessible(); 583 assert OkHttpClient.class.equals(clientField.getType()); 584 OkHttpClient returnValue = null; 585 try { 586 clientField.setAccessible(true); 587 returnValue = (OkHttpClient)clientField.get(operation); 588 } finally { 589 clientField.setAccessible(false); 590 } 591 return returnValue; 592 } 593 594 private static final String getApiGroup(final OperationSupport operation) throws ReflectiveOperationException { 595 Objects.requireNonNull(operation); 596 final Field apiGroupField = OperationSupport.class.getDeclaredField("apiGroup"); 597 assert !apiGroupField.isAccessible(); 598 assert String.class.equals(apiGroupField.getType()); 599 String returnValue = null; 600 try { 601 apiGroupField.setAccessible(true); 602 returnValue = (String)apiGroupField.get(operation); 603 } finally { 604 apiGroupField.setAccessible(false); 605 } 606 return returnValue; 607 } 608 609 private static final String getResourceT(final OperationSupport operation) throws ReflectiveOperationException { 610 Objects.requireNonNull(operation); 611 final Field resourceTField = OperationSupport.class.getDeclaredField("resourceT"); 612 assert !resourceTField.isAccessible(); 613 assert String.class.equals(resourceTField.getType()); 614 String returnValue = null; 615 try { 616 resourceTField.setAccessible(true); 617 returnValue = (String)resourceTField.get(operation); 618 } finally { 619 resourceTField.setAccessible(false); 620 } 621 return returnValue; 622 } 623 624 private static final Map<String, String> getFields(final BaseOperation<?, ?, ?, ?> operation) throws ReflectiveOperationException { 625 Objects.requireNonNull(operation); 626 final Method getFieldsMethod = BaseOperation.class.getDeclaredMethod("getFields"); 627 assert !getFieldsMethod.isAccessible(); 628 assert Map.class.equals(getFieldsMethod.getReturnType()); 629 Map<String, String> returnValue = null; 630 try { 631 getFieldsMethod.setAccessible(true); 632 @SuppressWarnings("unchecked") 633 final Map<String, String> temp = (Map<String, String>)getFieldsMethod.invoke(operation); 634 returnValue = temp; 635 } finally { 636 getFieldsMethod.setAccessible(false); 637 } 638 return returnValue; 639 } 640 641 private static final Map<String, String> getLabels(final BaseOperation<?, ?, ?, ?> operation) throws ReflectiveOperationException { 642 Objects.requireNonNull(operation); 643 final Method getLabelsMethod = BaseOperation.class.getDeclaredMethod("getLabels"); 644 assert !getLabelsMethod.isAccessible(); 645 assert Map.class.equals(getLabelsMethod.getReturnType()); 646 Map<String, String> returnValue = null; 647 try { 648 getLabelsMethod.setAccessible(true); 649 @SuppressWarnings("unchecked") 650 final Map<String, String> temp = (Map<String, String>)getLabelsMethod.invoke(operation); 651 returnValue = temp; 652 } finally { 653 getLabelsMethod.setAccessible(false); 654 } 655 return returnValue; 656 } 657 658 private static final Map<String, String[]> getLabelsIn(final BaseOperation<?, ?, ?, ?> operation) throws ReflectiveOperationException { 659 Objects.requireNonNull(operation); 660 final Method getLabelsInMethod = BaseOperation.class.getDeclaredMethod("getLabelsIn"); 661 assert !getLabelsInMethod.isAccessible(); 662 assert Map.class.equals(getLabelsInMethod.getReturnType()); 663 Map<String, String[]> returnValue = null; 664 try { 665 getLabelsInMethod.setAccessible(true); 666 @SuppressWarnings("unchecked") 667 final Map<String, String[]> temp = (Map<String, String[]>)getLabelsInMethod.invoke(operation); 668 returnValue = temp; 669 } finally { 670 getLabelsInMethod.setAccessible(false); 671 } 672 return returnValue; 673 } 674 675 private static final Map<String, String> getLabelsNot(final BaseOperation<?, ?, ?, ?> operation) throws ReflectiveOperationException { 676 Objects.requireNonNull(operation); 677 final Method getLabelsNotMethod = BaseOperation.class.getDeclaredMethod("getLabelsNot"); 678 assert !getLabelsNotMethod.isAccessible(); 679 assert Map.class.equals(getLabelsNotMethod.getReturnType()); 680 Map<String, String> returnValue = null; 681 try { 682 getLabelsNotMethod.setAccessible(true); 683 @SuppressWarnings("unchecked") 684 final Map<String, String> temp = (Map<String, String>)getLabelsNotMethod.invoke(operation); 685 returnValue = temp; 686 } finally { 687 getLabelsNotMethod.setAccessible(false); 688 } 689 return returnValue; 690 } 691 692 private static final Map<String, String[]> getLabelsNotIn(final BaseOperation<?, ?, ?, ?> operation) throws ReflectiveOperationException { 693 Objects.requireNonNull(operation); 694 final Method getLabelsNotInMethod = BaseOperation.class.getDeclaredMethod("getLabelsNotIn"); 695 assert !getLabelsNotInMethod.isAccessible(); 696 assert Map.class.equals(getLabelsNotInMethod.getReturnType()); 697 Map<String, String[]> returnValue = null; 698 try { 699 getLabelsNotInMethod.setAccessible(true); 700 @SuppressWarnings("unchecked") 701 final Map<String, String[]> temp = (Map<String, String[]>)getLabelsNotInMethod.invoke(operation); 702 returnValue = temp; 703 } finally { 704 getLabelsNotInMethod.setAccessible(false); 705 } 706 return returnValue; 707 } 708 709 710 /* 711 * Inner and nested classes. 712 */ 713 714 715 /** 716 * A {@link Watcher} of Kubernetes resources. 717 * 718 * @author <a href="https://about.me/lairdnelson" 719 * target="_parent">Laird Nelson</a> 720 * 721 * @see Watcher 722 */ 723 private final class WatchHandler implements Watcher<T> { 724 725 726 /* 727 * Constructors. 728 */ 729 730 731 /** 732 * Creates a new {@link WatchHandler}. 733 */ 734 private WatchHandler() { 735 super(); 736 final String cn = this.getClass().getName(); 737 final String mn = "<init>"; 738 if (logger.isLoggable(Level.FINER)) { 739 logger.entering(cn, mn); 740 logger.exiting(cn, mn); 741 } 742 } 743 744 745 /* 746 * Instance methods. 747 */ 748 749 750 /** 751 * Calls the {@link EventCache#add(Object, AbstractEvent.Type, 752 * HasMetadata)} method on the enclosing {@link Reflector}'s 753 * associated {@link EventCache} with information harvested from 754 * the supplied {@code resource}, and using an {@link Event.Type} 755 * selected appropriately given the supplied {@link 756 * Watcher.Action}. 757 * 758 * @param action the kind of Kubernetes event that happened; must 759 * not be {@code null} 760 * 761 * @param resource the {@link HasMetadata} object that was 762 * affected; must not be {@code null} 763 * 764 * @exception NullPointerException if {@code action} or {@code 765 * resource} was {@code null} 766 * 767 * @exception IllegalStateException if another error occurred 768 */ 769 @Override 770 public final void eventReceived(final Watcher.Action action, final T resource) { 771 final String cn = this.getClass().getName(); 772 final String mn = "eventReceived"; 773 if (logger.isLoggable(Level.FINER)) { 774 logger.entering(cn, mn, new Object[] { action, resource }); 775 } 776 Objects.requireNonNull(action); 777 Objects.requireNonNull(resource); 778 779 final ObjectMeta metadata = resource.getMetadata(); 780 assert metadata != null; 781 782 final Event.Type eventType; 783 784 switch (action) { 785 case ADDED: 786 eventType = Event.Type.ADDITION; 787 break; 788 case MODIFIED: 789 eventType = Event.Type.MODIFICATION; 790 break; 791 case DELETED: 792 eventType = Event.Type.DELETION; 793 break; 794 case ERROR: 795 // Uh...the Go code has: 796 // 797 // if event.Type == watch.Error { 798 // return apierrs.FromObject(event.Object) 799 // } 800 // 801 // Now, apierrs.FromObject is here: 802 // https://github.com/kubernetes/apimachinery/blob/kubernetes-1.9.2/pkg/api/errors/errors.go#L80-L88 803 // This is looking for a Status object. But 804 // WatchConnectionHandler will never forward on such a thing: 805 // https://github.com/fabric8io/kubernetes-client/blob/v3.1.8/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/WatchConnectionManager.java#L246-L258 806 // 807 // So it follows that if by some chance we get here, resource 808 // will definitely be a HasMetadata. We go back to the Go 809 // code again, and remember that if the type is Error, the 810 // equivalent of this watch handler simply returns and goes home. 811 // 812 // Now, if we were to throw a RuntimeException here, which is 813 // the idiomatic equivalent of returning and going home, this 814 // would cause a watch reconnect: 815 // https://github.com/fabric8io/kubernetes-client/blob/v3.1.8/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/WatchConnectionManager.java#L159-L205 816 // ...up to the reconnect limit. 817 // 818 // ...which is fine, but I'm not sure that in an error case a 819 // WatchEvent will ever HAVE a HasMetadata as its payload. 820 // Which means MAYBE we'll never get here. But if we do, all 821 // we can do is throw a RuntimeException...which ends up 822 // reducing to the same case as the default case below, so we 823 // fall through. 824 default: 825 eventType = null; 826 throw new IllegalStateException(); 827 } 828 829 // Add an Event of the proper kind to our EventCache. 830 if (eventType != null) { 831 if (logger.isLoggable(Level.FINE)) { 832 logger.logp(Level.FINE, cn, mn, "Adding event to cache: {0} {1}", new Object[] { eventType, resource }); 833 } 834 synchronized (eventCache) { 835 eventCache.add(Reflector.this, eventType, resource); 836 } 837 } 838 839 // Record the most recent resource version we're tracking to be 840 // that of this last successful watch() operation. We set it 841 // earlier during a list() operation. 842 setLastSynchronizationResourceVersion(metadata.getResourceVersion()); 843 844 if (logger.isLoggable(Level.FINER)) { 845 logger.exiting(cn, mn); 846 } 847 } 848 849 /** 850 * Invoked when the Kubernetes client connection closes. 851 * 852 * @param exception any {@link KubernetesClientException} that 853 * caused this closing to happen; may be {@code null} 854 */ 855 @Override 856 public final void onClose(final KubernetesClientException exception) { 857 final String cn = this.getClass().getName(); 858 final String mn = "onClose"; 859 if (logger.isLoggable(Level.FINER)) { 860 logger.entering(cn, mn, exception); 861 } 862 if (exception != null && logger.isLoggable(Level.WARNING)) { 863 logger.logp(Level.WARNING, cn, mn, exception.getMessage(), exception); 864 } 865 if (logger.isLoggable(Level.FINER)) { 866 logger.exiting(cn, mn, exception); 867 } 868 } 869 870 } 871 872}