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