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.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 public Optional<O> updateStatus(O object, Function<O, Object> updater) 204 throws ApiException { 205 return K8s.optional(api.updateStatus(object, updater)); 206 } 207 208 /** 209 * Updates the status of the given object. In case of conflict, 210 * get the current version of the object and tries again. Retries 211 * up to `retries` times. 212 * 213 * @param updater the function updating the status 214 * @param current the current state of the object, used for the first 215 * attempt to update 216 * @param retries the retries in case of conflict 217 * @return the updated model or empty if the object was not found 218 * @throws ApiException the api exception 219 */ 220 @SuppressWarnings({ "PMD.AssignmentInOperand" }) 221 public Optional<O> updateStatus(Function<O, Object> updater, O current, 222 int retries) throws ApiException { 223 while (true) { 224 try { 225 if (current == null) { 226 current = api.get(namespace, name) 227 .throwsApiException().getObject(); 228 } 229 return updateStatus(current, updater); 230 } catch (ApiException e) { 231 if (HttpURLConnection.HTTP_CONFLICT != e.getCode() 232 || retries-- <= 0) { 233 throw e; 234 } 235 // Get current version for new attempt 236 current = null; 237 } 238 } 239 } 240 241 /** 242 * Gets the object and updates the status. In case of conflict, retries 243 * up to `retries` times. 244 * 245 * @param updater the function updating the status 246 * @param retries the retries in case of conflict 247 * @return the updated model or empty if the object was not found 248 * @throws ApiException the api exception 249 */ 250 public Optional<O> updateStatus(Function<O, Object> updater, int retries) 251 throws ApiException { 252 return updateStatus(updater, null, retries); 253 } 254 255 /** 256 * Updates the status of the given object. In case of conflict, 257 * get the current version of the object and tries again. Retries 258 * up to `retries` times. 259 * 260 * @param updater the function updating the status 261 * @param current the current 262 * @return the kubernetes api response 263 * the updated model or empty if not successful 264 * @throws ApiException the api exception 265 */ 266 public Optional<O> updateStatus(Function<O, Object> updater, O current) 267 throws ApiException { 268 return updateStatus(updater, current, 16); 269 } 270 271 /** 272 * Updates the status. In case of conflict, retries up to 16 times. 273 * 274 * @param updater the function updating the status 275 * @return the kubernetes api response 276 * the updated model or empty if not successful 277 * @throws ApiException the api exception 278 */ 279 public Optional<O> updateStatus(Function<O, Object> updater) 280 throws ApiException { 281 return updateStatus(updater, null); 282 } 283 284 /** 285 * Patch the object. 286 * 287 * @param patchType the patch type 288 * @param patch the patch 289 * @param options the options 290 * @return the kubernetes api response if successful 291 * @throws ApiException the api exception 292 */ 293 public Optional<O> patch(String patchType, V1Patch patch, 294 PatchOptions options) throws ApiException { 295 return K8s 296 .optional(api.patch(namespace, name, patchType, patch, options) 297 .throwsApiException()); 298 } 299 300 /** 301 * Patch the object using default options. 302 * 303 * @param patchType the patch type 304 * @param patch the patch 305 * @return the kubernetes api response if successful 306 * @throws ApiException the api exception 307 */ 308 public Optional<O> 309 patch(String patchType, V1Patch patch) throws ApiException { 310 PatchOptions opts = new PatchOptions(); 311 return patch(patchType, patch, opts); 312 } 313 314 /** 315 * Apply the given definition. 316 * 317 * @param def the def 318 * @return the kubernetes api response if successful 319 * @throws ApiException the api exception 320 */ 321 public Optional<O> apply(DynamicKubernetesObject def) throws ApiException { 322 PatchOptions opts = new PatchOptions(); 323 opts.setForce(true); 324 opts.setFieldManager("kubernetes-java-kubectl-apply"); 325 return patch(V1Patch.PATCH_FORMAT_APPLY_YAML, 326 new V1Patch(client.getJSON().serialize(def)), opts); 327 } 328 329 /** 330 * Update the object. 331 * 332 * @param object the object 333 * @return the kubernetes api response 334 * @throws ApiException the api exception 335 */ 336 public KubernetesApiResponse<O> update(O object) throws ApiException { 337 return api.update(object).throwsApiException(); 338 } 339 340 /** 341 * Update the object. 342 * 343 * @param object the object 344 * @param options the options 345 * @return the kubernetes api response 346 * @throws ApiException the api exception 347 */ 348 public KubernetesApiResponse<O> update(O object, UpdateOptions options) 349 throws ApiException { 350 return api.update(object, options).throwsApiException(); 351 } 352 353 /** 354 * A supplier for generic stubs. 355 * 356 * @param <O> the object type 357 * @param <L> the object list type 358 * @param <R> the result type 359 */ 360 @FunctionalInterface 361 public interface GenericSupplier<O extends KubernetesObject, 362 L extends KubernetesListObject, R extends K8sGenericStub<O, L>> { 363 364 /** 365 * Gets a new stub. 366 * 367 * @param client the client 368 * @param namespace the namespace 369 * @param name the name 370 * @return the result 371 */ 372 R get(K8sClient client, String namespace, String name); 373 } 374 375 @Override 376 @SuppressWarnings("PMD.UseLocaleWithCaseConversions") 377 public String toString() { 378 return (Strings.isNullOrEmpty(group()) ? "" : group() + "/") 379 + version().toUpperCase() + kind() + " " + namespace + ":" + name; 380 } 381 382 /** 383 * Get a namespaced object stub for a newly created object. 384 * 385 * @param <O> the object type 386 * @param <L> the object list type 387 * @param <R> the stub type 388 * @param objectClass the object class 389 * @param objectListClass the object list class 390 * @param client the client 391 * @param context the context 392 * @param model the model 393 * @param provider the provider 394 * @return the stub if the object exists 395 * @throws ApiException the api exception 396 */ 397 public static <O extends KubernetesObject, L extends KubernetesListObject, 398 R extends K8sGenericStub<O, L>> 399 R create(Class<O> objectClass, Class<L> objectListClass, 400 K8sClient client, APIResource context, O model, 401 GenericSupplier<O, L, R> provider) throws ApiException { 402 var api = new GenericKubernetesApi<>(objectClass, objectListClass, 403 context.getGroup(), context.getPreferredVersion(), 404 context.getResourcePlural(), client); 405 api.create(model).throwsApiException(); 406 return provider.get(client, model.getMetadata().getNamespace(), 407 model.getMetadata().getName()); 408 } 409 410 /** 411 * Get the stubs for the objects in the given namespace that match 412 * the criteria from the given options. 413 * 414 * @param <O> the object type 415 * @param <L> the object list type 416 * @param <R> the stub type 417 * @param objectClass the object class 418 * @param objectListClass the object list class 419 * @param client the client 420 * @param context the context 421 * @param namespace the namespace 422 * @param options the options 423 * @param provider the provider 424 * @return the collection 425 * @throws ApiException the api exception 426 */ 427 public static <O extends KubernetesObject, L extends KubernetesListObject, 428 R extends K8sGenericStub<O, L>> 429 Collection<R> list(Class<O> objectClass, Class<L> objectListClass, 430 K8sClient client, APIResource context, String namespace, 431 ListOptions options, GenericSupplier<O, L, R> provider) 432 throws ApiException { 433 var result = new ArrayList<R>(); 434 for (var version : candidateVersions(context)) { 435 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 436 var api = new GenericKubernetesApi<>(objectClass, objectListClass, 437 context.getGroup(), version, context.getResourcePlural(), 438 client); 439 var objs = api.list(namespace, options).throwsApiException(); 440 for (var item : objs.getObject().getItems()) { 441 result.add(provider.get(client, namespace, 442 item.getMetadata().getName())); 443 } 444 } 445 return result; 446 } 447 448 private static List<String> candidateVersions(APIResource context) { 449 var result = new LinkedList<>(context.getVersions()); 450 result.remove(context.getPreferredVersion()); 451 result.add(0, context.getPreferredVersion()); 452 return result; 453 } 454 455 /** 456 * Api resource. 457 * 458 * @param client the client 459 * @param gvk the gvk 460 * @return the API resource 461 * @throws ApiException the api exception 462 */ 463 public static APIResource apiResource(K8sClient client, 464 GroupVersionKind gvk) throws ApiException { 465 var context = K8s.context(client, gvk.getGroup(), gvk.getVersion(), 466 gvk.getKind()); 467 if (context.isEmpty()) { 468 throw new ApiException("No known API for " + gvk.getGroup() 469 + "/" + gvk.getVersion() + " " + gvk.getKind()); 470 } 471 return context.get(); 472 } 473 474}