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 public interface GenericSupplier<O extends KubernetesObject, 363 L extends KubernetesListObject, R extends K8sGenericStub<O, L>> { 364 365 /** 366 * Gets a new stub. 367 * 368 * @param client the client 369 * @param namespace the namespace 370 * @param name the name 371 * @return the result 372 */ 373 @SuppressWarnings("PMD.UseObjectForClearerAPI") 374 R get(K8sClient client, String namespace, String name); 375 } 376 377 @Override 378 @SuppressWarnings("PMD.UseLocaleWithCaseConversions") 379 public String toString() { 380 return (Strings.isNullOrEmpty(group()) ? "" : group() + "/") 381 + version().toUpperCase() + kind() + " " + namespace + ":" + name; 382 } 383 384 /** 385 * Get a namespaced object stub for a newly created object. 386 * 387 * @param <O> the object type 388 * @param <L> the object list type 389 * @param <R> the stub type 390 * @param objectClass the object class 391 * @param objectListClass the object list class 392 * @param client the client 393 * @param context the context 394 * @param model the model 395 * @param provider the provider 396 * @return the stub if the object exists 397 * @throws ApiException the api exception 398 */ 399 @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", 400 "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" }) 401 public static <O extends KubernetesObject, L extends KubernetesListObject, 402 R extends K8sGenericStub<O, L>> 403 R create(Class<O> objectClass, Class<L> objectListClass, 404 K8sClient client, APIResource context, O model, 405 GenericSupplier<O, L, R> provider) throws ApiException { 406 var api = new GenericKubernetesApi<>(objectClass, objectListClass, 407 context.getGroup(), context.getPreferredVersion(), 408 context.getResourcePlural(), client); 409 api.create(model).throwsApiException(); 410 return provider.get(client, model.getMetadata().getNamespace(), 411 model.getMetadata().getName()); 412 } 413 414 /** 415 * Get the stubs for the objects in the given namespace that match 416 * the criteria from the given options. 417 * 418 * @param <O> the object type 419 * @param <L> the object list type 420 * @param <R> the stub type 421 * @param objectClass the object class 422 * @param objectListClass the object list class 423 * @param client the client 424 * @param context the context 425 * @param namespace the namespace 426 * @param options the options 427 * @param provider the provider 428 * @return the collection 429 * @throws ApiException the api exception 430 */ 431 public static <O extends KubernetesObject, L extends KubernetesListObject, 432 R extends K8sGenericStub<O, L>> 433 Collection<R> list(Class<O> objectClass, Class<L> objectListClass, 434 K8sClient client, APIResource context, String namespace, 435 ListOptions options, GenericSupplier<O, L, R> provider) 436 throws ApiException { 437 var result = new ArrayList<R>(); 438 for (var version : candidateVersions(context)) { 439 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 440 var api = new GenericKubernetesApi<>(objectClass, objectListClass, 441 context.getGroup(), version, context.getResourcePlural(), 442 client); 443 var objs = api.list(namespace, options).throwsApiException(); 444 for (var item : objs.getObject().getItems()) { 445 result.add(provider.get(client, namespace, 446 item.getMetadata().getName())); 447 } 448 } 449 return result; 450 } 451 452 private static List<String> candidateVersions(APIResource context) { 453 var result = new LinkedList<>(context.getVersions()); 454 result.remove(context.getPreferredVersion()); 455 result.add(0, context.getPreferredVersion()); 456 return result; 457 } 458 459 /** 460 * Api resource. 461 * 462 * @param client the client 463 * @param gvk the gvk 464 * @return the API resource 465 * @throws ApiException the api exception 466 */ 467 public static APIResource apiResource(K8sClient client, 468 GroupVersionKind gvk) throws ApiException { 469 var context = K8s.context(client, gvk.getGroup(), gvk.getVersion(), 470 gvk.getKind()); 471 if (context.isEmpty()) { 472 throw new ApiException("No known API for " + gvk.getGroup() 473 + "/" + gvk.getVersion() + " " + gvk.getKind()); 474 } 475 return context.get(); 476 } 477 478}