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}