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.options.GetOptions; 030import io.kubernetes.client.util.generic.options.ListOptions; 031import io.kubernetes.client.util.generic.options.PatchOptions; 032import java.net.HttpURLConnection; 033import java.util.ArrayList; 034import java.util.Collection; 035import java.util.LinkedList; 036import java.util.List; 037import java.util.Optional; 038import java.util.function.Function; 039 040/** 041 * A stub for cluster scoped objects. This stub provides the 042 * functions common to all Kubernetes objects, but uses variables 043 * for all types. This class should be used as base class only. 044 * 045 * @param <O> the generic type 046 * @param <L> the generic type 047 */ 048@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", 049 "PMD.CouplingBetweenObjects" }) 050public class K8sClusterGenericStub<O extends KubernetesObject, 051 L extends KubernetesListObject> { 052 protected final K8sClient client; 053 private final GenericKubernetesApi<O, L> api; 054 protected final APIResource context; 055 protected final String name; 056 057 /** 058 * Instantiates a new stub for the object specified. If the object 059 * exists in the context specified, the version (see 060 * {@link #version()} is bound to the existing object's version. 061 * Else the stub is dangling with the version set to the context's 062 * preferred version. 063 * 064 * @param objectClass the object class 065 * @param objectListClass the object list class 066 * @param client the client 067 * @param context the context 068 * @param name the name 069 */ 070 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 071 protected K8sClusterGenericStub(Class<O> objectClass, 072 Class<L> objectListClass, K8sClient client, APIResource context, 073 String name) { 074 this.client = client; 075 this.name = name; 076 077 // Bind version 078 var foundVersion = context.getPreferredVersion(); 079 GenericKubernetesApi<O, L> testApi = null; 080 GetOptions mdOpts 081 = new GetOptions().isPartialObjectMetadataRequest(true); 082 for (var version : candidateVersions(context)) { 083 testApi = new GenericKubernetesApi<>(objectClass, objectListClass, 084 context.getGroup(), version, context.getResourcePlural(), 085 client); 086 if (testApi.get(name, mdOpts).isSuccess()) { 087 foundVersion = version; 088 break; 089 } 090 } 091 if (foundVersion.equals(context.getPreferredVersion())) { 092 this.context = context; 093 } else { 094 this.context = K8s.preferred(context, foundVersion); 095 } 096 097 api = Optional.ofNullable(testApi) 098 .orElseGet(() -> new GenericKubernetesApi<>(objectClass, 099 objectListClass, group(), version(), plural(), client)); 100 } 101 102 /** 103 * Gets the context. 104 * 105 * @return the context 106 */ 107 public APIResource context() { 108 return context; 109 } 110 111 /** 112 * Gets the group. 113 * 114 * @return the group 115 */ 116 public String group() { 117 return context.getGroup(); 118 } 119 120 /** 121 * Gets the version. 122 * 123 * @return the version 124 */ 125 public String version() { 126 return context.getPreferredVersion(); 127 } 128 129 /** 130 * Gets the kind. 131 * 132 * @return the kind 133 */ 134 public String kind() { 135 return context.getKind(); 136 } 137 138 /** 139 * Gets the plural. 140 * 141 * @return the plural 142 */ 143 public String plural() { 144 return context.getResourcePlural(); 145 } 146 147 /** 148 * Gets the name. 149 * 150 * @return the name 151 */ 152 public String name() { 153 return name; 154 } 155 156 /** 157 * Delete the Kubernetes object. 158 * 159 * @throws ApiException the API exception 160 */ 161 public void delete() throws ApiException { 162 var result = api.delete(name); 163 if (result.isSuccess() 164 || result.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) { 165 return; 166 } 167 result.throwsApiException(); 168 } 169 170 /** 171 * Retrieves and returns the current state of the object. 172 * 173 * @return the object's state 174 * @throws ApiException the api exception 175 */ 176 public Optional<O> model() throws ApiException { 177 return K8s.optional(api.get(name)); 178 } 179 180 /** 181 * Updates the object's status. 182 * 183 * @param object the current state of the object (passed to `status`) 184 * @param status function that returns the new status 185 * @return the updated model or empty if not successful 186 * @throws ApiException the api exception 187 */ 188 public Optional<O> updateStatus(O object, 189 Function<O, Object> status) throws ApiException { 190 return K8s.optional(api.updateStatus(object, status)); 191 } 192 193 /** 194 * Updates the status. 195 * 196 * @param status the status 197 * @return the kubernetes api response 198 * the updated model or empty if not successful 199 * @throws ApiException the api exception 200 */ 201 public Optional<O> updateStatus(Function<O, Object> status) 202 throws ApiException { 203 return updateStatus(api.get(name).throwsApiException().getObject(), 204 status); 205 } 206 207 /** 208 * Patch the object. 209 * 210 * @param patchType the patch type 211 * @param patch the patch 212 * @param options the options 213 * @return the kubernetes api response 214 * @throws ApiException the api exception 215 */ 216 public Optional<O> patch(String patchType, V1Patch patch, 217 PatchOptions options) throws ApiException { 218 return K8s 219 .optional(api.patch(name, patchType, patch, options)); 220 } 221 222 /** 223 * Patch the object using default options. 224 * 225 * @param patchType the patch type 226 * @param patch the patch 227 * @return the kubernetes api response 228 * @throws ApiException the api exception 229 */ 230 public Optional<O> 231 patch(String patchType, V1Patch patch) throws ApiException { 232 PatchOptions opts = new PatchOptions(); 233 return patch(patchType, patch, opts); 234 } 235 236 /** 237 * A supplier for generic stubs. 238 * 239 * @param <O> the object type 240 * @param <L> the object list type 241 * @param <R> the result type 242 */ 243 @FunctionalInterface 244 public interface GenericSupplier<O extends KubernetesObject, 245 L extends KubernetesListObject, 246 R extends K8sClusterGenericStub<O, L>> { 247 248 /** 249 * Gets a new stub. 250 * 251 * @param objectClass the object class 252 * @param objectListClass the object list class 253 * @param client the client 254 * @param context the API resource 255 * @param name the name 256 * @return the result 257 */ 258 @SuppressWarnings("PMD.UseObjectForClearerAPI") 259 R get(Class<O> objectClass, Class<L> objectListClass, K8sClient client, 260 APIResource context, String name); 261 } 262 263 @Override 264 @SuppressWarnings("PMD.UseLocaleWithCaseConversions") 265 public String toString() { 266 return (Strings.isNullOrEmpty(group()) ? "" : group() + "/") 267 + version().toUpperCase() + kind() + " " + name; 268 } 269 270 /** 271 * Get an object stub. If the version in parameter 272 * `gvk` is an empty string, the stub refers to the first object 273 * found with matching group and kind. 274 * 275 * @param <O> the object type 276 * @param <L> the object list type 277 * @param <R> the stub type 278 * @param objectClass the object class 279 * @param objectListClass the object list class 280 * @param client the client 281 * @param gvk the group, version and kind 282 * @param name the name 283 * @param provider the provider 284 * @return the stub if the object exists 285 * @throws ApiException the api exception 286 */ 287 @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" }) 288 public static <O extends KubernetesObject, L extends KubernetesListObject, 289 R extends K8sClusterGenericStub<O, L>> 290 R get(Class<O> objectClass, Class<L> objectListClass, 291 K8sClient client, GroupVersionKind gvk, String name, 292 GenericSupplier<O, L, R> provider) throws ApiException { 293 var context = K8s.context(client, gvk.getGroup(), gvk.getVersion(), 294 gvk.getKind()); 295 if (context.isEmpty()) { 296 throw new ApiException("No known API for " + gvk.getGroup() 297 + "/" + gvk.getVersion() + " " + gvk.getKind()); 298 } 299 return provider.get(objectClass, objectListClass, client, context.get(), 300 name); 301 } 302 303 /** 304 * Get an object stub. 305 * 306 * @param <O> the object type 307 * @param <L> the object list type 308 * @param <R> the stub type 309 * @param objectClass the object class 310 * @param objectListClass the object list class 311 * @param client the client 312 * @param context the context 313 * @param name the name 314 * @param provider the provider 315 * @return the stub if the object exists 316 * @throws ApiException the api exception 317 */ 318 @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", 319 "PMD.UseObjectForClearerAPI" }) 320 public static <O extends KubernetesObject, L extends KubernetesListObject, 321 R extends K8sClusterGenericStub<O, L>> 322 R get(Class<O> objectClass, Class<L> objectListClass, 323 K8sClient client, APIResource context, String name, 324 GenericSupplier<O, L, R> provider) { 325 return provider.get(objectClass, objectListClass, client, context, 326 name); 327 } 328 329 /** 330 * Get an object stub for a newly created object. 331 * 332 * @param <O> the object type 333 * @param <L> the object list type 334 * @param <R> the stub type 335 * @param objectClass the object class 336 * @param objectListClass the object list class 337 * @param client the client 338 * @param context the context 339 * @param model the model 340 * @param provider the provider 341 * @return the stub if the object exists 342 * @throws ApiException the api exception 343 */ 344 @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", 345 "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" }) 346 public static <O extends KubernetesObject, L extends KubernetesListObject, 347 R extends K8sClusterGenericStub<O, L>> 348 R create(Class<O> objectClass, Class<L> objectListClass, 349 K8sClient client, APIResource context, O model, 350 GenericSupplier<O, L, R> provider) throws ApiException { 351 var api = new GenericKubernetesApi<>(objectClass, objectListClass, 352 context.getGroup(), context.getPreferredVersion(), 353 context.getResourcePlural(), client); 354 api.create(model).throwsApiException(); 355 return provider.get(objectClass, objectListClass, client, 356 context, model.getMetadata().getName()); 357 } 358 359 /** 360 * Get the stubs for the objects that match 361 * the criteria from the given options. 362 * 363 * @param <O> the object type 364 * @param <L> the object list type 365 * @param <R> the stub type 366 * @param objectClass the object class 367 * @param objectListClass the object list class 368 * @param client the client 369 * @param context the context 370 * @param options the options 371 * @param provider the provider 372 * @return the collection 373 * @throws ApiException the api exception 374 */ 375 public static <O extends KubernetesObject, L extends KubernetesListObject, 376 R extends K8sClusterGenericStub<O, L>> 377 Collection<R> list(Class<O> objectClass, Class<L> objectListClass, 378 K8sClient client, APIResource context, 379 ListOptions options, GenericSupplier<O, L, R> provider) 380 throws ApiException { 381 var result = new ArrayList<R>(); 382 for (var version : candidateVersions(context)) { 383 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 384 var api = new GenericKubernetesApi<>(objectClass, objectListClass, 385 context.getGroup(), version, context.getResourcePlural(), 386 client); 387 var objs = api.list(options).throwsApiException(); 388 for (var item : objs.getObject().getItems()) { 389 result.add(provider.get(objectClass, objectListClass, client, 390 context, item.getMetadata().getName())); 391 } 392 } 393 return result; 394 } 395 396 private static List<String> candidateVersions(APIResource context) { 397 var result = new LinkedList<>(context.getVersions()); 398 result.remove(context.getPreferredVersion()); 399 result.add(0, context.getPreferredVersion()); 400 return result; 401 } 402 403}