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