diff --git a/checkstyle.xml b/checkstyle.xml index 015ef09..088e543 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -30,8 +30,11 @@ + + + diff --git a/dev-example/test-vm.yaml b/dev-example/test-vm.yaml index 0a8a098..19144d5 100644 --- a/dev-example/test-vm.yaml +++ b/dev-example/test-vm.yaml @@ -13,11 +13,11 @@ spec: requests: cpu: 1 memory: 2Gi - + guestShutdownStops: true cloudInit: {} - + vm: # state: Running bootMenu: yes diff --git a/org.jdrupes.vmoperator.common/.settings/net.sf.jautodoc.prefs b/org.jdrupes.vmoperator.common/.settings/net.sf.jautodoc.prefs new file mode 100644 index 0000000..8b8b906 --- /dev/null +++ b/org.jdrupes.vmoperator.common/.settings/net.sf.jautodoc.prefs @@ -0,0 +1,7 @@ +add_header=true +eclipse.preferences.version=1 +header_text=/*\n * VM-Operator\n * Copyright (C) 2024 Michael N. Lipp\n * \n * This program is free software\: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n */ +project_specific_settings=true +visibility_package=false +visibility_private=false +visibility_protected=false diff --git a/org.jdrupes.vmoperator.common/build.gradle b/org.jdrupes.vmoperator.common/build.gradle index ed082a1..42c05ae 100644 --- a/org.jdrupes.vmoperator.common/build.gradle +++ b/org.jdrupes.vmoperator.common/build.gradle @@ -10,5 +10,6 @@ plugins { dependencies { api project(':org.jdrupes.vmoperator.util') - api 'io.kubernetes:client-java:[18.0.0,19)' + api 'io.kubernetes:client-java:[19.0.0,20.0.0)' + api 'org.yaml:snakeyaml' } diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java index f61b431..e350cf1 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8s.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2023 Michael N. Lipp + * Copyright (C) 2023,2024 Michael N. Lipp * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -18,29 +18,31 @@ package org.jdrupes.vmoperator.common; +import com.google.gson.JsonObject; +import io.kubernetes.client.Discovery; +import io.kubernetes.client.Discovery.APIResource; import io.kubernetes.client.common.KubernetesListObject; import io.kubernetes.client.common.KubernetesObject; +import io.kubernetes.client.common.KubernetesType; import io.kubernetes.client.custom.V1Patch; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.apis.ApisApi; -import io.kubernetes.client.openapi.apis.CustomObjectsApi; -import io.kubernetes.client.openapi.models.V1APIGroup; -import io.kubernetes.client.openapi.models.V1ConfigMap; -import io.kubernetes.client.openapi.models.V1ConfigMapList; -import io.kubernetes.client.openapi.models.V1GroupVersionForDiscovery; +import io.kubernetes.client.openapi.apis.EventsV1Api; +import io.kubernetes.client.openapi.models.EventsV1Event; import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.openapi.models.V1ObjectReference; -import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim; -import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimList; -import io.kubernetes.client.openapi.models.V1Pod; -import io.kubernetes.client.openapi.models.V1PodList; +import io.kubernetes.client.util.Strings; import io.kubernetes.client.util.generic.GenericKubernetesApi; -import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; -import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; -import io.kubernetes.client.util.generic.options.DeleteOptions; +import io.kubernetes.client.util.generic.KubernetesApiResponse; import io.kubernetes.client.util.generic.options.PatchOptions; +import java.io.Reader; +import java.net.HttpURLConnection; +import java.time.OffsetDateTime; +import java.util.Map; import java.util.Optional; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; /** * Helpers for K8s API. @@ -50,89 +52,80 @@ import java.util.Optional; public class K8s { /** - * Given a groupVersion, returns only the version. + * Returns the result from an API call as {@link Optional} if the + * call was successful. Returns an empty `Optional` if the status + * code is 404 (not found). Else throws an exception. * - * @param groupVersion the group version - * @return the string + * @param the generic type + * @param response the response + * @return the optional + * @throws ApiException the API exception */ - public static String version(String groupVersion) { - return groupVersion.substring(groupVersion.lastIndexOf('/') + 1); + public static Optional + optional(KubernetesApiResponse response) throws ApiException { + if (response.isSuccess()) { + return Optional.of(response.getObject()); + } + if (response.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) { + return Optional.empty(); + } + response.throwsApiException(); + // Never reached + return Optional.empty(); } /** - * Get PVC API. + * Convert Yaml to Json. * * @param client the client - * @return the generic kubernetes api + * @param yaml the yaml + * @return the json element */ - public static GenericKubernetesApi pvcApi(ApiClient client) { - return new GenericKubernetesApi<>(V1PersistentVolumeClaim.class, - V1PersistentVolumeClaimList.class, "", "v1", - "persistentvolumeclaims", client); + public static JsonObject yamlToJson(ApiClient client, Reader yaml) { + // Avoid Yaml.load due to + // https://github.com/kubernetes-client/java/issues/2741 + @SuppressWarnings("PMD.UseConcurrentHashMap") + Map yamlData + = new Yaml(new SafeConstructor(new LoaderOptions())).load(yaml); + + // There's no short-cut from Java (collections) to Gson + var gson = client.getJSON().getGson(); + var jsonText = gson.toJson(yamlData); + return gson.fromJson(jsonText, JsonObject.class); } /** - * Get config map API. - * - * @param client the client - * @return the generic kubernetes api - */ - public static GenericKubernetesApi cmApi(ApiClient client) { - return new GenericKubernetesApi<>(V1ConfigMap.class, - V1ConfigMapList.class, "", "v1", "configmaps", client); - } - - /** - * Get pod API. - * - * @param client the client - * @return the generic kubernetes api - */ - public static GenericKubernetesApi - podApi(ApiClient client) { - return new GenericKubernetesApi<>(V1Pod.class, V1PodList.class, "", - "v1", "pods", client); - } - - /** - * Get the API for a custom resource. + * Lookup the specified API resource. If the version is `null` or + * empty, the preferred version in the result is the default + * returned from the server. * * @param client the client * @param group the group + * @param version the version * @param kind the kind - * @param namespace the namespace - * @param name the name - * @return the dynamic kubernetes api + * @return the optional * @throws ApiException the api exception */ - @SuppressWarnings("PMD.UseObjectForClearerAPI") - public static Optional crApi(ApiClient client, - String group, String kind, String namespace, String name) - throws ApiException { - var apis = new ApisApi(client).getAPIVersions(); - var crdVersions = apis.getGroups().stream() - .filter(g -> g.getName().equals(group)).findFirst() - .map(V1APIGroup::getVersions).stream().flatMap(l -> l.stream()) - .map(V1GroupVersionForDiscovery::getVersion).toList(); - var coa = new CustomObjectsApi(client); - for (var crdVersion : crdVersions) { - var crdApiRes = coa.getAPIResources(group, crdVersion) - .getResources().stream().filter(r -> kind.equals(r.getKind())) - .findFirst(); - if (crdApiRes.isEmpty()) { - continue; - } - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - var crApi = new DynamicKubernetesApi(group, - crdVersion, crdApiRes.get().getName(), client); - var customResource = crApi.get(namespace, name); - if (customResource.isSuccess()) { - return Optional.of(crApi); - } + public static Optional context(ApiClient client, + String group, String version, String kind) throws ApiException { + var apiMatch = new Discovery(client).findAll().stream() + .filter(r -> r.getGroup().equals(group) && r.getKind().equals(kind) + && (Strings.isNullOrEmpty(version) + || r.getVersions().contains(version))) + .findFirst(); + if (apiMatch.isEmpty()) { + return Optional.empty(); } - return Optional.empty(); + var apiRes = apiMatch.get(); + if (!Strings.isNullOrEmpty(version)) { + if (!apiRes.getVersions().contains(version)) { + return Optional.empty(); + } + apiRes = new APIResource(apiRes.getGroup(), apiRes.getVersions(), + version, apiRes.getKind(), apiRes.getNamespaced(), + apiRes.getResourcePlural(), apiRes.getResourceSingular()); + } + return Optional.of(apiRes); } /** @@ -144,6 +137,7 @@ public class K8s { * @param meta the meta * @return the object */ + @Deprecated public static Optional get(GenericKubernetesApi api, V1ObjectMeta meta) { @@ -154,36 +148,6 @@ public class K8s { return Optional.empty(); } - /** - * Delete an object. - * - * @param the generic type - * @param the generic type - * @param api the api - * @param object the object - */ - public static - void delete(GenericKubernetesApi api, T object) - throws ApiException { - api.delete(object.getMetadata().getNamespace(), - object.getMetadata().getName()).throwsApiException(); - } - - /** - * Delete an object. - * - * @param the generic type - * @param the generic type - * @param api the api - * @param object the object - */ - public static - void delete(GenericKubernetesApi api, T object, - DeleteOptions options) throws ApiException { - api.delete(object.getMetadata().getNamespace(), - object.getMetadata().getName(), options).throwsApiException(); - } - /** * Apply the given patch data. * @@ -213,7 +177,7 @@ public class K8s { * @return the v 1 object reference */ public static V1ObjectReference - objectReference(DynamicKubernetesObject object) { + objectReference(KubernetesObject object) { return new V1ObjectReference().apiVersion(object.getApiVersion()) .kind(object.getKind()) .namespace(object.getMetadata().getNamespace()) @@ -221,4 +185,54 @@ public class K8s { .resourceVersion(object.getMetadata().getResourceVersion()) .uid(object.getMetadata().getUid()); } + + /** + * Creates an event related to the object, adding reasonable defaults. + * + * * If `kind` is not set, it is set to "Event". + * * If `metadata.namespace` is not set, it is set + * to the object's namespace. + * * If neither `metadata.name` nor `matadata.generateName` are set, + * set `generateName` to the object's name with a dash appended. + * * If `reportingInstance` is not set, set it to the object's name. + * * If `eventTime` is not set, set it to now. + * * If `type` is not set, set it to "Normal" + * * If `regarding` is not set, set it to the given object. + * + * @param event the event + * @throws ApiException + */ + @SuppressWarnings("PMD.NPathComplexity") + public static void createEvent(ApiClient client, + KubernetesObject object, EventsV1Event event) + throws ApiException { + if (Strings.isNullOrEmpty(event.getKind())) { + event.kind("Event"); + } + if (event.getMetadata() == null) { + event.metadata(new V1ObjectMeta()); + } + if (Strings.isNullOrEmpty(event.getMetadata().getNamespace())) { + event.getMetadata().namespace(object.getMetadata().getNamespace()); + } + if (Strings.isNullOrEmpty(event.getMetadata().getName()) + && Strings.isNullOrEmpty(event.getMetadata().getGenerateName())) { + event.getMetadata() + .generateName(object.getMetadata().getName() + "-"); + } + if (Strings.isNullOrEmpty(event.getReportingInstance())) { + event.reportingInstance(object.getMetadata().getName()); + } + if (event.getEventTime() == null) { + event.eventTime(OffsetDateTime.now()); + } + if (Strings.isNullOrEmpty(event.getType())) { + event.type("Normal"); + } + if (event.getRegarding() == null) { + event.regarding(objectReference(object)); + } + new EventsV1Api(client).createNamespacedEvent( + object.getMetadata().getNamespace(), event, null, null, null, null); + } } diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java new file mode 100644 index 0000000..b7106fb --- /dev/null +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java @@ -0,0 +1,759 @@ +/* + * VM-Operator + * Copyright (C) 2024 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.common; + +import io.kubernetes.client.openapi.ApiCallback; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.ApiResponse; +import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.Pair; +import io.kubernetes.client.openapi.auth.Authentication; +import io.kubernetes.client.util.ClientBuilder; +import io.kubernetes.client.util.generic.options.PatchOptions; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; +import java.text.DateFormat; +import java.time.format.DateTimeFormatter; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import javax.net.ssl.KeyManager; +import okhttp3.Call; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Request.Builder; +import okhttp3.RequestBody; +import okhttp3.Response; + +/** + * A client with some additional properties. + */ +@SuppressWarnings({ "PMD.ExcessivePublicCount", "PMD.TooManyMethods", + "PMD.LinguisticNaming", "checkstyle:LineLength" }) +public class K8sClient extends ApiClient { + + private ApiClient apiClient; + private PatchOptions defaultPatchOptions; + + /** + * Instantiates a new client. + * + * @throws IOException Signals that an I/O exception has occurred. + */ + public K8sClient() throws IOException { + defaultPatchOptions = new PatchOptions(); + defaultPatchOptions.setFieldManager("kubernetes-java-kubectl-apply"); + } + + private ApiClient apiClient() { + if (apiClient == null) { + try { + apiClient = ClientBuilder.standard().build(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + return apiClient; + } + + /** + * Gets the default patch options. + * + * @return the defaultPatchOptions + */ + public PatchOptions defaultPatchOptions() { + return defaultPatchOptions; + } + + /** + * Changes the default patch options. + * + * @param patchOptions the patch options + * @return the client + */ + public K8sClient with(PatchOptions patchOptions) { + defaultPatchOptions = patchOptions; + return this; + } + + /** + * @return + * @see ApiClient#getBasePath() + */ + public String getBasePath() { + return apiClient().getBasePath(); + } + + /** + * @param basePath + * @return + * @see ApiClient#setBasePath(java.lang.String) + */ + public ApiClient setBasePath(String basePath) { + return apiClient().setBasePath(basePath); + } + + /** + * @return + * @see ApiClient#getHttpClient() + */ + public OkHttpClient getHttpClient() { + return apiClient().getHttpClient(); + } + + /** + * @param newHttpClient + * @return + * @see ApiClient#setHttpClient(okhttp3.OkHttpClient) + */ + public ApiClient setHttpClient(OkHttpClient newHttpClient) { + return apiClient().setHttpClient(newHttpClient); + } + + /** + * @return + * @see ApiClient#getJSON() + */ + @SuppressWarnings("abbreviationAsWordInName") + public JSON getJSON() { + return apiClient().getJSON(); + } + + /** + * @param json + * @return + * @see ApiClient#setJSON(io.kubernetes.client.openapi.JSON) + */ + @SuppressWarnings("abbreviationAsWordInName") + public ApiClient setJSON(JSON json) { + return apiClient().setJSON(json); + } + + /** + * @return + * @see ApiClient#isVerifyingSsl() + */ + public boolean isVerifyingSsl() { + return apiClient().isVerifyingSsl(); + } + + /** + * @param verifyingSsl + * @return + * @see ApiClient#setVerifyingSsl(boolean) + */ + public ApiClient setVerifyingSsl(boolean verifyingSsl) { + return apiClient().setVerifyingSsl(verifyingSsl); + } + + /** + * @return + * @see ApiClient#getSslCaCert() + */ + public InputStream getSslCaCert() { + return apiClient().getSslCaCert(); + } + + /** + * @param sslCaCert + * @return + * @see ApiClient#setSslCaCert(java.io.InputStream) + */ + public ApiClient setSslCaCert(InputStream sslCaCert) { + return apiClient().setSslCaCert(sslCaCert); + } + + /** + * @return + * @see ApiClient#getKeyManagers() + */ + public KeyManager[] getKeyManagers() { + return apiClient().getKeyManagers(); + } + + /** + * @param managers + * @return + * @see ApiClient#setKeyManagers(javax.net.ssl.KeyManager[]) + */ + @SuppressWarnings("PMD.UseVarargs") + public ApiClient setKeyManagers(KeyManager[] managers) { + return apiClient().setKeyManagers(managers); + } + + /** + * @return + * @see ApiClient#getDateFormat() + */ + public DateFormat getDateFormat() { + return apiClient().getDateFormat(); + } + + /** + * @param dateFormat + * @return + * @see ApiClient#setDateFormat(java.text.DateFormat) + */ + public ApiClient setDateFormat(DateFormat dateFormat) { + return apiClient().setDateFormat(dateFormat); + } + + /** + * @param dateFormat + * @return + * @see ApiClient#setSqlDateFormat(java.text.DateFormat) + */ + public ApiClient setSqlDateFormat(DateFormat dateFormat) { + return apiClient().setSqlDateFormat(dateFormat); + } + + /** + * @param dateFormat + * @return + * @see ApiClient#setOffsetDateTimeFormat(java.time.format.DateTimeFormatter) + */ + public ApiClient setOffsetDateTimeFormat(DateTimeFormatter dateFormat) { + return apiClient().setOffsetDateTimeFormat(dateFormat); + } + + /** + * @param dateFormat + * @return + * @see ApiClient#setLocalDateFormat(java.time.format.DateTimeFormatter) + */ + public ApiClient setLocalDateFormat(DateTimeFormatter dateFormat) { + return apiClient().setLocalDateFormat(dateFormat); + } + + /** + * @param lenientOnJson + * @return + * @see ApiClient#setLenientOnJson(boolean) + */ + public ApiClient setLenientOnJson(boolean lenientOnJson) { + return apiClient().setLenientOnJson(lenientOnJson); + } + + /** + * @return + * @see ApiClient#getAuthentications() + */ + public Map getAuthentications() { + return apiClient().getAuthentications(); + } + + /** + * @param authName + * @return + * @see ApiClient#getAuthentication(java.lang.String) + */ + public Authentication getAuthentication(String authName) { + return apiClient().getAuthentication(authName); + } + + /** + * @param username + * @see ApiClient#setUsername(java.lang.String) + */ + public void setUsername(String username) { + apiClient().setUsername(username); + } + + /** + * @param password + * @see ApiClient#setPassword(java.lang.String) + */ + public void setPassword(String password) { + apiClient().setPassword(password); + } + + /** + * @param apiKey + * @see ApiClient#setApiKey(java.lang.String) + */ + public void setApiKey(String apiKey) { + apiClient().setApiKey(apiKey); + } + + /** + * @param apiKeyPrefix + * @see ApiClient#setApiKeyPrefix(java.lang.String) + */ + public void setApiKeyPrefix(String apiKeyPrefix) { + apiClient().setApiKeyPrefix(apiKeyPrefix); + } + + /** + * @param accessToken + * @see ApiClient#setAccessToken(java.lang.String) + */ + public void setAccessToken(String accessToken) { + apiClient().setAccessToken(accessToken); + } + + /** + * @param userAgent + * @return + * @see ApiClient#setUserAgent(java.lang.String) + */ + public ApiClient setUserAgent(String userAgent) { + return apiClient().setUserAgent(userAgent); + } + + /** + * @return + * @see java.lang.Object#toString() + */ + public String toString() { + return apiClient().toString(); + } + + /** + * @param key + * @param value + * @return + * @see ApiClient#addDefaultHeader(java.lang.String, java.lang.String) + */ + public ApiClient addDefaultHeader(String key, String value) { + return apiClient().addDefaultHeader(key, value); + } + + /** + * @param key + * @param value + * @return + * @see ApiClient#addDefaultCookie(java.lang.String, java.lang.String) + */ + public ApiClient addDefaultCookie(String key, String value) { + return apiClient().addDefaultCookie(key, value); + } + + /** + * @return + * @see ApiClient#isDebugging() + */ + public boolean isDebugging() { + return apiClient().isDebugging(); + } + + /** + * @param debugging + * @return + * @see ApiClient#setDebugging(boolean) + */ + public ApiClient setDebugging(boolean debugging) { + return apiClient().setDebugging(debugging); + } + + /** + * @return + * @see ApiClient#getTempFolderPath() + */ + public String getTempFolderPath() { + return apiClient().getTempFolderPath(); + } + + /** + * @param tempFolderPath + * @return + * @see ApiClient#setTempFolderPath(java.lang.String) + */ + public ApiClient setTempFolderPath(String tempFolderPath) { + return apiClient().setTempFolderPath(tempFolderPath); + } + + /** + * @return + * @see ApiClient#getConnectTimeout() + */ + public int getConnectTimeout() { + return apiClient().getConnectTimeout(); + } + + /** + * @param connectionTimeout + * @return + * @see ApiClient#setConnectTimeout(int) + */ + public ApiClient setConnectTimeout(int connectionTimeout) { + return apiClient().setConnectTimeout(connectionTimeout); + } + + /** + * @return + * @see ApiClient#getReadTimeout() + */ + public int getReadTimeout() { + return apiClient().getReadTimeout(); + } + + /** + * @param readTimeout + * @return + * @see ApiClient#setReadTimeout(int) + */ + public ApiClient setReadTimeout(int readTimeout) { + return apiClient().setReadTimeout(readTimeout); + } + + /** + * @return + * @see ApiClient#getWriteTimeout() + */ + public int getWriteTimeout() { + return apiClient().getWriteTimeout(); + } + + /** + * @param writeTimeout + * @return + * @see ApiClient#setWriteTimeout(int) + */ + public ApiClient setWriteTimeout(int writeTimeout) { + return apiClient().setWriteTimeout(writeTimeout); + } + + /** + * @param param + * @return + * @see ApiClient#parameterToString(java.lang.Object) + */ + public String parameterToString(Object param) { + return apiClient().parameterToString(param); + } + + /** + * @param name + * @param value + * @return + * @see ApiClient#parameterToPair(java.lang.String, java.lang.Object) + */ + public List parameterToPair(String name, Object value) { + return apiClient().parameterToPair(name, value); + } + + /** + * @param collectionFormat + * @param name + * @param value + * @return + * @see ApiClient#parameterToPairs(java.lang.String, java.lang.String, java.util.Collection) + */ + @SuppressWarnings({ "rawtypes", "PMD.AvoidDuplicateLiterals" }) + public List parameterToPairs(String collectionFormat, String name, + Collection value) { + return apiClient().parameterToPairs(collectionFormat, name, value); + } + + /** + * @param collectionFormat + * @param value + * @return + * @see ApiClient#collectionPathParameterToString(java.lang.String, java.util.Collection) + */ + @SuppressWarnings("rawtypes") + public String collectionPathParameterToString(String collectionFormat, + Collection value) { + return apiClient().collectionPathParameterToString(collectionFormat, + value); + } + + /** + * @param filename + * @return + * @see ApiClient#sanitizeFilename(java.lang.String) + */ + public String sanitizeFilename(String filename) { + return apiClient().sanitizeFilename(filename); + } + + /** + * @param mime + * @return + * @see ApiClient#isJsonMime(java.lang.String) + */ + public boolean isJsonMime(String mime) { + return apiClient().isJsonMime(mime); + } + + /** + * @param accepts + * @return + * @see ApiClient#selectHeaderAccept(java.lang.String[]) + */ + @SuppressWarnings("PMD.UseVarargs") + public String selectHeaderAccept(String[] accepts) { + return apiClient().selectHeaderAccept(accepts); + } + + /** + * @param contentTypes + * @return + * @see ApiClient#selectHeaderContentType(java.lang.String[]) + */ + @SuppressWarnings("PMD.UseVarargs") + public String selectHeaderContentType(String[] contentTypes) { + return apiClient().selectHeaderContentType(contentTypes); + } + + /** + * @param str + * @return + * @see ApiClient#escapeString(java.lang.String) + */ + public String escapeString(String str) { + return apiClient().escapeString(str); + } + + /** + * @param + * @param response + * @param returnType + * @return + * @throws ApiException + * @see ApiClient#deserialize(okhttp3.Response, java.lang.reflect.Type) + */ + public T deserialize(Response response, Type returnType) + throws ApiException { + return apiClient().deserialize(response, returnType); + } + + /** + * @param obj + * @param contentType + * @return + * @throws ApiException + * @see ApiClient#serialize(java.lang.Object, java.lang.String) + */ + public RequestBody serialize(Object obj, String contentType) + throws ApiException { + return apiClient().serialize(obj, contentType); + } + + /** + * @param response + * @return + * @throws ApiException + * @see ApiClient#downloadFileFromResponse(okhttp3.Response) + */ + public File downloadFileFromResponse(Response response) + throws ApiException { + return apiClient().downloadFileFromResponse(response); + } + + /** + * @param response + * @return + * @throws IOException + * @see ApiClient#prepareDownloadFile(okhttp3.Response) + */ + public File prepareDownloadFile(Response response) throws IOException { + return apiClient().prepareDownloadFile(response); + } + + /** + * @param + * @param call + * @return + * @throws ApiException + * @see ApiClient#execute(okhttp3.Call) + */ + public ApiResponse execute(Call call) throws ApiException { + return apiClient().execute(call); + } + + /** + * @param + * @param call + * @param returnType + * @return + * @throws ApiException + * @see ApiClient#execute(okhttp3.Call, java.lang.reflect.Type) + */ + public ApiResponse execute(Call call, Type returnType) + throws ApiException { + return apiClient().execute(call, returnType); + } + + /** + * @param + * @param call + * @param callback + * @see ApiClient#executeAsync(okhttp3.Call, io.kubernetes.client.openapi.ApiCallback) + */ + public void executeAsync(Call call, ApiCallback callback) { + apiClient().executeAsync(call, callback); + } + + /** + * @param + * @param call + * @param returnType + * @param callback + * @see ApiClient#executeAsync(okhttp3.Call, java.lang.reflect.Type, io.kubernetes.client.openapi.ApiCallback) + */ + public void executeAsync(Call call, Type returnType, + ApiCallback callback) { + apiClient().executeAsync(call, returnType, callback); + } + + /** + * @param + * @param response + * @param returnType + * @return + * @throws ApiException + * @see ApiClient#handleResponse(okhttp3.Response, java.lang.reflect.Type) + */ + public T handleResponse(Response response, Type returnType) + throws ApiException { + return apiClient().handleResponse(response, returnType); + } + + /** + * @param path + * @param method + * @param queryParams + * @param collectionQueryParams + * @param body + * @param headerParams + * @param cookieParams + * @param formParams + * @param authNames + * @param callback + * @return + * @throws ApiException + * @see ApiClient#buildCall(java.lang.String, java.lang.String, java.util.List, java.util.List, java.lang.Object, java.util.Map, java.util.Map, java.util.Map, java.lang.String[], io.kubernetes.client.openapi.ApiCallback) + */ + @SuppressWarnings({ "rawtypes", "PMD.ExcessiveParameterList" }) + public Call buildCall(String path, String method, List queryParams, + List collectionQueryParams, Object body, + Map headerParams, Map cookieParams, + Map formParams, String[] authNames, + ApiCallback callback) throws ApiException { + return apiClient().buildCall(path, method, queryParams, + collectionQueryParams, body, headerParams, cookieParams, formParams, + authNames, callback); + } + + /** + * @param path + * @param method + * @param queryParams + * @param collectionQueryParams + * @param body + * @param headerParams + * @param cookieParams + * @param formParams + * @param authNames + * @param callback + * @return + * @throws ApiException + * @see ApiClient#buildRequest(java.lang.String, java.lang.String, java.util.List, java.util.List, java.lang.Object, java.util.Map, java.util.Map, java.util.Map, java.lang.String[], io.kubernetes.client.openapi.ApiCallback) + */ + @SuppressWarnings({ "rawtypes", "PMD.ExcessiveParameterList" }) + public Request buildRequest(String path, String method, + List queryParams, List collectionQueryParams, + Object body, Map headerParams, + Map cookieParams, Map formParams, + String[] authNames, ApiCallback callback) throws ApiException { + return apiClient().buildRequest(path, method, queryParams, + collectionQueryParams, body, headerParams, cookieParams, formParams, + authNames, callback); + } + + /** + * @param path + * @param queryParams + * @param collectionQueryParams + * @return + * @see ApiClient#buildUrl(java.lang.String, java.util.List, java.util.List) + */ + public String buildUrl(String path, List queryParams, + List collectionQueryParams) { + return apiClient().buildUrl(path, queryParams, collectionQueryParams); + } + + /** + * @param headerParams + * @param reqBuilder + * @see ApiClient#processHeaderParams(java.util.Map, okhttp3.Request.Builder) + */ + public void processHeaderParams(Map headerParams, + Builder reqBuilder) { + apiClient().processHeaderParams(headerParams, reqBuilder); + } + + /** + * @param cookieParams + * @param reqBuilder + * @see ApiClient#processCookieParams(java.util.Map, okhttp3.Request.Builder) + */ + public void processCookieParams(Map cookieParams, + Builder reqBuilder) { + apiClient().processCookieParams(cookieParams, reqBuilder); + } + + /** + * @param authNames + * @param queryParams + * @param headerParams + * @param cookieParams + * @see ApiClient#updateParamsForAuth(java.lang.String[], java.util.List, java.util.Map, java.util.Map) + */ + public void updateParamsForAuth(String[] authNames, List queryParams, + Map headerParams, + Map cookieParams) { + apiClient().updateParamsForAuth(authNames, queryParams, headerParams, + cookieParams); + } + + /** + * @param formParams + * @return + * @see ApiClient#buildRequestBodyFormEncoding(java.util.Map) + */ + public RequestBody + buildRequestBodyFormEncoding(Map formParams) { + return apiClient().buildRequestBodyFormEncoding(formParams); + } + + /** + * @param formParams + * @return + * @see ApiClient#buildRequestBodyMultipart(java.util.Map) + */ + public RequestBody + buildRequestBodyMultipart(Map formParams) { + return apiClient().buildRequestBodyMultipart(formParams); + } + + /** + * @param file + * @return + * @see ApiClient#guessContentTypeFromFile(java.io.File) + */ + public String guessContentTypeFromFile(File file) { + return apiClient().guessContentTypeFromFile(file); + } + +} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java new file mode 100644 index 0000000..6a4410f --- /dev/null +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModel.java @@ -0,0 +1,114 @@ +/* + * VM-Operator + * Copyright (C) 2024 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.common; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import io.kubernetes.client.common.KubernetesObject; +import io.kubernetes.client.openapi.models.V1ObjectMeta; + +/** + * Represents a Kubernetes object using a JSON data structure. + * Some information that is common to all Kubernetes objects, + * notably the metadata, is made available through the methods + * defined by {@link KubernetesObject}. + */ +@SuppressWarnings("PMD.DataClass") +public class K8sDynamicModel implements KubernetesObject { + + private final V1ObjectMeta metadata; + private final JsonObject data; + + /** + * Instantiates a new model from the JSON representation. + * + * @param delegate the gson instance to use for extracting structured data + * @param json the JSON + */ + public K8sDynamicModel(Gson delegate, JsonObject json) { + this.data = json; + metadata = delegate.fromJson(data.get("metadata"), V1ObjectMeta.class); + } + + @Override + public String getApiVersion() { + return apiVersion(); + } + + /** + * Gets the API version. (Abbreviated method name for convenience.) + * + * @return the API version + */ + public String apiVersion() { + return data.get("apiVersion").getAsString(); + } + + @Override + public String getKind() { + return kind(); + } + + /** + * Gets the kind. (Abbreviated method name for convenience.) + * + * @return the kind + */ + public String kind() { + return data.get("kind").getAsString(); + } + + @Override + public V1ObjectMeta getMetadata() { + return metadata; + } + + /** + * Gets the metadata. (Abbreviated method name for convenience.) + * + * @return the metadata + */ + public V1ObjectMeta metadata() { + return metadata; + } + + /** + * Gets the data. + * + * @return the data + */ + public JsonObject data() { + return data; + } + + /** + * Convenience method for getting the status. + * + * @return the JSON object describing the status + */ + public JsonObject status() { + return data.getAsJsonObject("status"); + } + + @Override + public String toString() { + return data.toString(); + } + +} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelTypeAdapterFactory.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelTypeAdapterFactory.java new file mode 100644 index 0000000..9018744 --- /dev/null +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelTypeAdapterFactory.java @@ -0,0 +1,130 @@ +/* + * VM-Operator + * Copyright (C) 2024 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.common; + +import com.google.gson.Gson; +import com.google.gson.InstanceCreator; +import com.google.gson.JsonObject; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.lang.reflect.Type; + +/** + * A factory for creating K8sDynamicModel(s) objects. + */ +public class K8sDynamicModelTypeAdapterFactory implements TypeAdapterFactory { + + /** + * Creates a type adapter for the given type. + * + * @param the generic type + * @param gson the gson + * @param typeToken the type token + * @return the type adapter or null if the type is not handles by + * this factory + */ + @SuppressWarnings("unchecked") + public TypeAdapter create(Gson gson, TypeToken typeToken) { + if (TypeToken.get(K8sDynamicModel.class).equals(typeToken)) { + return (TypeAdapter) (new K8sDynamicModelCreator(gson)); + } + if (TypeToken.get(K8sDynamicModels.class).equals(typeToken)) { + return (TypeAdapter) (new K8sDynamicModelsCreator(gson)); + } + return null; + } + + /** + * The Class K8sDynamicModelCreator. + */ + /* default */ class K8sDynamicModelCreator + extends TypeAdapter + implements InstanceCreator { + private final Gson delegate; + + /** + * Instantiates a new object state creator. + * + * @param delegate the delegate + */ + public K8sDynamicModelCreator(Gson delegate) { + this.delegate = delegate; + } + + @Override + public K8sDynamicModel createInstance(Type type) { + return new K8sDynamicModel(delegate, null); + } + + @Override + public void write(JsonWriter jsonWriter, K8sDynamicModel state) + throws IOException { + jsonWriter.jsonValue(delegate.toJson(state.data())); + } + + @Override + public K8sDynamicModel read(JsonReader jsonReader) + throws IOException { + return new K8sDynamicModel(delegate, + delegate.fromJson(jsonReader, JsonObject.class)); + } + } + + /** + * The Class K8sDynamicModelsCreator. + */ + /* default */class K8sDynamicModelsCreator + extends TypeAdapter + implements InstanceCreator { + + private final Gson delegate; + + /** + * Instantiates a new object states creator. + * + * @param delegate the delegate + */ + public K8sDynamicModelsCreator(Gson delegate) { + this.delegate = delegate; + } + + @Override + public K8sDynamicModels createInstance(Type type) { + return new K8sDynamicModels(delegate, null); + } + + @Override + public void write(JsonWriter jsonWriter, K8sDynamicModels states) + throws IOException { + jsonWriter.jsonValue(delegate.toJson(states.data())); + } + + @Override + public K8sDynamicModels read(JsonReader jsonReader) + throws IOException { + return new K8sDynamicModels(delegate, + delegate.fromJson(jsonReader, JsonObject.class)); + } + } + +} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModels.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModels.java new file mode 100644 index 0000000..165b10e --- /dev/null +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModels.java @@ -0,0 +1,163 @@ +/* + * VM-Operator + * Copyright (C) 2024 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.common; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.kubernetes.client.common.KubernetesListObject; +import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.openapi.models.V1ListMeta; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Represents a list of Kubernetes objects each of which is + * represented using a JSON data structure. + * Some information that is common to all Kubernetes objects, + * notably the metadata, is made available through the methods + * defined by {@link KubernetesListObject}. + */ +public class K8sDynamicModels implements KubernetesListObject { + + private final JsonObject data; + private final V1ListMeta metadata; + private final List items; + + /** + * Initialize the object list using the given JSON data. + * + * @param delegate the gson instance to use for extracting structured data + * @param data the data + */ + public K8sDynamicModels(Gson delegate, JsonObject data) { + this.data = data; + metadata = delegate.fromJson(data.get("metadata"), V1ListMeta.class); + items = new ArrayList<>(); + for (JsonElement e : data.get("items").getAsJsonArray()) { + items.add(new K8sDynamicModel(delegate, e.getAsJsonObject())); + } + } + + @Override + public String getApiVersion() { + return apiVersion(); + } + + /** + * Gets the API version. (Abbreviated method name for convenience.) + * + * @return the API version + */ + public String apiVersion() { + return data.get("apiVersion").getAsString(); + } + + @Override + public String getKind() { + return kind(); + } + + /** + * Gets the kind. (Abbreviated method name for convenience.) + * + * @return the kind + */ + public String kind() { + return data.get("kind").getAsString(); + } + + @Override + public V1ListMeta getMetadata() { + return metadata; + } + + /** + * Gets the metadata. (Abbreviated method name for convenience.) + * + * @return the metadata + */ + public V1ListMeta metadata() { + return metadata; + } + + /** + * Returns the JSON representation of this object. + * + * @return the JOSN representation + */ + public JsonObject data() { + return data; + } + + @Override + public List getItems() { + return items; + } + + /** + * Sets the api version. + * + * @param apiVersion the new api version + */ + public void setApiVersion(String apiVersion) { + data.addProperty("apiVersion", apiVersion); + } + + /** + * Sets the kind. + * + * @param kind the new kind + */ + public void setKind(String kind) { + data.addProperty("kind", kind); + } + + /** + * Sets the metadata. + * + * @param objectMeta the new metadata + */ + public void setMetadata(V1ListMeta objectMeta) { + data.add("metadata", + Configuration.getDefaultApiClient().getJSON().getGson() + .toJsonTree(objectMeta)); + } + + @Override + public int hashCode() { + return Objects.hash(data); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + K8sDynamicModels other = (K8sDynamicModels) obj; + return Objects.equals(data, other.data); + } +} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java new file mode 100644 index 0000000..1ab33ca --- /dev/null +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicStub.java @@ -0,0 +1,109 @@ +/* + * VM-Operator + * Copyright (C) 2024 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.common; + +import io.kubernetes.client.Discovery.APIResource; +import io.kubernetes.client.apimachinery.GroupVersionKind; +import io.kubernetes.client.openapi.ApiException; +import java.io.Reader; + +/** + * A stub for namespaced custom objects. It uses a dynamic model + * (see {@link K8sDynamicModel}) for representing the object's + * state and can therefore be used for any kind of object, especially + * custom objects. + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class K8sDynamicStub + extends K8sGenericStub { + + /** + * Instantiates a new dynamic stub. + * + * @param objectClass the object class + * @param objectListClass the object list class + * @param client the client + * @param context the context + * @param namespace the namespace + * @param name the name + */ + public K8sDynamicStub(Class objectClass, + Class objectListClass, K8sClient client, + APIResource context, String namespace, String name) { + super(objectClass, objectListClass, client, context, namespace, name); + } + + /** + * Get a dynamic object stub. If the version in parameter + * `gvk` is an empty string, the stub refers to the first object with + * matching group and kind. + * + * @param client the client + * @param gvk the group, version and kind + * @param namespace the namespace + * @param name the name + * @return the stub if the object exists + * @throws ApiException the api exception + */ + @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", + "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" }) + public static K8sDynamicStub get(K8sClient client, + GroupVersionKind gvk, String namespace, String name) + throws ApiException { + return K8sGenericStub.get(K8sDynamicModel.class, K8sDynamicModels.class, + client, gvk, namespace, name, K8sDynamicStub::new); + } + + /** + * Get a dynamic object stub. + * + * @param client the client + * @param context the context + * @param namespace the namespace + * @param name the name + * @return the stub if the object exists + * @throws ApiException the api exception + */ + @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", + "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" }) + public static K8sDynamicStub get(K8sClient client, + APIResource context, String namespace, String name) + throws ApiException { + return K8sGenericStub.get(K8sDynamicModel.class, K8sDynamicModels.class, + client, context, namespace, name, K8sDynamicStub::new); + } + + /** + * Creates a stub from yaml. + * + * @param client the client + * @param context the context + * @param yaml the yaml + * @return the k 8 s dynamic stub + * @throws ApiException the api exception + */ + public static K8sDynamicStub createFromYaml(K8sClient client, + APIResource context, Reader yaml) throws ApiException { + var model = new K8sDynamicModel(client.getJSON().getGson(), + K8s.yamlToJson(client, yaml)); + return K8sGenericStub.create(K8sDynamicModel.class, + K8sDynamicModels.class, client, context, model, + K8sDynamicStub::new); + } +} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java new file mode 100644 index 0000000..30c6699 --- /dev/null +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sGenericStub.java @@ -0,0 +1,418 @@ +/* + * VM-Operator + * Copyright (C) 2024 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.common; + +import com.google.gson.Gson; +import io.kubernetes.client.Discovery.APIResource; +import io.kubernetes.client.apimachinery.GroupVersionKind; +import io.kubernetes.client.common.KubernetesListObject; +import io.kubernetes.client.common.KubernetesObject; +import io.kubernetes.client.custom.V1Patch; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.util.Strings; +import io.kubernetes.client.util.generic.GenericKubernetesApi; +import io.kubernetes.client.util.generic.options.ListOptions; +import io.kubernetes.client.util.generic.options.PatchOptions; +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; +import java.util.function.Function; + +/** + * A stub for namespaced custom objects. This stub provides the + * functions common to all Kubernetes objects, but uses variables + * for all types. This class should be used as base class only. + * + * @param the generic type + * @param the generic type + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class K8sGenericStub { + protected final K8sClient client; + private final GenericKubernetesApi api; + protected final String group; + protected final String version; + protected final String kind; + protected final String plural; + protected final String namespace; + protected final String name; + + /** + * Get a namespaced object stub. If the version in parameter + * `gvk` is an empty string, the stub refers to the first object + * found with matching group and kind. + * + * @param the object type + * @param the object list type + * @param the stub type + * @param objectClass the object class + * @param objectListClass the object list class + * @param client the client + * @param gvk the group, version and kind + * @param namespace the namespace + * @param name the name + * @param provider the provider + * @return the stub if the object exists + * @throws ApiException the api exception + */ + @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", + "PMD.AvoidInstantiatingObjectsInLoops" }) + public static > + R get(Class objectClass, Class objectListClass, + K8sClient client, GroupVersionKind gvk, String namespace, + String name, GenericSupplier provider) + throws ApiException { + var context = K8s.context(client, gvk.getGroup(), gvk.getVersion(), + gvk.getKind()); + if (context.isEmpty()) { + throw new ApiException("No known API for " + gvk.getGroup() + + "/" + gvk.getVersion() + " " + gvk.getKind()); + } + return provider.get(objectClass, objectListClass, client, context.get(), + namespace, name); + } + + /** + * Get a namespaced object stub. + * + * @param the object type + * @param the object list type + * @param the stub type + * @param objectClass the object class + * @param objectListClass the object list class + * @param client the client + * @param context the context + * @param namespace the namespace + * @param name the name + * @param provider the provider + * @return the stub if the object exists + * @throws ApiException the api exception + */ + @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", + "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" }) + public static > + R get(Class objectClass, Class objectListClass, + K8sClient client, APIResource context, String namespace, + String name, GenericSupplier provider) + throws ApiException { + return provider.get(objectClass, objectListClass, client, + context, namespace, name); + } + + /** + * Get a namespaced object stub for a newly created object. + * + * @param the object type + * @param the object list type + * @param the stub type + * @param objectClass the object class + * @param objectListClass the object list class + * @param client the client + * @param context the context + * @param model the model + * @param provider the provider + * @return the stub if the object exists + * @throws ApiException the api exception + */ + @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", + "PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" }) + public static > + R create(Class objectClass, Class objectListClass, + K8sClient client, APIResource context, O model, + GenericSupplier provider) throws ApiException { + var api = new GenericKubernetesApi<>(objectClass, objectListClass, + context.getGroup(), context.getPreferredVersion(), + context.getResourcePlural(), client); + api.create(model).throwsApiException(); + return provider.get(objectClass, objectListClass, client, + context, model.getMetadata().getNamespace(), + model.getMetadata().getName()); + } + + /** + * Get the stubs for the objects in the given namespace that match + * the criteria from the given options. + * + * @param the object type + * @param the object list type + * @param the stub type + * @param objectClass the object class + * @param objectListClass the object list class + * @param client the client + * @param context the context + * @param namespace the namespace + * @param options the options + * @param provider the provider + * @return the collection + * @throws ApiException the api exception + */ + public static > + Collection list(Class objectClass, Class objectListClass, + K8sClient client, APIResource context, String namespace, + ListOptions options, SpecificSupplier provider) + throws ApiException { + var api = new GenericKubernetesApi<>(objectClass, objectListClass, + context.getGroup(), context.getPreferredVersion(), + context.getResourcePlural(), client); + var objs = api.list(namespace, options).throwsApiException(); + var result = new ArrayList(); + for (var item : objs.getObject().getItems()) { + result.add( + provider.get(client, namespace, item.getMetadata().getName())); + } + return result; + } + + /** + * Instantiates a new namespaced custom object stub. + * + * @param objectClass the object class + * @param objectListClass the object list class + * @param client the client + * @param context the context + * @param namespace the namespace + * @param name the name + */ + protected K8sGenericStub(Class objectClass, Class objectListClass, + K8sClient client, APIResource context, String namespace, + String name) { + this.client = client; + group = context.getGroup(); + version = context.getPreferredVersion(); + kind = context.getKind(); + plural = context.getResourcePlural(); + this.namespace = namespace; + this.name = name; + + Gson gson = client.getJSON().getGson(); + if (!checkAdapters(client)) { + client.getJSON().setGson(gson.newBuilder() + .registerTypeAdapterFactory( + new K8sDynamicModelTypeAdapterFactory()) + .create()); + } + api = new GenericKubernetesApi<>(objectClass, + objectListClass, group, version, plural, client); + } + + private boolean checkAdapters(ApiClient client) { + return K8sDynamicModelTypeAdapterFactory.K8sDynamicModelCreator.class + .equals(client.getJSON().getGson().getAdapter(K8sDynamicModel.class) + .getClass()) + && K8sDynamicModelTypeAdapterFactory.K8sDynamicModelsCreator.class + .equals(client.getJSON().getGson() + .getAdapter(K8sDynamicModels.class).getClass()); + } + + /** + * Gets the group. + * + * @return the group + */ + public String group() { + return group; + } + + /** + * Gets the version. + * + * @return the version + */ + public String version() { + return version; + } + + /** + * Gets the kind. + * + * @return the kind + */ + public String kind() { + return kind; + } + + /** + * Gets the plural. + * + * @return the plural + */ + public String plural() { + return plural; + } + + /** + * Gets the namespace. + * + * @return the namespace + */ + public String namespace() { + return namespace; + } + + /** + * Gets the name. + * + * @return the name + */ + public String name() { + return name; + } + + /** + * Delete the Kubernetes object. + * + * @throws ApiException the API exception + */ + public void delete() throws ApiException { + var result = api.delete(namespace, name); + if (result.isSuccess() + || result.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) { + return; + } + result.throwsApiException(); + } + + /** + * Retrieves and returns the current state of the object. + * + * @return the object's state + * @throws ApiException the api exception + */ + public Optional model() throws ApiException { + return K8s.optional(api.get(namespace, name)); + } + + /** + * Updates the object's status. + * + * @param object the current state of the object (passed to `status`) + * @param status function that returns the new status + * @return the updated model or empty if not successful + * @throws ApiException the api exception + */ + public Optional updateStatus(O object, + Function status) throws ApiException { + return K8s.optional(api.updateStatus(object, status)); + } + + /** + * Updates the status. + * + * @param status the status + * @return the kubernetes api response + * the updated model or empty if not successful + * @throws ApiException the api exception + */ + public Optional updateStatus(Function status) + throws ApiException { + return updateStatus( + api.get(namespace, name).throwsApiException().getObject(), status); + } + + /** + * Patch the object. + * + * @param patchType the patch type + * @param patch the patch + * @param options the options + * @return the kubernetes api response + * @throws ApiException the api exception + */ + public Optional patch(String patchType, V1Patch patch, + PatchOptions options) throws ApiException { + return K8s + .optional(api.patch(namespace, name, patchType, patch, options)); + } + + /** + * Patch the object using default options. + * + * @param patchType the patch type + * @param patch the patch + * @return the kubernetes api response + * @throws ApiException the api exception + */ + public Optional + patch(String patchType, V1Patch patch) throws ApiException { + PatchOptions opts = new PatchOptions(); + return patch(patchType, patch, opts); + } + + /** + * A supplier for generic stubs. + * + * @param the object type + * @param the object list type + * @param the result type + */ + public interface GenericSupplier> { + + /** + * Gets a new stub. + * + * @param objectClass the object class + * @param objectListClass the object list class + * @param client the client + * @param context the API resource + * @param namespace the namespace + * @param name the name + * @return the result + */ + @SuppressWarnings("PMD.UseObjectForClearerAPI") + R get(Class objectClass, Class objectListClass, K8sClient client, + APIResource context, String namespace, String name); + } + + /** + * A supplier for specific stubs. + * + * @param the object type + * @param the object list type + * @param the result type + */ + public interface SpecificSupplier> { + + /** + * Gets a new stub. + * + * @param client the client + * @param namespace the namespace + * @param name the name + * @return the result + */ + R get(K8sClient client, String namespace, String name); + } + + @Override + @SuppressWarnings("PMD.UseLocaleWithCaseConversions") + public String toString() { + return (Strings.isNullOrEmpty(group) ? "" : group + "/") + + version.toUpperCase() + kind + " " + namespace + ":" + name; + } + +} diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ConfigMapStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ConfigMapStub.java new file mode 100644 index 0000000..58a9516 --- /dev/null +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ConfigMapStub.java @@ -0,0 +1,60 @@ +/* + * VM-Operator + * Copyright (C) 2024 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.common; + +import io.kubernetes.client.Discovery.APIResource; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1ConfigMapList; +import java.util.List; + +/** + * A stub for config maps (v1). + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class K8sV1ConfigMapStub + extends K8sGenericStub { + + /** + * Instantiates a new stub. + * + * @param client the client + * @param namespace the namespace + * @param name the name + */ + protected K8sV1ConfigMapStub(K8sClient client, String namespace, + String name) { + super(V1ConfigMap.class, V1ConfigMapList.class, client, + new APIResource("", List.of("v1"), "v1", "ConfigMap", true, + "configmaps", "configmap"), + namespace, name); + } + + /** + * Gets the stub for the given namespace and name. + * + * @param client the client + * @param namespace the namespace + * @param name the name + * @return the config map stub + */ + public static K8sV1ConfigMapStub get(K8sClient client, String namespace, + String name) { + return new K8sV1ConfigMapStub(client, namespace, name); + } +} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1DeploymentStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1DeploymentStub.java new file mode 100644 index 0000000..049363d --- /dev/null +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1DeploymentStub.java @@ -0,0 +1,77 @@ +/* + * VM-Operator + * Copyright (C) 2024 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.common; + +import io.kubernetes.client.Discovery.APIResource; +import io.kubernetes.client.custom.V1Patch; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.models.V1Deployment; +import io.kubernetes.client.openapi.models.V1DeploymentList; +import java.util.List; +import java.util.Optional; + +/** + * A stub for pods (v1). + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class K8sV1DeploymentStub + extends K8sGenericStub { + + /** + * Instantiates a new stub. + * + * @param client the client + * @param namespace the namespace + * @param name the name + */ + protected K8sV1DeploymentStub(K8sClient client, String namespace, + String name) { + super(V1Deployment.class, V1DeploymentList.class, client, + new APIResource("apps", List.of("v1"), "v1", "Pod", true, + "deployments", "deployment"), + namespace, name); + } + + /** + * Gets the stub for the given namespace and name. + * + * @param client the client + * @param namespace the namespace + * @param name the name + * @return the deployment stub + */ + public static K8sV1DeploymentStub get(K8sClient client, String namespace, + String name) { + return new K8sV1DeploymentStub(client, namespace, name); + } + + /** + * Scales the deployment. + * + * @param replicas the replicas + * @return the new model or empty if not successful + * @throws ApiException the API exception + */ + public Optional scale(int replicas) throws ApiException { + return patch(V1Patch.PATCH_FORMAT_JSON_PATCH, + new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/replicas" + + "\", \"value\": " + replicas + "}]"), + client.defaultPatchOptions()); + } +} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java new file mode 100644 index 0000000..fe47a0f --- /dev/null +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java @@ -0,0 +1,78 @@ +/* + * VM-Operator + * Copyright (C) 2024 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.common; + +import io.kubernetes.client.Discovery.APIResource; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.models.V1Pod; +import io.kubernetes.client.openapi.models.V1PodList; +import io.kubernetes.client.util.generic.options.ListOptions; +import java.util.Collection; +import java.util.List; + +/** + * A stub for pods (v1). + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class K8sV1PodStub extends K8sGenericStub { + + public static final APIResource CONTEXT + = new APIResource("", List.of("v1"), "v1", "Pod", true, "pods", "pod"); + + /** + * Instantiates a new stub. + * + * @param client the client + * @param namespace the namespace + * @param name the name + */ + protected K8sV1PodStub(K8sClient client, String namespace, String name) { + super(V1Pod.class, V1PodList.class, client, CONTEXT, namespace, name); + } + + /** + * Gets the stub for the given namespace and name. + * + * @param client the client + * @param namespace the namespace + * @param name the name + * @return the kpod stub + */ + public static K8sV1PodStub get(K8sClient client, String namespace, + String name) { + return new K8sV1PodStub(client, namespace, name); + } + + /** + * Get the stubs for the objects in the given namespace that match + * the criteria from the given options. + * + * @param client the client + * @param namespace the namespace + * @param options the options + * @return the collection + * @throws ApiException the api exception + */ + public static Collection list(K8sClient client, + String namespace, ListOptions options) throws ApiException { + return K8sGenericStub.list(V1Pod.class, V1PodList.class, client, + CONTEXT, namespace, options, K8sV1PodStub::new); + } + +} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1StatefulSetStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1StatefulSetStub.java new file mode 100644 index 0000000..13462b9 --- /dev/null +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1StatefulSetStub.java @@ -0,0 +1,60 @@ +/* + * VM-Operator + * Copyright (C) 2024 Michael N. Lipp + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jdrupes.vmoperator.common; + +import io.kubernetes.client.Discovery.APIResource; +import io.kubernetes.client.openapi.models.V1StatefulSet; +import io.kubernetes.client.openapi.models.V1StatefulSetList; +import java.util.List; + +/** + * A stub for stateful sets (v1). + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class K8sV1StatefulSetStub + extends K8sGenericStub { + + /** + * Instantiates a new stub. + * + * @param client the client + * @param namespace the namespace + * @param name the name + */ + protected K8sV1StatefulSetStub(K8sClient client, String namespace, + String name) { + super(V1StatefulSet.class, V1StatefulSetList.class, client, + new APIResource("apps", List.of("v1"), "v1", "StatefulSet", true, + "statefulsets", "statefulset"), + namespace, name); + } + + /** + * Gets the stub for the given namespace and name. + * + * @param client the client + * @param namespace the namespace + * @param name the name + * @return the stateful set stub + */ + public static K8sV1StatefulSetStub get(K8sClient client, String namespace, + String name) { + return new K8sV1StatefulSetStub(client, namespace, name); + } +} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java index bc06e68..972693a 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmChannel.java @@ -18,8 +18,8 @@ package org.jdrupes.vmoperator.manager.events; -import io.kubernetes.client.openapi.ApiClient; -import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; +import org.jdrupes.vmoperator.common.K8sClient; +import org.jdrupes.vmoperator.common.K8sDynamicModel; import org.jgrapes.core.Channel; import org.jgrapes.core.EventPipeline; import org.jgrapes.core.Subchannel.DefaultSubchannel; @@ -31,8 +31,8 @@ import org.jgrapes.core.Subchannel.DefaultSubchannel; public class VmChannel extends DefaultSubchannel { private final EventPipeline pipeline; - private final ApiClient client; - private DynamicKubernetesObject vmDefinition; + private final K8sClient client; + private K8sDynamicModel vmDefinition; private long generation = -1; /** @@ -43,7 +43,7 @@ public class VmChannel extends DefaultSubchannel { * @param client the client */ public VmChannel(Channel mainChannel, EventPipeline pipeline, - ApiClient client) { + K8sClient client) { super(mainChannel); this.pipeline = pipeline; this.client = client; @@ -56,7 +56,7 @@ public class VmChannel extends DefaultSubchannel { * @return the watch channel */ @SuppressWarnings("PMD.LinguisticNaming") - public VmChannel setVmDefinition(DynamicKubernetesObject definition) { + public VmChannel setVmDefinition(K8sDynamicModel definition) { this.vmDefinition = definition; return this; } @@ -66,7 +66,7 @@ public class VmChannel extends DefaultSubchannel { * * @return the json object */ - public DynamicKubernetesObject vmDefinition() { + public K8sDynamicModel vmDefinition() { return vmDefinition; } @@ -109,7 +109,7 @@ public class VmChannel extends DefaultSubchannel { * * @return the API client */ - public ApiClient client() { + public K8sClient client() { return client; } } diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmDefChanged.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmDefChanged.java index fd5d43c..e9c9ca1 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmDefChanged.java +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/VmDefChanged.java @@ -19,7 +19,7 @@ package org.jdrupes.vmoperator.manager.events; import io.kubernetes.client.openapi.models.V1APIResource; -import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; +import org.jdrupes.vmoperator.common.K8sDynamicModel; import org.jgrapes.core.Channel; import org.jgrapes.core.Components; import org.jgrapes.core.Event; @@ -44,7 +44,7 @@ public class VmDefChanged extends Event { private final Type type; private final boolean specChanged; private final V1APIResource crd; - private final DynamicKubernetesObject vmDef; + private final K8sDynamicModel vmDef; /** * Instantiates a new VM changed event. @@ -55,7 +55,7 @@ public class VmDefChanged extends Event { * @param vmDefinition the VM definition */ public VmDefChanged(Type type, boolean specChanged, V1APIResource crd, - DynamicKubernetesObject vmDefinition) { + K8sDynamicModel vmDefinition) { this.type = type; this.specChanged = specChanged; this.crd = crd; @@ -92,7 +92,7 @@ public class VmDefChanged extends Event { * * @return the object. */ - public DynamicKubernetesObject vmDefinition() { + public K8sDynamicModel vmDefinition() { return vmDef; } diff --git a/org.jdrupes.vmoperator.manager/.settings/net.sf.jautodoc.prefs b/org.jdrupes.vmoperator.manager/.settings/net.sf.jautodoc.prefs index 6f3b6d4..03e8200 100644 --- a/org.jdrupes.vmoperator.manager/.settings/net.sf.jautodoc.prefs +++ b/org.jdrupes.vmoperator.manager/.settings/net.sf.jautodoc.prefs @@ -1,6 +1,6 @@ add_header=true eclipse.preferences.version=1 -header_text=/*\n * VM-Operator\n * Copyright (C) 2023 Michael N. Lipp\n * \n * This program is free software\: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n */ +header_text=/*\n * VM-Operator\n * Copyright (C) 2024 Michael N. Lipp\n * \n * This program is free software\: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n */ project_specific_settings=true replacements=\n\n\nReturns the\nSets the\nAdds the\nEdits the\nRemoves the\nInits the\nParses the\nCreates the\nBuilds the\nChecks if is\nPrints the\nChecks for\n\n\n visibility_package=false diff --git a/org.jdrupes.vmoperator.manager/build.gradle b/org.jdrupes.vmoperator.manager/build.gradle index d403be8..a4990d5 100644 --- a/org.jdrupes.vmoperator.manager/build.gradle +++ b/org.jdrupes.vmoperator.manager/build.gradle @@ -33,8 +33,6 @@ dependencies { runtimeOnly 'org.apache.logging.log4j:log4j-to-jul:2.20.0' runtimeOnly project(':org.jdrupes.vmoperator.vmconlet') - - testImplementation 'io.fabric8:kubernetes-client:[6.8.1,6.9)' } application { diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java index 589affc..ee693c2 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Controller.java @@ -18,20 +18,21 @@ package org.jdrupes.vmoperator.manager; +import io.kubernetes.client.apimachinery.GroupVersionKind; import io.kubernetes.client.custom.V1Patch; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.Configuration; -import io.kubernetes.client.util.Config; -import io.kubernetes.client.util.generic.options.PatchOptions; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.logging.Level; import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; -import org.jdrupes.vmoperator.common.K8s; +import org.jdrupes.vmoperator.common.K8sClient; +import org.jdrupes.vmoperator.common.K8sDynamicStub; import org.jdrupes.vmoperator.manager.events.Exit; import org.jdrupes.vmoperator.manager.events.ModifyVm; +import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jgrapes.core.Channel; import org.jgrapes.core.Component; @@ -160,35 +161,30 @@ public class Controller extends Component { * @throws IOException Signals that an I/O exception has occurred. */ @Handler - public void onModigyVm(ModifyVm event) throws ApiException, IOException { - patchVmSpec(event.name(), event.path(), event.value()); + public void onModifyVm(ModifyVm event, VmChannel channel) + throws ApiException, IOException { + patchVmSpec(channel.client(), event.name(), event.path(), + event.value()); } - private void patchVmSpec(String name, String path, Object value) + private void patchVmSpec(K8sClient client, String name, String path, + Object value) throws ApiException, IOException { - var crApi = K8s.crApi(Config.defaultClient(), VM_OP_GROUP, - VM_OP_KIND_VM, namespace, name); - if (crApi.isEmpty()) { - logger.warning(() -> "Trying to patch " + namespace + "/" + name - + " which does not exist."); - return; - } + var vmStub = K8sDynamicStub.get(client, + new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), namespace, + name); // Patch running - PatchOptions patchOpts = new PatchOptions(); - patchOpts.setFieldManager("kubernetes-java-kubectl-apply"); String valueAsText = value instanceof String ? "\"" + value + "\"" : value.toString(); - var res = crApi.get().patch(namespace, name, - V1Patch.PATCH_FORMAT_JSON_PATCH, + var res = vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH, new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/" + path + "\", \"value\": " + valueAsText + "}]"), - patchOpts); - if (!res.isSuccess()) { + client.defaultPatchOptions()); + if (!res.isPresent()) { logger.warning( - () -> "Cannot patch pod annotations: " + res.getStatus()); + () -> "Cannot patch pod annotations for " + vmStub.name()); } - } } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java index efa95f4..85158c7 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/LoadBalancerReconciler.java @@ -29,11 +29,11 @@ import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import io.kubernetes.client.util.generic.dynamic.Dynamics; import java.io.IOException; import java.io.StringWriter; -import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.logging.Logger; import org.jdrupes.vmoperator.common.K8s; +import org.jdrupes.vmoperator.common.K8sDynamicModel; import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.util.GsonPtr; @@ -79,19 +79,25 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; Map model, VmChannel channel) throws IOException, TemplateException, ApiException { // Check if to be generated - @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" }) - var lbs = Optional.of(model) + @SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "unchecked" }) + var lbsDef = Optional.of(model) .map(m -> (Map) m.get("reconciler")) .map(c -> c.get(LOAD_BALANCER_SERVICE)).orElse(Boolean.FALSE); - if (lbs instanceof Boolean isOn && !isOn) { - return; - } - if (!(lbs instanceof Map)) { + if (!(lbsDef instanceof Map) && !(lbsDef instanceof Boolean)) { logger.warning(() -> "\"" + LOAD_BALANCER_SERVICE + "\" in configuration must be boolean or mapping but is " - + lbs.getClass() + "."); + + lbsDef.getClass() + "."); return; } + if (lbsDef instanceof Boolean isOn && !isOn) { + return; + } + JsonObject cfgMeta = new JsonObject(); + if (lbsDef instanceof Map) { + var json = channel.client().getJSON(); + cfgMeta + = json.deserialize(json.serialize(lbsDef), JsonObject.class); + } // Combine template and data and parse result var fmTemplate = fmConfig.getTemplate("runnerLoadBalancer.ftl.yaml"); @@ -101,7 +107,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; // https://github.com/kubernetes-client/java/issues/2741 var svcDef = Dynamics.newFromYaml( new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); - mergeMetadata(svcDef, lbs, channel); + mergeMetadata(svcDef, cfgMeta, event.vmDefinition()); // Apply DynamicKubernetesApi svcApi = new DynamicKubernetesApi("", "v1", @@ -109,20 +115,10 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; K8s.apply(svcApi, svcDef, svcDef.getRaw().toString()); } - @SuppressWarnings("unchecked") private void mergeMetadata(DynamicKubernetesObject svcDef, - Object lbsConfig, VmChannel channel) { - // Get metadata from config - Map asmData = Collections.emptyMap(); - if (lbsConfig instanceof Map config) { - asmData = (Map) config; - } - var json = channel.client().getJSON(); - JsonObject cfgMeta - = json.deserialize(json.serialize(asmData), JsonObject.class); - + JsonObject cfgMeta, K8sDynamicModel vmDefinition) { // Get metadata from VM definition - var vmMeta = GsonPtr.to(channel.vmDefinition().getRaw()).to("spec") + var vmMeta = GsonPtr.to(vmDefinition.data()).to("spec") .get(JsonObject.class, LOAD_BALANCER_SERVICE) .map(JsonObject::deepCopy).orElseGet(() -> new JsonObject()); diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java index 2adb843..0683c76 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Reconciler.java @@ -44,6 +44,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import org.jdrupes.vmoperator.common.Convertions; +import org.jdrupes.vmoperator.common.K8sDynamicModel; import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type; @@ -206,8 +207,8 @@ public class Reconciler extends Component { lbReconciler.reconcile(event, model, channel); } - private DynamicKubernetesObject patchCr(DynamicKubernetesObject vmDef) { - var json = vmDef.getRaw().deepCopy(); + private DynamicKubernetesObject patchCr(K8sDynamicModel vmDef) { + var json = vmDef.data().deepCopy(); // Adjust cdromImage path adjustCdRomPaths(json); diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefulSetReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefulSetReconciler.java index 3cd6a39..8812a93 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefulSetReconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/StatefulSetReconciler.java @@ -22,14 +22,13 @@ import freemarker.template.Configuration; import freemarker.template.TemplateException; import io.kubernetes.client.custom.V1Patch; import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; import io.kubernetes.client.util.generic.dynamic.Dynamics; import io.kubernetes.client.util.generic.options.PatchOptions; import java.io.IOException; import java.io.StringWriter; import java.util.Map; import java.util.logging.Logger; -import org.jdrupes.vmoperator.common.K8s; +import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub; import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.util.GsonPtr; @@ -68,8 +67,6 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; public void reconcile(VmDefChanged event, Map model, VmChannel channel) throws IOException, TemplateException, ApiException { - DynamicKubernetesApi stsApi = new DynamicKubernetesApi("apps", "v1", - "statefulsets", channel.client()); var metadata = event.vmDefinition().getMetadata(); // Combine template and data and parse result @@ -83,25 +80,27 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; // If exists apply changes only when transitioning state // or not running. - var existing = K8s.get(stsApi, metadata); - if (existing.isPresent()) { - var current = GsonPtr.to(existing.get().getRaw()) - .to("spec").getAsInt("replicas").orElse(1); + var stsStub = K8sV1StatefulSetStub.get(channel.client(), + metadata.getNamespace(), metadata.getName()); + stsStub.model().ifPresent(sts -> { + var current = sts.getSpec().getReplicas(); var desired = GsonPtr.to(stsDef.getRaw()) .to("spec").getAsInt("replicas").orElse(1); if (current == 1 && desired == 1) { return; } - } + }); // Do apply changes PatchOptions opts = new PatchOptions(); opts.setForce(true); opts.setFieldManager("kubernetes-java-kubectl-apply"); - stsApi.patch(stsDef.getMetadata().getNamespace(), - stsDef.getMetadata().getName(), V1Patch.PATCH_FORMAT_APPLY_YAML, - new V1Patch(channel.client().getJSON().serialize(stsDef)), - opts).throwsApiException(); + if (stsStub.patch(V1Patch.PATCH_FORMAT_APPLY_YAML, + new V1Patch(channel.client().getJSON().serialize(stsDef)), opts) + .isEmpty()) { + logger.warning( + () -> "Could not patch stateful set for " + stsStub.name()); + } } } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java index 2d99727..c074ac2 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmWatcher.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2023 Michael N. Lipp + * Copyright (C) 2023,2024 Michael N. Lipp * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -21,6 +21,8 @@ package org.jdrupes.vmoperator.manager; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; +import io.kubernetes.client.apimachinery.GroupVersion; +import io.kubernetes.client.apimachinery.GroupVersionKind; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.apis.ApisApi; @@ -33,7 +35,6 @@ import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.util.Config; import io.kubernetes.client.util.Watch; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; -import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import io.kubernetes.client.util.generic.options.ListOptions; import java.io.IOException; import java.nio.file.Files; @@ -48,7 +49,10 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; -import org.jdrupes.vmoperator.common.K8s; +import org.jdrupes.vmoperator.common.K8sClient; +import org.jdrupes.vmoperator.common.K8sDynamicModel; +import org.jdrupes.vmoperator.common.K8sDynamicStub; +import org.jdrupes.vmoperator.common.K8sV1PodStub; import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; @@ -68,7 +72,7 @@ import org.jgrapes.util.events.ConfigurationUpdate; /** * Watches for changes of VM definitions. */ -@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports" }) public class VmWatcher extends Component { private String namespaceToWatch; @@ -269,13 +273,13 @@ public class VmWatcher extends Component { } private void handleVmDefinitionChange(V1APIResource vmsCrd, - Watch.Response vmDefStub) { - V1ObjectMeta metadata = vmDefStub.object.getMetadata(); + Watch.Response vmDefRef) throws ApiException { + V1ObjectMeta metadata = vmDefRef.object.getMetadata(); VmChannel channel = channels.computeIfAbsent(metadata.getName(), k -> { try { return new VmChannel(channel(), newEventPipeline(), - Config.defaultClient()); + new K8sClient()); } catch (IOException e) { logger.log(Level.SEVERE, e, () -> "Failed to create client" + " for handling changes: " + e.getMessage()); @@ -287,30 +291,27 @@ public class VmWatcher extends Component { } // Get full definition and associate with channel as backup - var apiVersion = K8s.version(vmDefStub.object.getApiVersion()); - DynamicKubernetesApi vmCrApi = new DynamicKubernetesApi(VM_OP_GROUP, - apiVersion, vmsCrd.getName(), channel.client()); - var curVmDef = K8s.get(vmCrApi, metadata); - curVmDef.ifPresent(def -> { - // Augment with "dynamic" data and associate with channel - addDynamicData(channel.client(), def); - channel.setVmDefinition(def); + @SuppressWarnings("PMD.ShortVariable") + var gv = GroupVersion.parse(vmDefRef.object.getApiVersion()); + var vmStub = K8sDynamicStub.get(channel.client(), + new GroupVersionKind(gv.getGroup(), gv.getVersion(), VM_OP_KIND_VM), + metadata.getNamespace(), metadata.getName()); + vmStub.model().ifPresent(vmDef -> { + addDynamicData(channel.client(), vmDef); + channel.setVmDefinition(vmDef); + + // Create and fire event + channel.pipeline().fire(new VmDefChanged(VmDefChanged.Type + .valueOf(vmDefRef.type), + channel + .setGeneration( + vmDefRef.object.getMetadata().getGeneration()), + vmsCrd, vmDef), channel); }); - - // Get eventual definition to use - var vmDef = curVmDef.orElse(channel.vmDefinition()); - - // Create and fire event - channel.pipeline().fire(new VmDefChanged(VmDefChanged.Type - .valueOf(vmDefStub.type), - channel - .setGeneration(vmDefStub.object.getMetadata().getGeneration()), - vmsCrd, vmDef), channel); } - private void addDynamicData(ApiClient client, - DynamicKubernetesObject vmDef) { - var rootNode = GsonPtr.to(vmDef.getRaw()).get(JsonObject.class); + private void addDynamicData(K8sClient client, K8sDynamicModel vmState) { + var rootNode = GsonPtr.to(vmState.data()).get(JsonObject.class); rootNode.addProperty("nodeName", ""); // VM definition status changes before the pod terminates. @@ -329,11 +330,18 @@ public class VmWatcher extends Component { var podSearch = new ListOptions(); podSearch.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + ",app.kubernetes.io/component=" + APP_NAME - + ",app.kubernetes.io/instance=" + vmDef.getMetadata().getName()); - var podList = K8s.podApi(client).list(namespaceToWatch, podSearch); - podList.getObject().getItems().stream().forEach(pod -> { - rootNode.addProperty("nodeName", pod.getSpec().getNodeName()); - }); + + ",app.kubernetes.io/instance=" + vmState.getMetadata().getName()); + try { + var podList + = K8sV1PodStub.list(client, namespaceToWatch, podSearch); + for (var podStub : podList) { + rootNode.addProperty("nodeName", + podStub.model().get().getSpec().getNodeName()); + } + } catch (ApiException e) { + logger.log(Level.WARNING, e, + () -> "Cannot access node information: " + e.getMessage()); + } } /** diff --git a/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java b/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java index 26eb387..13a93e1 100644 --- a/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java +++ b/org.jdrupes.vmoperator.manager/test/org/jdrupes/vmoperator/manager/BasicTests.java @@ -1,13 +1,18 @@ package org.jdrupes.vmoperator.manager; -import io.fabric8.kubernetes.client.Config; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.KubernetesClientBuilder; -import io.fabric8.kubernetes.client.dsl.base.ResourceDefinitionContext; +import io.kubernetes.client.Discovery.APIResource; +import io.kubernetes.client.openapi.ApiException; +import java.io.FileReader; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Map; +import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; +import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; +import org.jdrupes.vmoperator.common.K8s; +import org.jdrupes.vmoperator.common.K8sClient; +import org.jdrupes.vmoperator.common.K8sDynamicStub; +import org.jdrupes.vmoperator.common.K8sV1ConfigMapStub; +import org.jdrupes.vmoperator.common.K8sV1DeploymentStub; +import org.jdrupes.vmoperator.common.K8sV1StatefulSetStub; import org.junit.jupiter.api.AfterAll; import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.BeforeAll; @@ -18,8 +23,9 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; class BasicTests { - private static KubernetesClient client; - private static ResourceDefinitionContext vmsContext; + private static K8sClient client; + private static APIResource vmsContext; + private static K8sV1DeploymentStub mgrDeployment; @BeforeAll static void setUpBeforeClass() throws Exception { @@ -27,29 +33,27 @@ class BasicTests { assertNotNull(testCluster); // Get client - client = new KubernetesClientBuilder() - .withConfig(Config.autoConfigure(testCluster)).build(); + client = new K8sClient(); // Context for working with our CR - vmsContext = new ResourceDefinitionContext.Builder() - .withGroup("vmoperator.jdrupes.org").withKind("VirtualMachine") - .withPlural("vms").withNamespaced(true).withVersion("v1").build(); + var apiRes = K8s.context(client, VM_OP_GROUP, null, VM_OP_KIND_VM); + assertTrue(apiRes.isPresent()); + vmsContext = apiRes.get(); - // Cleanup - var resourcesInNamespace = client.genericKubernetesResources(vmsContext) - .inNamespace("vmop-dev"); - resourcesInNamespace.withName("unittest-vm").delete(); + // Cleanup existing VM + K8sDynamicStub.get(client, vmsContext, "vmop-dev", "unittest-vm") + .delete(); // Update manager pod by scaling deployment - client.apps().deployments().inNamespace("vmop-dev") - .withName("vm-operator").scale(0); - client.apps().deployments().inNamespace("vmop-dev") - .withName("vm-operator").scale(1); + mgrDeployment + = K8sV1DeploymentStub.get(client, "vmop-dev", "vm-operator"); + mgrDeployment.scale(0); + mgrDeployment.scale(1); // Wait until available + for (int i = 0; i < 10; i++) { - if (client.apps().deployments().inNamespace("vmop-dev") - .withName("vm-operator").get().getStatus().getConditions() + if (mgrDeployment.model().get().getStatus().getConditions() .stream().filter(c -> "Available".equals(c.getType())).findAny() .isPresent()) { return; @@ -62,44 +66,40 @@ class BasicTests { @AfterAll static void tearDownAfterClass() throws Exception { // Bring down manager - client.apps().deployments().inNamespace("vmop-dev") - .withName("vm-operator").scale(0); - client.close(); + mgrDeployment.scale(0); } @Test - void test() throws IOException, InterruptedException { + void test() throws IOException, InterruptedException, ApiException { // Load from Yaml - var vm = client.genericKubernetesResources(vmsContext) - .load(Files - .newInputStream(Path.of("test-resources/unittest-vm.yaml"))); - // Create Custom Resource - vm.create(); + var rdr = new FileReader("test-resources/unittest-vm.yaml"); + var vmStub = K8sDynamicStub.createFromYaml(client, vmsContext, rdr); + assertTrue(vmStub.model().isPresent()); // Wait for created resources - assertTrue(waitForConfigMap()); - assertTrue(waitForStatefulSet()); + assertTrue(waitForConfigMap(client)); + assertTrue(waitForStatefulSet(client)); // Check config map - var config = client.configMaps().inNamespace("vmop-dev") - .withName("unittest-vm").get(); + var config = K8sV1ConfigMapStub.get(client, "vmop-dev", "unittest-vm") + .model().get(); var yaml = new Yaml(new SafeConstructor(new LoaderOptions())) - .load((String) config.getData().get("config.yaml")); + .load(config.getData().get("config.yaml")); @SuppressWarnings("unchecked") - var currentRam = ((Map>>) yaml) + var maximumRam = ((Map>>) yaml) .get("/Runner").get("vm").get("maximumRam"); - assertEquals("4 GiB", currentRam); + assertEquals("4 GiB", maximumRam); // Cleanup - var resourcesInNamespace = client.genericKubernetesResources(vmsContext) - .inNamespace("vmop-dev"); - resourcesInNamespace.withName("unittest-vm").delete(); + K8sDynamicStub.get(client, vmsContext, "vmop-dev", "unittest-vm") + .delete(); } - private boolean waitForConfigMap() throws InterruptedException { + private boolean waitForConfigMap(K8sClient client) + throws InterruptedException, ApiException { + var stub = K8sV1ConfigMapStub.get(client, "vmop-dev", "unittest-vm"); for (int i = 0; i < 10; i++) { - if (client.configMaps().inNamespace("vmop-dev") - .withName("unittest-vm").get() != null) { + if (stub.model().isPresent()) { return true; } Thread.sleep(1000); @@ -107,10 +107,11 @@ class BasicTests { return false; } - private boolean waitForStatefulSet() throws InterruptedException { + private boolean waitForStatefulSet(K8sClient client) + throws InterruptedException, ApiException { + var stub = K8sV1StatefulSetStub.get(client, "vmop-dev", "unittest-vm"); for (int i = 0; i < 10; i++) { - if (client.apps().statefulSets().inNamespace("vmop-dev") - .withName("unittest-vm").get() != null) { + if (stub.model().isPresent()) { return true; } Thread.sleep(1000); diff --git a/org.jdrupes.vmoperator.runner.qemu/build.gradle b/org.jdrupes.vmoperator.runner.qemu/build.gradle index 7179b8f..82525c6 100644 --- a/org.jdrupes.vmoperator.runner.qemu/build.gradle +++ b/org.jdrupes.vmoperator.runner.qemu/build.gradle @@ -16,7 +16,7 @@ dependencies { implementation project(':org.jdrupes.vmoperator.common') implementation 'commons-cli:commons-cli:1.5.0' - implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:[2.15.1,3]' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:[2.16.1]' runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)' } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java index 1cb5e74..d55e027 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/StatusUpdater.java @@ -1,6 +1,6 @@ /* * VM-Operator - * Copyright (C) 2023 Michael N. Lipp + * Copyright (C) 2023,2024 Michael N. Lipp * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -19,27 +19,17 @@ package org.jdrupes.vmoperator.runner.qemu; import com.google.gson.JsonObject; +import io.kubernetes.client.apimachinery.GroupVersionKind; import io.kubernetes.client.custom.Quantity; import io.kubernetes.client.custom.Quantity.Format; import io.kubernetes.client.custom.V1Patch; import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.apis.ApisApi; -import io.kubernetes.client.openapi.apis.CustomObjectsApi; -import io.kubernetes.client.openapi.apis.EventsV1Api; import io.kubernetes.client.openapi.models.EventsV1Event; -import io.kubernetes.client.openapi.models.V1APIGroup; -import io.kubernetes.client.openapi.models.V1GroupVersionForDiscovery; -import io.kubernetes.client.openapi.models.V1ObjectMeta; -import io.kubernetes.client.util.Config; -import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; -import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; -import io.kubernetes.client.util.generic.options.PatchOptions; import java.io.IOException; import java.math.BigDecimal; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; -import java.time.OffsetDateTime; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -48,6 +38,9 @@ import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP; import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM; import org.jdrupes.vmoperator.common.K8s; +import org.jdrupes.vmoperator.common.K8sClient; +import org.jdrupes.vmoperator.common.K8sDynamicModel; +import org.jdrupes.vmoperator.common.K8sDynamicStub; import org.jdrupes.vmoperator.runner.qemu.events.BalloonChangeEvent; import org.jdrupes.vmoperator.runner.qemu.events.Exit; import org.jdrupes.vmoperator.runner.qemu.events.HotpluggableCpuStatus; @@ -75,11 +68,11 @@ public class StatusUpdater extends Component { private String namespace; private String vmName; - private DynamicKubernetesApi vmCrApi; - private EventsV1Api evtsApi; + private K8sClient apiClient; private long observedGeneration; private boolean guestShutdownStops; private boolean shutdownByGuest; + private K8sDynamicStub vmStub; /** * Instantiates a new status updater. @@ -88,6 +81,16 @@ public class StatusUpdater extends Component { */ public StatusUpdater(Channel componentChannel) { super(componentChannel); + try { + apiClient = new K8sClient(); + io.kubernetes.client.openapi.Configuration + .setDefaultApiClient(apiClient); + } catch (IOException e) { + logger.log(Level.SEVERE, e, + () -> "Cannot access events API, terminating."); + fire(new Exit(1)); + } + } /** @@ -154,59 +157,18 @@ public class StatusUpdater extends Component { return; } try { - initVmCrApi(event); - } catch (IOException | ApiException e) { + vmStub = K8sDynamicStub.get(apiClient, + new GroupVersionKind(VM_OP_GROUP, "", VM_OP_KIND_VM), + namespace, vmName); + vmStub.model().ifPresent(model -> { + observedGeneration = model.getMetadata().getGeneration(); + }); + } catch (ApiException e) { logger.log(Level.SEVERE, e, - () -> "Cannot access VM's CR, terminating."); + () -> "Cannot access VM object, terminating."); event.cancel(true); fire(new Exit(1)); } - try { - evtsApi = new EventsV1Api(Config.defaultClient()); - } catch (IOException e) { - logger.log(Level.SEVERE, e, - () -> "Cannot access events API, terminating."); - event.cancel(true); - fire(new Exit(1)); - } - } - - private void initVmCrApi(Start event) throws IOException, ApiException { - var client = Config.defaultClient(); - var apis = new ApisApi(client).getAPIVersions(); - var crdVersions = apis.getGroups().stream() - .filter(g -> g.getName().equals(VM_OP_GROUP)).findFirst() - .map(V1APIGroup::getVersions).stream().flatMap(l -> l.stream()) - .map(V1GroupVersionForDiscovery::getVersion).toList(); - var coa = new CustomObjectsApi(client); - for (var crdVersion : crdVersions) { - var crdApiRes = coa.getAPIResources(VM_OP_GROUP, - crdVersion).getResources().stream() - .filter(r -> VM_OP_KIND_VM.equals(r.getKind())).findFirst(); - if (crdApiRes.isEmpty()) { - continue; - } - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") - var crApi = new DynamicKubernetesApi(VM_OP_GROUP, - crdVersion, crdApiRes.get().getName(), client); - var vmCr = crApi.get(namespace, vmName); - if (vmCr.isSuccess()) { - vmCrApi = crApi; - observedGeneration - = vmCr.getObject().getMetadata().getGeneration(); - break; - } - } - if (vmCrApi == null) { - logger.severe(() -> "Cannot find VM's CR, terminating."); - event.cancel(true); - fire(new Exit(1)); - } - } - - @SuppressWarnings("PMD.AvoidDuplicateLiterals") - private JsonObject currentStatus(DynamicKubernetesObject vmCr) { - return vmCr.getRaw().getAsJsonObject("status").deepCopy(); } /** @@ -221,18 +183,19 @@ public class StatusUpdater extends Component { guestShutdownStops = event.configuration().guestShutdownStops; // Remainder applies only if we have a connection to k8s. - if (vmCrApi == null) { + if (vmStub == null) { return; } + // A change of the runner configuration is typically caused // by a new version of the CR. So we observe the new CR. - var vmCr = vmCrApi.get(namespace, vmName).throwsApiException() - .getObject(); - if (vmCr.getMetadata().getGeneration() == observedGeneration) { + var vmDef = vmStub.model(); + if (vmDef.isPresent() + && vmDef.get().metadata().getGeneration() == observedGeneration) { return; } - vmCrApi.updateStatus(vmCr, from -> { - JsonObject status = currentStatus(from); + vmStub.updateStatus(vmDef.get(), from -> { + JsonObject status = from.status(); status.getAsJsonArray("conditions").asList().stream() .map(cond -> (JsonObject) cond).filter(cond -> "Running" .equals(cond.get("type").getAsString())) @@ -249,15 +212,15 @@ public class StatusUpdater extends Component { * @throws ApiException */ @Handler + @SuppressWarnings("PMD.AssignmentInOperand") public void onRunnerStateChanged(RunnerStateChange event) throws ApiException { - if (vmCrApi == null) { + K8sDynamicModel vmDef; + if (vmStub == null || (vmDef = vmStub.model().orElse(null)) == null) { return; } - var vmCr = vmCrApi.get(namespace, vmName).throwsApiException() - .getObject(); - vmCrApi.updateStatus(vmCr, from -> { - JsonObject status = currentStatus(from); + vmStub.updateStatus(vmDef, from -> { + JsonObject status = from.status(); status.getAsJsonArray("conditions").asList().stream() .map(cond -> (JsonObject) cond) .forEach(cond -> { @@ -266,7 +229,7 @@ public class StatusUpdater extends Component { } }); if (event.state() == State.STARTING) { - status.addProperty("ram", GsonPtr.to(from.getRaw()) + status.addProperty("ram", GsonPtr.to(from.data()) .getAsString("spec", "vm", "maximumRam").orElse("0")); status.addProperty("cpus", 1); } else if (event.state() == State.STOPPED) { @@ -274,40 +237,32 @@ public class StatusUpdater extends Component { status.addProperty("cpus", 0); } return status; - }).throwsApiException(); + }); // Maybe stop VM if (event.state() == State.TERMINATING && !event.failed() && guestShutdownStops && shutdownByGuest) { logger.info(() -> "Stopping VM because of shutdown by guest."); - PatchOptions patchOpts = new PatchOptions(); - patchOpts.setFieldManager("kubernetes-java-kubectl-apply"); - var res = vmCrApi.patch(namespace, vmName, - V1Patch.PATCH_FORMAT_JSON_PATCH, + var res = vmStub.patch(V1Patch.PATCH_FORMAT_JSON_PATCH, new V1Patch("[{\"op\": \"replace\", \"path\": \"/spec/vm/state" + "\", \"value\": \"Stopped\"}]"), - patchOpts); - if (!res.isSuccess()) { + apiClient.defaultPatchOptions()); + if (!res.isPresent()) { logger.warning( - () -> "Cannot patch pod annotations: " + res.getStatus()); + () -> "Cannot patch pod annotations for: " + vmStub.name()); } } // Log event - var evt = new EventsV1Event().kind("Event") - .metadata(new V1ObjectMeta().namespace(namespace) - .generateName("vmrunner-")) + var evt = new EventsV1Event() .reportingController(VM_OP_GROUP + "/" + APP_NAME) - .reportingInstance(vmCr.getMetadata().getName()) - .eventTime(OffsetDateTime.now()).type("Normal") - .regarding(K8s.objectReference(vmCr)) .action("StatusUpdate").reason(event.reason()) .note(event.message()); - evtsApi.createNamespacedEvent(namespace, evt, null, null, null, null); + K8s.createEvent(apiClient, vmDef, evt); } private void updateRunningCondition(RunnerStateChange event, - DynamicKubernetesObject from, JsonObject cond) { + K8sDynamicModel from, JsonObject cond) { boolean reportedRunning = "True".equals(cond.get("status").getAsString()); if (RUNNING_STATES.contains(event.state()) @@ -336,18 +291,16 @@ public class StatusUpdater extends Component { */ @Handler public void onBallonChange(BalloonChangeEvent event) throws ApiException { - if (vmCrApi == null) { + if (vmStub == null) { return; } - var vmCr = vmCrApi.get(namespace, vmName).throwsApiException() - .getObject(); - vmCrApi.updateStatus(vmCr, from -> { - JsonObject status = currentStatus(from); + vmStub.updateStatus(from -> { + JsonObject status = from.status(); status.addProperty("ram", new Quantity(new BigDecimal(event.size()), Format.BINARY_SI) .toSuffixedString()); return status; - }).throwsApiException(); + }); } /** @@ -358,16 +311,14 @@ public class StatusUpdater extends Component { */ @Handler public void onCpuChange(HotpluggableCpuStatus event) throws ApiException { - if (vmCrApi == null) { + if (vmStub == null) { return; } - var vmCr = vmCrApi.get(namespace, vmName).throwsApiException() - .getObject(); - vmCrApi.updateStatus(vmCr, from -> { - JsonObject status = currentStatus(from); + vmStub.updateStatus(from -> { + JsonObject status = from.status(); status.addProperty("cpus", event.usedCpus().size()); return status; - }).throwsApiException(); + }); } /** diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java index f096485..993a844 100644 --- a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java +++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/VmConlet.java @@ -25,7 +25,6 @@ import freemarker.template.Template; import freemarker.template.TemplateNotFoundException; import io.kubernetes.client.custom.Quantity; import io.kubernetes.client.custom.Quantity.Format; -import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; @@ -38,6 +37,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.jdrupes.json.JsonBeanDecoder; import org.jdrupes.json.JsonDecodeException; +import org.jdrupes.vmoperator.common.K8sDynamicModel; import org.jdrupes.vmoperator.manager.events.ModifyVm; import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmDefChanged; @@ -46,7 +46,6 @@ import org.jdrupes.vmoperator.util.GsonPtr; import org.jgrapes.core.Channel; import org.jgrapes.core.Event; import org.jgrapes.core.Manager; -import org.jgrapes.core.NamedChannel; import org.jgrapes.core.annotation.Handler; import org.jgrapes.webconsole.base.Conlet.RenderMode; import org.jgrapes.webconsole.base.ConletBaseModel; @@ -70,7 +69,9 @@ public class VmConlet extends FreeMarkerConlet { private static final Set MODES = RenderMode.asSet( RenderMode.Preview, RenderMode.View); - private final Map vmInfos + private final Map vmInfos + = new ConcurrentHashMap<>(); + private final Map vmChannels = new ConcurrentHashMap<>(); private final TimeSeries summarySeries = new TimeSeries(Duration.ofDays(1)); private Summary cachedSummary; @@ -162,7 +163,7 @@ public class VmConlet extends FreeMarkerConlet { } if (sendVmInfos) { for (var vmInfo : vmInfos.values()) { - var def = JsonBeanDecoder.create(vmInfo.getRaw().toString()) + var def = JsonBeanDecoder.create(vmInfo.data().toString()) .readObject(); channel.respond(new NotifyConletView(type(), conletId, "updateVm", def)); @@ -185,9 +186,10 @@ public class VmConlet extends FreeMarkerConlet { "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals" }) public void onVmDefChanged(VmDefChanged event, VmChannel channel) throws JsonDecodeException, IOException { + var vmName = event.vmDefinition().getMetadata().getName(); if (event.type() == Type.DELETED) { - var vmName = event.vmDefinition().getMetadata().getName(); vmInfos.remove(vmName); + vmChannels.remove(vmName); for (var entry : conletIdsByConsoleConnection().entrySet()) { for (String conletId : entry.getValue()) { entry.getKey().respond(new NotifyConletView(type(), @@ -195,8 +197,11 @@ public class VmConlet extends FreeMarkerConlet { } } } else { - var vmDef = convertQuantities(event); - var def = JsonBeanDecoder.create(vmDef.getRaw().toString()) + var vmDef = new K8sDynamicModel(channel.client().getJSON() + .getGson(), convertQuantities(event.vmDefinition().data())); + vmInfos.put(vmName, vmDef); + vmChannels.put(vmName, channel); + var def = JsonBeanDecoder.create(vmDef.data().toString()) .readObject(); for (var entry : conletIdsByConsoleConnection().entrySet()) { for (String conletId : entry.getValue()) { @@ -217,28 +222,25 @@ public class VmConlet extends FreeMarkerConlet { } @SuppressWarnings("PMD.AvoidDuplicateLiterals") - private DynamicKubernetesObject convertQuantities(VmDefChanged event) { + private JsonObject convertQuantities(JsonObject vmDef) { // Clone and remove managed fields - var vmDef = new DynamicKubernetesObject( - event.vmDefinition().getRaw().deepCopy()); - GsonPtr.to(vmDef.getRaw()).to("metadata").get(JsonObject.class) + var json = vmDef.deepCopy(); + GsonPtr.to(json).to("metadata").get(JsonObject.class) .remove("managedFields"); // Convert RAM sizes to unitless numbers - var vmSpec = GsonPtr.to(vmDef.getRaw()).to("spec", "vm"); + var vmSpec = GsonPtr.to(json).to("spec", "vm"); vmSpec.set("maximumRam", Quantity.fromString( vmSpec.getAsString("maximumRam").orElse("0")).getNumber() .toBigInteger()); vmSpec.set("currentRam", Quantity.fromString( vmSpec.getAsString("currentRam").orElse("0")).getNumber() .toBigInteger()); - var status = GsonPtr.to(vmDef.getRaw()).to("status"); + var status = GsonPtr.to(json).to("status"); status.set("ram", Quantity.fromString( status.getAsString("ram").orElse("0")).getNumber() .toBigInteger()); - String vmName = event.vmDefinition().getMetadata().getName(); - vmInfos.put(vmName, vmDef); - return vmDef; + return json; } /** @@ -323,7 +325,7 @@ public class VmConlet extends FreeMarkerConlet { Summary summary = new Summary(); for (var vmDef : vmInfos.values()) { summary.totalVms += 1; - var status = GsonPtr.to(vmDef.getRaw()).to("status"); + var status = GsonPtr.to(vmDef.data()).to("status"); summary.usedCpus += status.getAsInt("cpus").orElse(0); summary.usedRam = summary.usedRam.add(status.getAsString("ram") .map(BigInteger::new).orElse(BigInteger.ZERO)); @@ -346,25 +348,28 @@ public class VmConlet extends FreeMarkerConlet { ConsoleConnection channel, VmsModel conletState) throws Exception { event.stop(); + var vmName = event.params().asString(0); + var vmChannel = vmChannels.get(vmName); + if (vmChannel == null) { + return; + } switch (event.method()) { case "start": - fire(new ModifyVm(event.params().asString(0), "state", "Running", - new NamedChannel("manager"))); + fire(new ModifyVm(vmName, "state", "Running", vmChannel)); break; case "stop": - fire(new ModifyVm(event.params().asString(0), "state", "Stopped", - new NamedChannel("manager"))); + fire(new ModifyVm(vmName, "state", "Stopped", vmChannel)); break; case "cpus": - fire(new ModifyVm(event.params().asString(0), "currentCpus", + fire(new ModifyVm(vmName, "currentCpus", new BigDecimal(event.params().asDouble(1)).toBigInteger(), - new NamedChannel("manager"))); + vmChannel)); break; case "ram": - fire(new ModifyVm(event.params().asString(0), "currentRam", + fire(new ModifyVm(vmName, "currentRam", new Quantity(new BigDecimal(event.params().asDouble(1)), Format.BINARY_SI).toSuffixedString(), - new NamedChannel("manager"))); + vmChannel)); break; default:// ignore break;