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