001/*
002 * VM-Operator
003 * Copyright (C) 2023,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 com.google.gson.JsonObject;
022import io.kubernetes.client.Discovery;
023import io.kubernetes.client.Discovery.APIResource;
024import io.kubernetes.client.common.KubernetesListObject;
025import io.kubernetes.client.common.KubernetesObject;
026import io.kubernetes.client.common.KubernetesType;
027import io.kubernetes.client.custom.V1Patch;
028import io.kubernetes.client.openapi.ApiClient;
029import io.kubernetes.client.openapi.ApiException;
030import io.kubernetes.client.openapi.apis.EventsV1Api;
031import io.kubernetes.client.openapi.models.EventsV1Event;
032import io.kubernetes.client.openapi.models.V1ObjectMeta;
033import io.kubernetes.client.openapi.models.V1ObjectReference;
034import io.kubernetes.client.util.Strings;
035import io.kubernetes.client.util.generic.GenericKubernetesApi;
036import io.kubernetes.client.util.generic.KubernetesApiResponse;
037import io.kubernetes.client.util.generic.options.PatchOptions;
038import java.io.Reader;
039import java.net.HttpURLConnection;
040import java.time.OffsetDateTime;
041import java.util.Map;
042import java.util.Optional;
043import org.yaml.snakeyaml.LoaderOptions;
044import org.yaml.snakeyaml.Yaml;
045import org.yaml.snakeyaml.constructor.SafeConstructor;
046
047/**
048 * Helpers for K8s API.
049 */
050@SuppressWarnings({ "PMD.ShortClassName", "PMD.UseUtilityClass",
051    "PMD.DataflowAnomalyAnalysis" })
052public class K8s {
053
054    /**
055     * Returns the result from an API call as {@link Optional} if the
056     * call was successful. Returns an empty `Optional` if the status
057     * code is 404 (not found). Else throws an exception.
058     *
059     * @param <T> the generic type
060     * @param response the response
061     * @return the optional
062     * @throws ApiException the API exception
063     */
064    public static <T extends KubernetesType> Optional<T>
065            optional(KubernetesApiResponse<T> response) throws ApiException {
066        if (response.isSuccess()) {
067            return Optional.of(response.getObject());
068        }
069        if (response.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) {
070            return Optional.empty();
071        }
072        response.throwsApiException();
073        // Never reached
074        return Optional.empty();
075    }
076
077    /**
078     * Returns a new context with the given version as preferred version.
079     *
080     * @param context the context
081     * @param version the version
082     * @return the API resource
083     */
084    public static APIResource preferred(APIResource context, String version) {
085        assert context.getVersions().contains(version);
086        return new APIResource(context.getGroup(),
087            context.getVersions(), version, context.getKind(),
088            context.getNamespaced(), context.getResourcePlural(),
089            context.getResourceSingular());
090    }
091
092    /**
093     * Return a string representation of the context (API resource).
094     *
095     * @param context the context
096     * @return the string
097     */
098    @SuppressWarnings("PMD.UseLocaleWithCaseConversions")
099    public static String toString(APIResource context) {
100        return (Strings.isNullOrEmpty(context.getGroup()) ? ""
101            : context.getGroup() + "/")
102            + context.getPreferredVersion().toUpperCase()
103            + context.getKind();
104    }
105
106    /**
107     * Convert Yaml to Json.
108     *
109     * @param client the client
110     * @param yaml the yaml
111     * @return the json element
112     */
113    public static JsonObject yamlToJson(ApiClient client, Reader yaml) {
114        // Avoid Yaml.load due to
115        // https://github.com/kubernetes-client/java/issues/2741
116        @SuppressWarnings("PMD.UseConcurrentHashMap")
117        Map<String, Object> yamlData
118            = new Yaml(new SafeConstructor(new LoaderOptions())).load(yaml);
119
120        // There's no short-cut from Java (collections) to Gson
121        var gson = client.getJSON().getGson();
122        var jsonText = gson.toJson(yamlData);
123        return gson.fromJson(jsonText, JsonObject.class);
124    }
125
126    /**
127     * Lookup the specified API resource. If the version is `null` or
128     * empty, the preferred version in the result is the default
129     * returned from the server.
130     *
131     * @param client the client
132     * @param group the group
133     * @param version the version
134     * @param kind the kind
135     * @return the optional
136     * @throws ApiException the api exception
137     */
138    public static Optional<APIResource> context(ApiClient client,
139            String group, String version, String kind) throws ApiException {
140        var apiMatch = new Discovery(client).findAll().stream()
141            .filter(r -> r.getGroup().equals(group) && r.getKind().equals(kind)
142                && (Strings.isNullOrEmpty(version)
143                    || r.getVersions().contains(version)))
144            .findFirst();
145        if (apiMatch.isEmpty()) {
146            return Optional.empty();
147        }
148        var apiRes = apiMatch.get();
149        if (!Strings.isNullOrEmpty(version)) {
150            if (!apiRes.getVersions().contains(version)) {
151                return Optional.empty();
152            }
153            apiRes = new APIResource(apiRes.getGroup(), apiRes.getVersions(),
154                version, apiRes.getKind(), apiRes.getNamespaced(),
155                apiRes.getResourcePlural(), apiRes.getResourceSingular());
156        }
157        return Optional.of(apiRes);
158    }
159
160    /**
161     * Apply the given patch data.
162     *
163     * @param <T> the generic type
164     * @param <LT> the generic type
165     * @param api the api
166     * @param existing the existing
167     * @param update the update
168     * @return the t
169     * @throws ApiException the api exception
170     */
171    @SuppressWarnings("PMD.GenericsNaming")
172    public static <T extends KubernetesObject, LT extends KubernetesListObject>
173            T apply(GenericKubernetesApi<T, LT> api, T existing, String update)
174                    throws ApiException {
175        PatchOptions opts = new PatchOptions();
176        opts.setForce(true);
177        opts.setFieldManager("kubernetes-java-kubectl-apply");
178        var response = api.patch(existing.getMetadata().getNamespace(),
179            existing.getMetadata().getName(), V1Patch.PATCH_FORMAT_APPLY_YAML,
180            new V1Patch(update), opts).throwsApiException();
181        return response.getObject();
182    }
183
184    /**
185     * Create an object reference.
186     *
187     * @param object the object
188     * @return the v 1 object reference
189     */
190    public static V1ObjectReference
191            objectReference(KubernetesObject object) {
192        return new V1ObjectReference().apiVersion(object.getApiVersion())
193            .kind(object.getKind())
194            .namespace(object.getMetadata().getNamespace())
195            .name(object.getMetadata().getName())
196            .resourceVersion(object.getMetadata().getResourceVersion())
197            .uid(object.getMetadata().getUid());
198    }
199
200    /**
201     * Creates an event related to the object, adding reasonable defaults.
202     * 
203     *   * If `kind` is not set, it is set to "Event".
204     *   * If `metadata.namespace` is not set, it is set 
205     *     to the object's namespace.
206     *   * If neither `metadata.name` nor `matadata.generateName` are set,
207     *     set `generateName` to the object's name with a dash appended.
208     *   * If `reportingInstance` is not set, set it to the object's name.
209     *   * If `eventTime` is not set, set it to now.
210     *   * If `type` is not set, set it to "Normal"
211     *   * If `regarding` is not set, set it to the given object.
212     *
213     * @param client the client
214     * @param object the object
215     * @param event the event
216     * @throws ApiException the api exception
217     */
218    @SuppressWarnings("PMD.NPathComplexity")
219    public static void createEvent(ApiClient client,
220            KubernetesObject object, EventsV1Event event)
221            throws ApiException {
222        if (Strings.isNullOrEmpty(event.getKind())) {
223            event.kind("Event");
224        }
225        if (event.getMetadata() == null) {
226            event.metadata(new V1ObjectMeta());
227        }
228        if (Strings.isNullOrEmpty(event.getMetadata().getNamespace())) {
229            event.getMetadata().namespace(object.getMetadata().getNamespace());
230        }
231        if (Strings.isNullOrEmpty(event.getMetadata().getName())
232            && Strings.isNullOrEmpty(event.getMetadata().getGenerateName())) {
233            event.getMetadata()
234                .generateName(object.getMetadata().getName() + "-");
235        }
236        if (Strings.isNullOrEmpty(event.getReportingInstance())) {
237            event.reportingInstance(object.getMetadata().getName());
238        }
239        if (event.getEventTime() == null) {
240            event.eventTime(OffsetDateTime.now());
241        }
242        if (Strings.isNullOrEmpty(event.getType())) {
243            event.type("Normal");
244        }
245        if (event.getRegarding() == null) {
246            event.regarding(objectReference(object));
247        }
248        new EventsV1Api(client).createNamespacedEvent(
249            object.getMetadata().getNamespace(), event, null, null, null, null);
250    }
251}