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