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