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