001/* 002 * VM-Operator 003 * Copyright (C) 2024 Michael N. Lipp 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <https://www.gnu.org/licenses/>. 017 */ 018 019package org.jdrupes.vmoperator.common; 020 021import io.kubernetes.client.Discovery.APIResource; 022import io.kubernetes.client.apimachinery.GroupVersionKind; 023import io.kubernetes.client.common.KubernetesListObject; 024import io.kubernetes.client.common.KubernetesObject; 025import io.kubernetes.client.custom.V1Patch; 026import io.kubernetes.client.openapi.ApiException; 027import io.kubernetes.client.util.Strings; 028import io.kubernetes.client.util.generic.GenericKubernetesApi; 029import io.kubernetes.client.util.generic.KubernetesApiResponse; 030import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; 031import io.kubernetes.client.util.generic.options.GetOptions; 032import io.kubernetes.client.util.generic.options.ListOptions; 033import io.kubernetes.client.util.generic.options.PatchOptions; 034import io.kubernetes.client.util.generic.options.UpdateOptions; 035import java.net.HttpURLConnection; 036import java.util.ArrayList; 037import java.util.Collection; 038import java.util.LinkedList; 039import java.util.List; 040import java.util.Optional; 041import java.util.function.Function; 042 043/** 044 * A stub for namespaced custom objects. This stub provides the 045 * functions common to all Kubernetes objects, but uses variables 046 * for all types. This class should be used as base class only. 047 * 048 * @param <O> the generic type 049 * @param <L> the generic type 050 */ 051@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.TooManyMethods" }) 052public class K8sGenericStub<O extends KubernetesObject, 053 L extends KubernetesListObject> { 054 protected final K8sClient client; 055 private final GenericKubernetesApi<O, L> api; 056 protected final APIResource context; 057 protected final String namespace; 058 protected final String name; 059 060 /** 061 * Instantiates a new stub for the object specified. If the object 062 * exists in the context specified, the version (see 063 * {@link #version()} is bound to the existing object's version. 064 * Else the stub is dangling with the version set to the context's 065 * preferred version. 066 * 067 * @param objectClass the object class 068 * @param objectListClass the object list class 069 * @param client the client 070 * @param context the context 071 * @param namespace the namespace 072 * @param name the name 073 */ 074 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 075 protected K8sGenericStub(Class<O> objectClass, Class<L> objectListClass, 076 K8sClient client, APIResource context, String namespace, 077 String name) { 078 this.client = client; 079 this.namespace = namespace; 080 this.name = name; 081 082 // Bind version 083 var foundVersion = context.getPreferredVersion(); 084 GenericKubernetesApi<O, L> testApi = null; 085 GetOptions mdOpts 086 = new GetOptions().isPartialObjectMetadataRequest(true); 087 for (var version : candidateVersions(context)) { 088 testApi = new GenericKubernetesApi<>(objectClass, objectListClass, 089 context.getGroup(), version, context.getResourcePlural(), 090 client); 091 if (testApi.get(namespace, name, mdOpts) 092 .isSuccess()) { 093 foundVersion = version; 094 break; 095 } 096 } 097 if (foundVersion.equals(context.getPreferredVersion())) { 098 this.context = context; 099 } else { 100 this.context = K8s.preferred(context, foundVersion); 101 } 102 103 api = Optional.ofNullable(testApi) 104 .orElseGet(() -> new GenericKubernetesApi<>(objectClass, 105 objectListClass, group(), version(), plural(), client)); 106 } 107 108 /** 109 * Gets the context. 110 * 111 * @return the context 112 */ 113 public APIResource context() { 114 return context; 115 } 116 117 /** 118 * Gets the group. 119 * 120 * @return the group 121 */ 122 public String group() { 123 return context.getGroup(); 124 } 125 126 /** 127 * Gets the version. 128 * 129 * @return the version 130 */ 131 public String version() { 132 return context.getPreferredVersion(); 133 } 134 135 /** 136 * Gets the kind. 137 * 138 * @return the kind 139 */ 140 public String kind() { 141 return context.getKind(); 142 } 143 144 /** 145 * Gets the plural. 146 * 147 * @return the plural 148 */ 149 public String plural() { 150 return context.getResourcePlural(); 151 } 152 153 /** 154 * Gets the namespace. 155 * 156 * @return the namespace 157 */ 158 public String namespace() { 159 return namespace; 160 } 161 162 /** 163 * Gets the name. 164 * 165 * @return the name 166 */ 167 public String name() { 168 return name; 169 } 170 171 /** 172 * Delete the Kubernetes object. 173 * 174 * @throws ApiException the API exception 175 */ 176 public void delete() throws ApiException { 177 var result = api.delete(namespace, name); 178 if (result.isSuccess() 179 || result.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) { 180 return; 181 } 182 result.throwsApiException(); 183 } 184 185 /** 186 * Retrieves and returns the current state of the object. 187 * 188 * @return the object's state 189 * @throws ApiException the api exception 190 */ 191 public Optional<O> model() throws ApiException { 192 return K8s.optional(api.get(namespace, name)); 193 } 194 195 /** 196 * Updates the object's status. Does not retry in case of conflict. 197 * 198 * @param object the current state of the object (passed to `status`) 199 * @param updater function that returns the new status 200 * @return the updated model or empty if the object was not found 201 * @throws ApiException the api exception 202 */ 203 @SuppressWarnings("PMD.AssignmentInOperand") 204 public Optional<O> updateStatus(O object, Function<O, Object> updater) 205 throws ApiException { 206 return K8s.optional(api.updateStatus(object, updater)); 207 } 208 209 /** 210 * Updates the status of the given object. In case of conflict, 211 * get the current version of the object and tries again. Retries 212 * up to `retries` times. 213 * 214 * @param updater the function updating the status 215 * @param current the current state of the object, used for the first 216 * attempt to update 217 * @param retries the retries in case of conflict 218 * @return the updated model or empty if the object was not found 219 * @throws ApiException the api exception 220 */ 221 @SuppressWarnings({ "PMD.AssignmentInOperand", "PMD.UnusedAssignment" }) 222 public Optional<O> updateStatus(Function<O, Object> updater, O current, 223 int retries) throws ApiException { 224 while (true) { 225 try { 226 if (current == null) { 227 current = api.get(namespace, name) 228 .throwsApiException().getObject(); 229 } 230 return updateStatus(current, updater); 231 } catch (ApiException e) { 232 if (HttpURLConnection.HTTP_CONFLICT != e.getCode() 233 || retries-- <= 0) { 234 throw e; 235 } 236 // Get current version for new attempt 237 current = null; 238 } 239 } 240 } 241 242 /** 243 * Gets the object and updates the status. In case of conflict, retries 244 * up to `retries` times. 245 * 246 * @param updater the function updating the status 247 * @param retries the retries in case of conflict 248 * @return the updated model or empty if the object was not found 249 * @throws ApiException the api exception 250 */ 251 @SuppressWarnings({ "PMD.AssignmentInOperand", "PMD.UnusedAssignment" }) 252 public Optional<O> updateStatus(Function<O, Object> updater, int retries) 253 throws ApiException { 254 return updateStatus(updater, null, retries); 255 } 256 257 /** 258 * Updates the status of the given object. In case of conflict, 259 * get the current version of the object and tries again. Retries 260 * up to `retries` times. 261 * 262 * @param updater the function updating the status 263 * @param current the current 264 * @return the kubernetes api response 265 * the updated model or empty if not successful 266 * @throws ApiException the api exception 267 */ 268 public Optional<O> updateStatus(Function<O, Object> updater, O current) 269 throws ApiException { 270 return updateStatus(updater, current, 16); 271 } 272 273 /** 274 * Updates the status. In case of conflict, retries up to 16 times. 275 * 276 * @param updater the function updating the status 277 * @return the kubernetes api response 278 * the updated model or empty if not successful 279 * @throws ApiException the api exception 280 */ 281 public Optional<O> updateStatus(Function<O, Object> updater) 282 throws ApiException { 283 return updateStatus(updater, null); 284 } 285 286 /** 287 * Patch the object. 288 * 289 * @param patchType the patch type 290 * @param patch the patch 291 * @param options the options 292 * @return the kubernetes api response if successful 293 * @throws ApiException the api exception 294 */ 295 public Optional<O> patch(String patchType, V1Patch patch, 296 PatchOptions options) throws ApiException { 297 return K8s 298 .optional(api.patch(namespace, name, patchType, patch, options) 299 .throwsApiException()); 300 } 301 302 /** 303 * Patch the object using default options. 304 * 305 * @param patchType the patch type 306 * @param patch the patch 307 * @return the kubernetes api response if successful 308 * @throws ApiException the api exception 309 */ 310 public Optional<O> 311 patch(String patchType, V1Patch patch) throws ApiException { 312 PatchOptions opts = new PatchOptions(); 313 return patch(patchType, patch, opts); 314 } 315 316 /** 317 * Apply the given definition. 318 * 319 * @param def the def 320 * @return the kubernetes api response if successful 321 * @throws ApiException the api exception 322 */ 323 public Optional<O> apply(DynamicKubernetesObject def) throws ApiException { 324 PatchOptions opts = new PatchOptions(); 325 opts.setForce(true); 326 opts.setFieldManager("kubernetes-java-kubectl-apply"); 327 return patch(V1Patch.PATCH_FORMAT_APPLY_YAML, 328 new V1Patch(client.getJSON().serialize(def)), opts); 329 } 330 331 /** 332 * Update the object. 333 * 334 * @param object the object 335 * @return the kubernetes api response 336 * @throws ApiException the api exception 337 */ 338 public KubernetesApiResponse<O> update(O object) throws ApiException { 339 return api.update(object).throwsApiException(); 340 } 341 342 /** 343 * Update the object. 344 * 345 * @param object the object 346 * @param options the options 347 * @return the kubernetes api response 348 * @throws ApiException the api exception 349 */ 350 public KubernetesApiResponse<O> update(O object, UpdateOptions options) 351 throws ApiException { 352 return api.update(object, options).throwsApiException(); 353 } 354 355 /** 356 * A supplier for generic stubs. 357 * 358 * @param <O> the object type 359 * @param <L> the object list type 360 * @param <R> the result type 361 */ 362 @FunctionalInterface 363 public interface GenericSupplier<O extends KubernetesObject, 364 L extends KubernetesListObject, R extends K8sGenericStub<O, L>> { 365 366 /** 367 * Gets a new stub. 368 * 369 * @param client the client 370 * @param namespace the namespace 371 * @param name the name 372 * @return the result 373 */ 374 @SuppressWarnings("PMD.UseObjectForClearerAPI") 375 R get(K8sClient client, String namespace, String name); 376 } 377 378 @Override 379 @SuppressWarnings("PMD.UseLocaleWithCaseConversions") 380 public String toString() { 381 return (Strings.isNullOrEmpty(group()) ? "" : group() + "/") 382 + version().toUpperCase() + kind() + " " + namespace + ":" + name; 383 } 384 385 /** 386 * Get a namespaced object stub for a newly created object. 387 * 388 * @param <O> the object type 389 * @param <L> the object list type 390 * @param <R> the stub type 391 * @param objectClass the object class 392 * @param objectListClass the object list class 393 * @param client the client 394 * @param context the context 395 * @param model the model 396 * @param provider the provider 397 * @return the stub if the object exists 398 * @throws ApiException the api exception 399 */ 400 @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", 401 "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" }) 402 public static <O extends KubernetesObject, L extends KubernetesListObject, 403 R extends K8sGenericStub<O, L>> 404 R create(Class<O> objectClass, Class<L> objectListClass, 405 K8sClient client, APIResource context, O model, 406 GenericSupplier<O, L, R> provider) throws ApiException { 407 var api = new GenericKubernetesApi<>(objectClass, objectListClass, 408 context.getGroup(), context.getPreferredVersion(), 409 context.getResourcePlural(), client); 410 api.create(model).throwsApiException(); 411 return provider.get(client, model.getMetadata().getNamespace(), 412 model.getMetadata().getName()); 413 } 414 415 /** 416 * Get the stubs for the objects in the given namespace that match 417 * the criteria from the given options. 418 * 419 * @param <O> the object type 420 * @param <L> the object list type 421 * @param <R> the stub type 422 * @param objectClass the object class 423 * @param objectListClass the object list class 424 * @param client the client 425 * @param context the context 426 * @param namespace the namespace 427 * @param options the options 428 * @param provider the provider 429 * @return the collection 430 * @throws ApiException the api exception 431 */ 432 public static <O extends KubernetesObject, L extends KubernetesListObject, 433 R extends K8sGenericStub<O, L>> 434 Collection<R> list(Class<O> objectClass, Class<L> objectListClass, 435 K8sClient client, APIResource context, String namespace, 436 ListOptions options, GenericSupplier<O, L, R> provider) 437 throws ApiException { 438 var result = new ArrayList<R>(); 439 for (var version : candidateVersions(context)) { 440 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 441 var api = new GenericKubernetesApi<>(objectClass, objectListClass, 442 context.getGroup(), version, context.getResourcePlural(), 443 client); 444 var objs = api.list(namespace, options).throwsApiException(); 445 for (var item : objs.getObject().getItems()) { 446 result.add(provider.get(client, namespace, 447 item.getMetadata().getName())); 448 } 449 } 450 return result; 451 } 452 453 private static List<String> candidateVersions(APIResource context) { 454 var result = new LinkedList<>(context.getVersions()); 455 result.remove(context.getPreferredVersion()); 456 result.add(0, context.getPreferredVersion()); 457 return result; 458 } 459 460 /** 461 * Api resource. 462 * 463 * @param client the client 464 * @param gvk the gvk 465 * @return the API resource 466 * @throws ApiException the api exception 467 */ 468 public static APIResource apiResource(K8sClient client, 469 GroupVersionKind gvk) throws ApiException { 470 var context = K8s.context(client, gvk.getGroup(), gvk.getVersion(), 471 gvk.getKind()); 472 if (context.isEmpty()) { 473 throw new ApiException("No known API for " + gvk.getGroup() 474 + "/" + gvk.getVersion() + " " + gvk.getKind()); 475 } 476 return context.get(); 477 } 478 479}