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}