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 public interface GenericSupplier<O extends KubernetesObject, 244 L extends KubernetesListObject, 245 R extends K8sClusterGenericStub<O, L>> { 246 247 /** 248 * Gets a new stub. 249 * 250 * @param objectClass the object class 251 * @param objectListClass the object list class 252 * @param client the client 253 * @param context the API resource 254 * @param name the name 255 * @return the result 256 */ 257 @SuppressWarnings("PMD.UseObjectForClearerAPI") 258 R get(Class<O> objectClass, Class<L> objectListClass, K8sClient client, 259 APIResource context, String name); 260 } 261 262 @Override 263 @SuppressWarnings("PMD.UseLocaleWithCaseConversions") 264 public String toString() { 265 return (Strings.isNullOrEmpty(group()) ? "" : group() + "/") 266 + version().toUpperCase() + kind() + " " + name; 267 } 268 269 /** 270 * Get an object stub. If the version in parameter 271 * `gvk` is an empty string, the stub refers to the first object 272 * found with matching group and kind. 273 * 274 * @param <O> the object type 275 * @param <L> the object list type 276 * @param <R> the stub type 277 * @param objectClass the object class 278 * @param objectListClass the object list class 279 * @param client the client 280 * @param gvk the group, version and kind 281 * @param name the name 282 * @param provider the provider 283 * @return the stub if the object exists 284 * @throws ApiException the api exception 285 */ 286 @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" }) 287 public static <O extends KubernetesObject, L extends KubernetesListObject, 288 R extends K8sClusterGenericStub<O, L>> 289 R get(Class<O> objectClass, Class<L> objectListClass, 290 K8sClient client, GroupVersionKind gvk, String name, 291 GenericSupplier<O, L, R> provider) throws ApiException { 292 var context = K8s.context(client, gvk.getGroup(), gvk.getVersion(), 293 gvk.getKind()); 294 if (context.isEmpty()) { 295 throw new ApiException("No known API for " + gvk.getGroup() 296 + "/" + gvk.getVersion() + " " + gvk.getKind()); 297 } 298 return provider.get(objectClass, objectListClass, client, context.get(), 299 name); 300 } 301 302 /** 303 * Get an object stub. 304 * 305 * @param <O> the object type 306 * @param <L> the object list type 307 * @param <R> the stub type 308 * @param objectClass the object class 309 * @param objectListClass the object list class 310 * @param client the client 311 * @param context the context 312 * @param name the name 313 * @param provider the provider 314 * @return the stub if the object exists 315 * @throws ApiException the api exception 316 */ 317 @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", 318 "PMD.UseObjectForClearerAPI" }) 319 public static <O extends KubernetesObject, L extends KubernetesListObject, 320 R extends K8sClusterGenericStub<O, L>> 321 R get(Class<O> objectClass, Class<L> objectListClass, 322 K8sClient client, APIResource context, String name, 323 GenericSupplier<O, L, R> provider) { 324 return provider.get(objectClass, objectListClass, client, context, 325 name); 326 } 327 328 /** 329 * Get an object stub for a newly created object. 330 * 331 * @param <O> the object type 332 * @param <L> the object list type 333 * @param <R> the stub type 334 * @param objectClass the object class 335 * @param objectListClass the object list class 336 * @param client the client 337 * @param context the context 338 * @param model the model 339 * @param provider the provider 340 * @return the stub if the object exists 341 * @throws ApiException the api exception 342 */ 343 @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", 344 "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" }) 345 public static <O extends KubernetesObject, L extends KubernetesListObject, 346 R extends K8sClusterGenericStub<O, L>> 347 R create(Class<O> objectClass, Class<L> objectListClass, 348 K8sClient client, APIResource context, O model, 349 GenericSupplier<O, L, R> provider) throws ApiException { 350 var api = new GenericKubernetesApi<>(objectClass, objectListClass, 351 context.getGroup(), context.getPreferredVersion(), 352 context.getResourcePlural(), client); 353 api.create(model).throwsApiException(); 354 return provider.get(objectClass, objectListClass, client, 355 context, model.getMetadata().getName()); 356 } 357 358 /** 359 * Get the stubs for the objects that match 360 * the criteria from the given options. 361 * 362 * @param <O> the object type 363 * @param <L> the object list type 364 * @param <R> the stub type 365 * @param objectClass the object class 366 * @param objectListClass the object list class 367 * @param client the client 368 * @param context the context 369 * @param options the options 370 * @param provider the provider 371 * @return the collection 372 * @throws ApiException the api exception 373 */ 374 public static <O extends KubernetesObject, L extends KubernetesListObject, 375 R extends K8sClusterGenericStub<O, L>> 376 Collection<R> list(Class<O> objectClass, Class<L> objectListClass, 377 K8sClient client, APIResource context, 378 ListOptions options, GenericSupplier<O, L, R> provider) 379 throws ApiException { 380 var result = new ArrayList<R>(); 381 for (var version : candidateVersions(context)) { 382 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 383 var api = new GenericKubernetesApi<>(objectClass, objectListClass, 384 context.getGroup(), version, context.getResourcePlural(), 385 client); 386 var objs = api.list(options).throwsApiException(); 387 for (var item : objs.getObject().getItems()) { 388 result.add(provider.get(objectClass, objectListClass, client, 389 context, item.getMetadata().getName())); 390 } 391 } 392 return result; 393 } 394 395 private static List<String> candidateVersions(APIResource context) { 396 var result = new LinkedList<>(context.getVersions()); 397 result.remove(context.getPreferredVersion()); 398 result.add(0, context.getPreferredVersion()); 399 return result; 400 } 401 402}