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}