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