diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs index 44c5061..72733d9 100644 --- a/.settings/org.eclipse.buildship.core.prefs +++ b/.settings/org.eclipse.buildship.core.prefs @@ -5,7 +5,7 @@ connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) connection.project.dir= eclipse.preferences.version=1 gradle.user.home= -java.home=/usr/lib/jvm/java-17-openjdk-17.0.8.0.7-1.fc37.x86_64 +java.home= jvm.arguments= offline.mode=false override.workspace.settings=true diff --git a/deploy/crds/vms-crd.yaml b/deploy/crds/vms-crd.yaml index e4db3d5..de65f8a 100644 --- a/deploy/crds/vms-crd.yaml +++ b/deploy/crds/vms-crd.yaml @@ -1385,9 +1385,20 @@ spec: type: object properties: port: + description: >- + Port number used for the Spice server. type: integer default: 5900 - ticket: + server: + description: >- + Server (address) to use for connecting + to the spice server. Defaults to the address + of the node that the VM is running on. + type: string + proxyUrl: + description: >- + If specified, is copied to the generated + viewer configuration files. type: string streamingVideo: type: string @@ -1413,7 +1424,8 @@ spec: default: "0" displayPasswordSerial: description: >- - Counts changes of the display password. + Counts changes of the display password. Set to -1 + by the runner if password protection is not enabled. type: integer default: 0 conditions: diff --git a/dev-example/config.yaml b/dev-example/config.yaml index 09183b4..3c94254 100644 --- a/dev-example/config.yaml +++ b/dev-example/config.yaml @@ -41,3 +41,7 @@ other: - --org.jdrupes.vmoperator.vmconlet.VmConlet - org.jgrapes.webconlet.oidclogin.LoginConlet + "/ComponentCollector": + "/VmViewer": + displayResource: + preferredIpVersion: ipv4 diff --git a/dev-example/kustomization.yaml b/dev-example/kustomization.yaml index 70c6ae6..9829bf6 100644 --- a/dev-example/kustomization.yaml +++ b/dev-example/kustomization.yaml @@ -66,7 +66,10 @@ patches: # Others cannot use any conlet (except login conlet to log out) other: - org.jgrapes.webconlet.locallogin.LoginConlet - + "/ComponentCollector": + "/VmViewer": + displayResource: + preferredIpVersion: ipv4 - target: group: apps version: v1 diff --git a/dev-example/test-vm-display-secret.yaml b/dev-example/test-vm-display-secret.yaml index f3b5ccb..a6f0fe6 100644 --- a/dev-example/test-vm-display-secret.yaml +++ b/dev-example/test-vm-display-secret.yaml @@ -10,3 +10,4 @@ metadata: type: Opaque data: display-password: dGVzdC12bQ== + password-expiry: KzMw diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java index d5d457c..150bb3b 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/Constants.java @@ -21,6 +21,7 @@ package org.jdrupes.vmoperator.common; /** * Some constants. */ +@SuppressWarnings("PMD.DataClass") public class Constants { /** The Constant APP_NAME. */ 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 2c29fab..481f724 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 @@ -44,7 +44,6 @@ import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; -// TODO: Auto-generated Javadoc /** * Helpers for K8s API. */ @@ -168,6 +167,7 @@ public class K8s { * @return the object */ @Deprecated + @SuppressWarnings("PMD.GenericsNaming") public static Optional get(GenericKubernetesApi api, V1ObjectMeta meta) { @@ -189,6 +189,7 @@ public class K8s { * @return the t * @throws ApiException the api exception */ + @SuppressWarnings("PMD.GenericsNaming") public static T apply(GenericKubernetesApi api, T existing, String update) throws ApiException { 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 index b7106fb..37b0b97 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClient.java @@ -48,7 +48,8 @@ import okhttp3.Response; * A client with some additional properties. */ @SuppressWarnings({ "PMD.ExcessivePublicCount", "PMD.TooManyMethods", - "PMD.LinguisticNaming", "checkstyle:LineLength" }) + "PMD.LinguisticNaming", "checkstyle:LineLength", + "PMD.CouplingBetweenObjects", "PMD.GodClass" }) public class K8sClient extends ApiClient { private ApiClient apiClient; @@ -96,381 +97,510 @@ public class K8sClient extends ApiClient { } /** - * @return + * Gets the base path. + * + * @return the base path * @see ApiClient#getBasePath() */ + @Override public String getBasePath() { return apiClient().getBasePath(); } /** - * @param basePath - * @return + * Sets the base path. + * + * @param basePath the base path + * @return the api client * @see ApiClient#setBasePath(java.lang.String) */ + @Override public ApiClient setBasePath(String basePath) { return apiClient().setBasePath(basePath); } /** - * @return + * Gets the http client. + * + * @return the http client * @see ApiClient#getHttpClient() */ + @Override public OkHttpClient getHttpClient() { return apiClient().getHttpClient(); } /** - * @param newHttpClient - * @return + * Sets the http client. + * + * @param newHttpClient the new http client + * @return the api client * @see ApiClient#setHttpClient(okhttp3.OkHttpClient) */ + @Override public ApiClient setHttpClient(OkHttpClient newHttpClient) { return apiClient().setHttpClient(newHttpClient); } /** - * @return + * Gets the json. + * + * @return the json * @see ApiClient#getJSON() */ @SuppressWarnings("abbreviationAsWordInName") + @Override public JSON getJSON() { return apiClient().getJSON(); } /** - * @param json - * @return + * Sets the JSON. + * + * @param json the json + * @return the api client * @see ApiClient#setJSON(io.kubernetes.client.openapi.JSON) */ @SuppressWarnings("abbreviationAsWordInName") + @Override public ApiClient setJSON(JSON json) { return apiClient().setJSON(json); } /** - * @return + * Checks if is verifying ssl. + * + * @return true, if is verifying ssl * @see ApiClient#isVerifyingSsl() */ + @Override public boolean isVerifyingSsl() { return apiClient().isVerifyingSsl(); } /** - * @param verifyingSsl - * @return + * Sets the verifying ssl. + * + * @param verifyingSsl the verifying ssl + * @return the api client * @see ApiClient#setVerifyingSsl(boolean) */ + @Override public ApiClient setVerifyingSsl(boolean verifyingSsl) { return apiClient().setVerifyingSsl(verifyingSsl); } /** - * @return + * Gets the ssl ca cert. + * + * @return the ssl ca cert * @see ApiClient#getSslCaCert() */ + @Override public InputStream getSslCaCert() { return apiClient().getSslCaCert(); } /** - * @param sslCaCert - * @return + * Sets the ssl ca cert. + * + * @param sslCaCert the ssl ca cert + * @return the api client * @see ApiClient#setSslCaCert(java.io.InputStream) */ + @Override public ApiClient setSslCaCert(InputStream sslCaCert) { return apiClient().setSslCaCert(sslCaCert); } /** - * @return + * Gets the key managers. + * + * @return the key managers * @see ApiClient#getKeyManagers() */ + @Override public KeyManager[] getKeyManagers() { return apiClient().getKeyManagers(); } /** - * @param managers - * @return + * Sets the key managers. + * + * @param managers the managers + * @return the api client * @see ApiClient#setKeyManagers(javax.net.ssl.KeyManager[]) */ @SuppressWarnings("PMD.UseVarargs") + @Override public ApiClient setKeyManagers(KeyManager[] managers) { return apiClient().setKeyManagers(managers); } /** - * @return + * Gets the date format. + * + * @return the date format * @see ApiClient#getDateFormat() */ + @Override public DateFormat getDateFormat() { return apiClient().getDateFormat(); } /** - * @param dateFormat - * @return + * Sets the date format. + * + * @param dateFormat the date format + * @return the api client * @see ApiClient#setDateFormat(java.text.DateFormat) */ + @Override public ApiClient setDateFormat(DateFormat dateFormat) { return apiClient().setDateFormat(dateFormat); } /** - * @param dateFormat - * @return + * Sets the sql date format. + * + * @param dateFormat the date format + * @return the api client * @see ApiClient#setSqlDateFormat(java.text.DateFormat) */ + @Override public ApiClient setSqlDateFormat(DateFormat dateFormat) { return apiClient().setSqlDateFormat(dateFormat); } /** - * @param dateFormat - * @return + * Sets the offset date time format. + * + * @param dateFormat the date format + * @return the api client * @see ApiClient#setOffsetDateTimeFormat(java.time.format.DateTimeFormatter) */ + @Override public ApiClient setOffsetDateTimeFormat(DateTimeFormatter dateFormat) { return apiClient().setOffsetDateTimeFormat(dateFormat); } /** - * @param dateFormat - * @return + * Sets the local date format. + * + * @param dateFormat the date format + * @return the api client * @see ApiClient#setLocalDateFormat(java.time.format.DateTimeFormatter) */ + @Override public ApiClient setLocalDateFormat(DateTimeFormatter dateFormat) { return apiClient().setLocalDateFormat(dateFormat); } /** - * @param lenientOnJson - * @return + * Sets the lenient on json. + * + * @param lenientOnJson the lenient on json + * @return the api client * @see ApiClient#setLenientOnJson(boolean) */ + @Override public ApiClient setLenientOnJson(boolean lenientOnJson) { return apiClient().setLenientOnJson(lenientOnJson); } /** - * @return + * Gets the authentications. + * + * @return the authentications * @see ApiClient#getAuthentications() */ + @Override public Map getAuthentications() { return apiClient().getAuthentications(); } /** - * @param authName - * @return + * Gets the authentication. + * + * @param authName the auth name + * @return the authentication * @see ApiClient#getAuthentication(java.lang.String) */ + @Override public Authentication getAuthentication(String authName) { return apiClient().getAuthentication(authName); } /** - * @param username + * Sets the username. + * + * @param username the new username * @see ApiClient#setUsername(java.lang.String) */ + @Override public void setUsername(String username) { apiClient().setUsername(username); } /** - * @param password + * Sets the password. + * + * @param password the new password * @see ApiClient#setPassword(java.lang.String) */ + @Override public void setPassword(String password) { apiClient().setPassword(password); } /** - * @param apiKey + * Sets the api key. + * + * @param apiKey the new api key * @see ApiClient#setApiKey(java.lang.String) */ + @Override public void setApiKey(String apiKey) { apiClient().setApiKey(apiKey); } /** - * @param apiKeyPrefix + * Sets the api key prefix. + * + * @param apiKeyPrefix the new api key prefix * @see ApiClient#setApiKeyPrefix(java.lang.String) */ + @Override public void setApiKeyPrefix(String apiKeyPrefix) { apiClient().setApiKeyPrefix(apiKeyPrefix); } /** - * @param accessToken + * Sets the access token. + * + * @param accessToken the new access token * @see ApiClient#setAccessToken(java.lang.String) */ + @Override public void setAccessToken(String accessToken) { apiClient().setAccessToken(accessToken); } /** - * @param userAgent - * @return + * Sets the user agent. + * + * @param userAgent the user agent + * @return the api client * @see ApiClient#setUserAgent(java.lang.String) */ + @Override public ApiClient setUserAgent(String userAgent) { return apiClient().setUserAgent(userAgent); } /** - * @return + * To string. + * + * @return the string * @see java.lang.Object#toString() */ + @Override public String toString() { return apiClient().toString(); } /** - * @param key - * @param value - * @return + * Adds the default header. + * + * @param key the key + * @param value the value + * @return the api client * @see ApiClient#addDefaultHeader(java.lang.String, java.lang.String) */ + @Override public ApiClient addDefaultHeader(String key, String value) { return apiClient().addDefaultHeader(key, value); } /** - * @param key - * @param value - * @return + * Adds the default cookie. + * + * @param key the key + * @param value the value + * @return the api client * @see ApiClient#addDefaultCookie(java.lang.String, java.lang.String) */ + @Override public ApiClient addDefaultCookie(String key, String value) { return apiClient().addDefaultCookie(key, value); } /** - * @return + * Checks if is debugging. + * + * @return true, if is debugging * @see ApiClient#isDebugging() */ + @Override public boolean isDebugging() { return apiClient().isDebugging(); } /** - * @param debugging - * @return + * Sets the debugging. + * + * @param debugging the debugging + * @return the api client * @see ApiClient#setDebugging(boolean) */ + @Override public ApiClient setDebugging(boolean debugging) { return apiClient().setDebugging(debugging); } /** - * @return + * Gets the temp folder path. + * + * @return the temp folder path * @see ApiClient#getTempFolderPath() */ + @Override public String getTempFolderPath() { return apiClient().getTempFolderPath(); } /** - * @param tempFolderPath - * @return + * Sets the temp folder path. + * + * @param tempFolderPath the temp folder path + * @return the api client * @see ApiClient#setTempFolderPath(java.lang.String) */ + @Override public ApiClient setTempFolderPath(String tempFolderPath) { return apiClient().setTempFolderPath(tempFolderPath); } /** - * @return + * Gets the connect timeout. + * + * @return the connect timeout * @see ApiClient#getConnectTimeout() */ + @Override public int getConnectTimeout() { return apiClient().getConnectTimeout(); } /** - * @param connectionTimeout - * @return + * Sets the connect timeout. + * + * @param connectionTimeout the connection timeout + * @return the api client * @see ApiClient#setConnectTimeout(int) */ + @Override public ApiClient setConnectTimeout(int connectionTimeout) { return apiClient().setConnectTimeout(connectionTimeout); } /** - * @return + * Gets the read timeout. + * + * @return the read timeout * @see ApiClient#getReadTimeout() */ + @Override public int getReadTimeout() { return apiClient().getReadTimeout(); } /** - * @param readTimeout - * @return + * Sets the read timeout. + * + * @param readTimeout the read timeout + * @return the api client * @see ApiClient#setReadTimeout(int) */ + @Override public ApiClient setReadTimeout(int readTimeout) { return apiClient().setReadTimeout(readTimeout); } /** - * @return + * Gets the write timeout. + * + * @return the write timeout * @see ApiClient#getWriteTimeout() */ + @Override public int getWriteTimeout() { return apiClient().getWriteTimeout(); } /** - * @param writeTimeout - * @return + * Sets the write timeout. + * + * @param writeTimeout the write timeout + * @return the api client * @see ApiClient#setWriteTimeout(int) */ + @Override public ApiClient setWriteTimeout(int writeTimeout) { return apiClient().setWriteTimeout(writeTimeout); } /** - * @param param - * @return + * Parameter to string. + * + * @param param the param + * @return the string * @see ApiClient#parameterToString(java.lang.Object) */ + @Override public String parameterToString(Object param) { return apiClient().parameterToString(param); } /** - * @param name - * @param value - * @return + * Parameter to pair. + * + * @param name the name + * @param value the value + * @return the list * @see ApiClient#parameterToPair(java.lang.String, java.lang.Object) */ + @Override public List parameterToPair(String name, Object value) { return apiClient().parameterToPair(name, value); } /** - * @param collectionFormat - * @param name - * @param value - * @return + * Parameter to pairs. + * + * @param collectionFormat the collection format + * @param name the name + * @param value the value + * @return the list * @see ApiClient#parameterToPairs(java.lang.String, java.lang.String, java.util.Collection) */ @SuppressWarnings({ "rawtypes", "PMD.AvoidDuplicateLiterals" }) + @Override public List parameterToPairs(String collectionFormat, String name, Collection value) { return apiClient().parameterToPairs(collectionFormat, name, value); } /** - * @param collectionFormat - * @param value - * @return + * Collection path parameter to string. + * + * @param collectionFormat the collection format + * @param value the value + * @return the string * @see ApiClient#collectionPathParameterToString(java.lang.String, java.util.Collection) */ @SuppressWarnings("rawtypes") + @Override public String collectionPathParameterToString(String collectionFormat, Collection value) { return apiClient().collectionPathParameterToString(collectionFormat, @@ -478,173 +608,218 @@ public class K8sClient extends ApiClient { } /** - * @param filename - * @return + * Sanitize filename. + * + * @param filename the filename + * @return the string * @see ApiClient#sanitizeFilename(java.lang.String) */ + @Override public String sanitizeFilename(String filename) { return apiClient().sanitizeFilename(filename); } /** - * @param mime - * @return + * Checks if is json mime. + * + * @param mime the mime + * @return true, if is json mime * @see ApiClient#isJsonMime(java.lang.String) */ + @Override public boolean isJsonMime(String mime) { return apiClient().isJsonMime(mime); } /** - * @param accepts - * @return + * Select header accept. + * + * @param accepts the accepts + * @return the string * @see ApiClient#selectHeaderAccept(java.lang.String[]) */ @SuppressWarnings("PMD.UseVarargs") + @Override public String selectHeaderAccept(String[] accepts) { return apiClient().selectHeaderAccept(accepts); } /** - * @param contentTypes - * @return + * Select header content type. + * + * @param contentTypes the content types + * @return the string * @see ApiClient#selectHeaderContentType(java.lang.String[]) */ @SuppressWarnings("PMD.UseVarargs") + @Override public String selectHeaderContentType(String[] contentTypes) { return apiClient().selectHeaderContentType(contentTypes); } /** - * @param str - * @return + * Escape string. + * + * @param str the str + * @return the string * @see ApiClient#escapeString(java.lang.String) */ + @Override public String escapeString(String str) { return apiClient().escapeString(str); } /** - * @param - * @param response - * @param returnType - * @return - * @throws ApiException + * Deserialize. + * + * @param the generic type + * @param response the response + * @param returnType the return type + * @return the t + * @throws ApiException the api exception * @see ApiClient#deserialize(okhttp3.Response, java.lang.reflect.Type) */ + @Override public T deserialize(Response response, Type returnType) throws ApiException { return apiClient().deserialize(response, returnType); } /** - * @param obj - * @param contentType - * @return - * @throws ApiException + * Serialize. + * + * @param obj the obj + * @param contentType the content type + * @return the request body + * @throws ApiException the api exception * @see ApiClient#serialize(java.lang.Object, java.lang.String) */ + @Override public RequestBody serialize(Object obj, String contentType) throws ApiException { return apiClient().serialize(obj, contentType); } /** - * @param response - * @return - * @throws ApiException + * Download file from response. + * + * @param response the response + * @return the file + * @throws ApiException the api exception * @see ApiClient#downloadFileFromResponse(okhttp3.Response) */ + @Override public File downloadFileFromResponse(Response response) throws ApiException { return apiClient().downloadFileFromResponse(response); } /** - * @param response - * @return - * @throws IOException + * Prepare download file. + * + * @param response the response + * @return the file + * @throws IOException Signals that an I/O exception has occurred. * @see ApiClient#prepareDownloadFile(okhttp3.Response) */ + @Override public File prepareDownloadFile(Response response) throws IOException { return apiClient().prepareDownloadFile(response); } /** - * @param - * @param call - * @return - * @throws ApiException + * Execute. + * + * @param the generic type + * @param call the call + * @return the api response + * @throws ApiException the api exception * @see ApiClient#execute(okhttp3.Call) */ + @Override public ApiResponse execute(Call call) throws ApiException { return apiClient().execute(call); } /** - * @param - * @param call - * @param returnType - * @return - * @throws ApiException + * Execute. + * + * @param the generic type + * @param call the call + * @param returnType the return type + * @return the api response + * @throws ApiException the api exception * @see ApiClient#execute(okhttp3.Call, java.lang.reflect.Type) */ + @Override public ApiResponse execute(Call call, Type returnType) throws ApiException { return apiClient().execute(call, returnType); } /** - * @param - * @param call - * @param callback + * Execute async. + * + * @param the generic type + * @param call the call + * @param callback the callback * @see ApiClient#executeAsync(okhttp3.Call, io.kubernetes.client.openapi.ApiCallback) */ + @Override public void executeAsync(Call call, ApiCallback callback) { apiClient().executeAsync(call, callback); } /** - * @param - * @param call - * @param returnType - * @param callback + * Execute async. + * + * @param the generic type + * @param call the call + * @param returnType the return type + * @param callback the callback * @see ApiClient#executeAsync(okhttp3.Call, java.lang.reflect.Type, io.kubernetes.client.openapi.ApiCallback) */ + @Override public void executeAsync(Call call, Type returnType, ApiCallback callback) { apiClient().executeAsync(call, returnType, callback); } /** - * @param - * @param response - * @param returnType - * @return - * @throws ApiException + * Handle response. + * + * @param the generic type + * @param response the response + * @param returnType the return type + * @return the t + * @throws ApiException the api exception * @see ApiClient#handleResponse(okhttp3.Response, java.lang.reflect.Type) */ + @Override 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 + * Builds the call. + * + * @param path the path + * @param method the method + * @param queryParams the query params + * @param collectionQueryParams the collection query params + * @param body the body + * @param headerParams the header params + * @param cookieParams the cookie params + * @param formParams the form params + * @param authNames the auth names + * @param callback the callback + * @return the call + * @throws ApiException the api exception * @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" }) + @Override public Call buildCall(String path, String method, List queryParams, List collectionQueryParams, Object body, Map headerParams, Map cookieParams, @@ -656,21 +831,24 @@ public class K8sClient extends ApiClient { } /** - * @param path - * @param method - * @param queryParams - * @param collectionQueryParams - * @param body - * @param headerParams - * @param cookieParams - * @param formParams - * @param authNames - * @param callback - * @return - * @throws ApiException + * Builds the request. + * + * @param path the path + * @param method the method + * @param queryParams the query params + * @param collectionQueryParams the collection query params + * @param body the body + * @param headerParams the header params + * @param cookieParams the cookie params + * @param formParams the form params + * @param authNames the auth names + * @param callback the callback + * @return the request + * @throws ApiException the api exception * @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" }) + @Override public Request buildRequest(String path, String method, List queryParams, List collectionQueryParams, Object body, Map headerParams, @@ -682,44 +860,56 @@ public class K8sClient extends ApiClient { } /** - * @param path - * @param queryParams - * @param collectionQueryParams - * @return + * Builds the url. + * + * @param path the path + * @param queryParams the query params + * @param collectionQueryParams the collection query params + * @return the string * @see ApiClient#buildUrl(java.lang.String, java.util.List, java.util.List) */ + @Override public String buildUrl(String path, List queryParams, List collectionQueryParams) { return apiClient().buildUrl(path, queryParams, collectionQueryParams); } /** - * @param headerParams - * @param reqBuilder + * Process header params. + * + * @param headerParams the header params + * @param reqBuilder the req builder * @see ApiClient#processHeaderParams(java.util.Map, okhttp3.Request.Builder) */ + @Override public void processHeaderParams(Map headerParams, Builder reqBuilder) { apiClient().processHeaderParams(headerParams, reqBuilder); } /** - * @param cookieParams - * @param reqBuilder + * Process cookie params. + * + * @param cookieParams the cookie params + * @param reqBuilder the req builder * @see ApiClient#processCookieParams(java.util.Map, okhttp3.Request.Builder) */ + @Override public void processCookieParams(Map cookieParams, Builder reqBuilder) { apiClient().processCookieParams(cookieParams, reqBuilder); } /** - * @param authNames - * @param queryParams - * @param headerParams - * @param cookieParams + * Update params for auth. + * + * @param authNames the auth names + * @param queryParams the query params + * @param headerParams the header params + * @param cookieParams the cookie params * @see ApiClient#updateParamsForAuth(java.lang.String[], java.util.List, java.util.Map, java.util.Map) */ + @Override public void updateParamsForAuth(String[] authNames, List queryParams, Map headerParams, Map cookieParams) { @@ -728,30 +918,39 @@ public class K8sClient extends ApiClient { } /** - * @param formParams - * @return + * Builds the request body form encoding. + * + * @param formParams the form params + * @return the request body * @see ApiClient#buildRequestBodyFormEncoding(java.util.Map) */ + @Override public RequestBody buildRequestBodyFormEncoding(Map formParams) { return apiClient().buildRequestBodyFormEncoding(formParams); } /** - * @param formParams - * @return + * Builds the request body multipart. + * + * @param formParams the form params + * @return the request body * @see ApiClient#buildRequestBodyMultipart(java.util.Map) */ + @Override public RequestBody buildRequestBodyMultipart(Map formParams) { return apiClient().buildRequestBodyMultipart(formParams); } /** - * @param file - * @return + * Guess content type from file. + * + * @param file the file + * @return the string * @see ApiClient#guessContentTypeFromFile(java.io.File) */ + @Override public String guessContentTypeFromFile(File file) { return apiClient().guessContentTypeFromFile(file); } diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClusterGenericStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClusterGenericStub.java new file mode 100644 index 0000000..81a4eab --- /dev/null +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sClusterGenericStub.java @@ -0,0 +1,401 @@ +/* + * 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.common.KubernetesListObject; +import io.kubernetes.client.common.KubernetesObject; +import io.kubernetes.client.custom.V1Patch; +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.GetOptions; +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.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +/** + * A stub for cluster scoped 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 K8sClusterGenericStub { + protected final K8sClient client; + private final GenericKubernetesApi api; + protected final APIResource context; + protected final String name; + + /** + * Instantiates a new stub for the object specified. If the object + * exists in the context specified, the version (see + * {@link #version()} is bound to the existing object's version. + * Else the stub is dangling with the version set to the context's + * preferred version. + * + * @param objectClass the object class + * @param objectListClass the object list class + * @param client the client + * @param context the context + * @param name the name + */ + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + protected K8sClusterGenericStub(Class objectClass, + Class objectListClass, K8sClient client, APIResource context, + String name) { + this.client = client; + this.name = name; + + // Bind version + var foundVersion = context.getPreferredVersion(); + GenericKubernetesApi testApi = null; + GetOptions mdOpts + = new GetOptions().isPartialObjectMetadataRequest(true); + for (var version : candidateVersions(context)) { + testApi = new GenericKubernetesApi<>(objectClass, objectListClass, + context.getGroup(), version, context.getResourcePlural(), + client); + if (testApi.get(name, mdOpts).isSuccess()) { + foundVersion = version; + break; + } + } + if (foundVersion.equals(context.getPreferredVersion())) { + this.context = context; + } else { + this.context = K8s.preferred(context, foundVersion); + } + + api = Optional.ofNullable(testApi) + .orElseGet(() -> new GenericKubernetesApi<>(objectClass, + objectListClass, group(), version(), plural(), client)); + } + + /** + * Gets the context. + * + * @return the context + */ + public APIResource context() { + return context; + } + + /** + * Gets the group. + * + * @return the group + */ + public String group() { + return context.getGroup(); + } + + /** + * Gets the version. + * + * @return the version + */ + public String version() { + return context.getPreferredVersion(); + } + + /** + * Gets the kind. + * + * @return the kind + */ + public String kind() { + return context.getKind(); + } + + /** + * Gets the plural. + * + * @return the plural + */ + public String plural() { + return context.getResourcePlural(); + } + + /** + * 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(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(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(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(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 name the name + * @return the result + */ + @SuppressWarnings("PMD.UseObjectForClearerAPI") + R get(Class objectClass, Class objectListClass, K8sClient client, + APIResource context, String name); + } + + @Override + @SuppressWarnings("PMD.UseLocaleWithCaseConversions") + public String toString() { + return (Strings.isNullOrEmpty(group()) ? "" : group() + "/") + + version().toUpperCase() + kind() + " " + name; + } + + /** + * Get an 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 name the name + * @param provider the provider + * @return the stub if the object exists + * @throws ApiException the api exception + */ + @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" }) + public static > + R get(Class objectClass, Class objectListClass, + K8sClient client, GroupVersionKind gvk, 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(), + name); + } + + /** + * Get an 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 name the name + * @param provider the provider + * @return the stub if the object exists + * @throws ApiException the api exception + */ + @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", + "PMD.UseObjectForClearerAPI" }) + public static > + R get(Class objectClass, Class objectListClass, + K8sClient client, APIResource context, String name, + GenericSupplier provider) { + return provider.get(objectClass, objectListClass, client, context, + name); + } + + /** + * Get an 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().getName()); + } + + /** + * Get the stubs for the objects 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 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, + ListOptions options, GenericSupplier provider) + throws ApiException { + var result = new ArrayList(); + for (var version : candidateVersions(context)) { + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + var api = new GenericKubernetesApi<>(objectClass, objectListClass, + context.getGroup(), version, context.getResourcePlural(), + client); + var objs = api.list(options).throwsApiException(); + for (var item : objs.getObject().getItems()) { + result.add(provider.get(objectClass, objectListClass, client, + context, item.getMetadata().getName())); + } + } + return result; + } + + private static List candidateVersions(APIResource context) { + var result = new LinkedList<>(context.getVersions()); + result.remove(context.getPreferredVersion()); + result.add(0, context.getPreferredVersion()); + return result; + } + +} 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 index 9018744..33a8e18 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelTypeAdapterFactory.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sDynamicModelTypeAdapterFactory.java @@ -44,12 +44,13 @@ public class K8sDynamicModelTypeAdapterFactory implements TypeAdapterFactory { * this factory */ @SuppressWarnings("unchecked") + @Override public TypeAdapter create(Gson gson, TypeToken typeToken) { if (TypeToken.get(K8sDynamicModel.class).equals(typeToken)) { - return (TypeAdapter) (new K8sDynamicModelCreator(gson)); + return (TypeAdapter) new K8sDynamicModelCreator(gson); } if (TypeToken.get(K8sDynamicModels.class).equals(typeToken)) { - return (TypeAdapter) (new K8sDynamicModelsCreator(gson)); + return (TypeAdapter) new K8sDynamicModelsCreator(gson); } return null; } diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sObserver.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sObserver.java index 148ce83..2545a30 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sObserver.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sObserver.java @@ -87,7 +87,7 @@ public class K8sObserver { try { - logger.info(() -> "Watching " + context.getResourcePlural() + logger.config(() -> "Watching " + context.getResourcePlural() + " (" + context.getPreferredVersion() + ")" + " in " + namespace); diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1NodeStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1NodeStub.java new file mode 100644 index 0000000..050c593 --- /dev/null +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1NodeStub.java @@ -0,0 +1,85 @@ +/* + * 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.V1Node; +import io.kubernetes.client.openapi.models.V1NodeList; +import io.kubernetes.client.util.generic.options.ListOptions; +import java.util.Collection; +import java.util.List; + +/** + * A stub for nodes (v1). + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class K8sV1NodeStub extends K8sClusterGenericStub { + + public static final APIResource CONTEXT = new APIResource("", List.of("v1"), + "v1", "Node", true, "nodes", "node"); + + /** + * Instantiates a new stub. + * + * @param client the client + * @param name the name + */ + protected K8sV1NodeStub(K8sClient client, String name) { + super(V1Node.class, V1NodeList.class, client, CONTEXT, name); + } + + /** + * Gets the stub for the given name. + * + * @param client the client + * @param name the name + * @return the config map stub + */ + public static K8sV1NodeStub get(K8sClient client, String name) { + return new K8sV1NodeStub(client, name); + } + + /** + * Get the stubs for the objects that match + * the criteria from the given options. + * + * @param client the client + * @param options the options + * @return the collection + * @throws ApiException the api exception + */ + public static Collection list(K8sClient client, + ListOptions options) throws ApiException { + return K8sClusterGenericStub.list(V1Node.class, V1NodeList.class, + client, CONTEXT, options, K8sV1NodeStub::getGeneric); + } + + /** + * Provide {@link GenericSupplier}. + */ + @SuppressWarnings({ "PMD.UnusedFormalParameter", + "PMD.UnusedPrivateMethod" }) + private static K8sV1NodeStub getGeneric(Class objectClass, + Class objectListClass, K8sClient client, + APIResource context, String name) { + return new K8sV1NodeStub(client, name); + } + +} \ 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 index b3da776..1cea986 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1PodStub.java @@ -79,7 +79,8 @@ public class K8sV1PodStub extends K8sGenericStub { /** * Provide {@link GenericSupplier}. */ - @SuppressWarnings("PMD.UnusedFormalParameter") + @SuppressWarnings({ "PMD.UnusedFormalParameter", + "PMD.UnusedPrivateMethod" }) private static K8sV1PodStub getGeneric(Class objectClass, Class objectListClass, K8sClient client, APIResource context, String namespace, String name) { diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java index 18973e0..9a43883 100644 --- a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1SecretStub.java @@ -81,7 +81,8 @@ public class K8sV1SecretStub extends K8sGenericStub { /** * Provide {@link GenericSupplier}. */ - @SuppressWarnings("PMD.UnusedFormalParameter") + @SuppressWarnings({ "PMD.UnusedFormalParameter", + "PMD.UnusedPrivateMethod" }) private static K8sV1SecretStub getGeneric(Class objectClass, Class objectListClass, K8sClient client, APIResource context, String namespace, String name) { diff --git a/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ServiceStub.java b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ServiceStub.java new file mode 100644 index 0000000..74f7f61 --- /dev/null +++ b/org.jdrupes.vmoperator.common/src/org/jdrupes/vmoperator/common/K8sV1ServiceStub.java @@ -0,0 +1,92 @@ +/* + * 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.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.util.generic.options.ListOptions; +import java.util.Collection; +import java.util.List; +import org.jdrupes.vmoperator.common.K8sGenericStub.GenericSupplier; + +/** + * A stub for secrets (v1). + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class K8sV1ServiceStub extends K8sGenericStub { + + public static final APIResource CONTEXT = new APIResource("", List.of("v1"), + "v1", "Service", true, "services", "service"); + + /** + * Instantiates a new stub. + * + * @param client the client + * @param namespace the namespace + * @param name the name + */ + protected K8sV1ServiceStub(K8sClient client, String namespace, + String name) { + super(V1Service.class, V1ServiceList.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 config map stub + */ + public static K8sV1ServiceStub get(K8sClient client, String namespace, + String name) { + return new K8sV1ServiceStub(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(V1Service.class, V1ServiceList.class, client, + CONTEXT, namespace, options, K8sV1ServiceStub::getGeneric); + } + + /** + * Provide {@link GenericSupplier}. + */ + @SuppressWarnings({ "PMD.UnusedFormalParameter", + "PMD.UnusedPrivateMethod" }) + private static K8sV1ServiceStub getGeneric(Class objectClass, + Class objectListClass, K8sClient client, + APIResource context, String namespace, String name) { + return new K8sV1ServiceStub(client, namespace, name); + } + +} \ No newline at end of file diff --git a/org.jdrupes.vmoperator.manager.events/build.gradle b/org.jdrupes.vmoperator.manager.events/build.gradle index 56c364f..566e200 100644 --- a/org.jdrupes.vmoperator.manager.events/build.gradle +++ b/org.jdrupes.vmoperator.manager.events/build.gradle @@ -11,4 +11,5 @@ plugins { dependencies { api 'org.jgrapes:org.jgrapes.core:[1.19.0,2)' api project(':org.jdrupes.vmoperator.common') + api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.16.1,3]' } diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelCache.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelCache.java index fc8df7a..1e6d031 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelCache.java +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ChannelCache.java @@ -44,7 +44,7 @@ public class ChannelCache { */ @SuppressWarnings("PMD.ShortClassName") private static class Data { - public WeakReference channel; + public final WeakReference channel; public A associated; /** diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/DisplaySecretChanged.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/DisplayPasswordChanged.java similarity index 89% rename from org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/DisplaySecretChanged.java rename to org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/DisplayPasswordChanged.java index 69dabe6..9185bbc 100644 --- a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/DisplaySecretChanged.java +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/DisplayPasswordChanged.java @@ -28,7 +28,7 @@ import org.jgrapes.core.Event; * Indicates that a display secret has changed. */ @SuppressWarnings("PMD.DataClass") -public class DisplaySecretChanged extends Event { +public class DisplayPasswordChanged extends Event { private final ResponseType type; private final V1Secret secret; @@ -39,7 +39,7 @@ public class DisplaySecretChanged extends Event { * @param type the type * @param secret the secret */ - public DisplaySecretChanged(ResponseType type, V1Secret secret) { + public DisplayPasswordChanged(ResponseType type, V1Secret secret) { this.type = type; this.secret = secret; } @@ -68,8 +68,7 @@ public class DisplaySecretChanged extends Event { builder.append(Components.objectName(this)).append(" [") .append(secret.getMetadata().getName()).append(' ').append(type); if (channels() != null) { - builder.append(", channels="); - builder.append(Channel.toString(channels())); + builder.append(", channels=").append(Channel.toString(channels())); } builder.append(']'); return builder.toString(); diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplayPassword.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplayPassword.java new file mode 100644 index 0000000..77dc298 --- /dev/null +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/GetDisplayPassword.java @@ -0,0 +1,58 @@ +/* + * 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.manager.events; + +import java.util.Optional; +import org.jgrapes.core.Event; + +/** + * Gets the current display secret. + */ +@SuppressWarnings("PMD.DataClass") +public class GetDisplayPassword extends Event { + + private final String vmName; + + /** + * Instantiates a new returns the display secret. + * + * @param vmName the vm name + */ + public GetDisplayPassword(String vmName) { + this.vmName = vmName; + } + + /** + * Gets the vm name. + * + * @return the vm name + */ + public String vmName() { + return vmName; + } + + /** + * Return the password. Should only be called when the event is completed. + * + * @return the optional + */ + public Optional password() { + return currentResults().stream().findFirst(); + } +} diff --git a/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ServiceChanged.java b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ServiceChanged.java new file mode 100644 index 0000000..a8008e0 --- /dev/null +++ b/org.jdrupes.vmoperator.manager.events/src/org/jdrupes/vmoperator/manager/events/ServiceChanged.java @@ -0,0 +1,76 @@ +/* + * 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.manager.events; + +import io.kubernetes.client.openapi.models.V1Service; +import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; +import org.jgrapes.core.Channel; +import org.jgrapes.core.Components; +import org.jgrapes.core.Event; + +/** + * Indicates that a service has changed. + */ +@SuppressWarnings("PMD.DataClass") +public class ServiceChanged extends Event { + + private final ResponseType type; + private final V1Service service; + + /** + * Initializes a new service changed event. + * + * @param type the type + * @param service the service + */ + public ServiceChanged(ResponseType type, V1Service service) { + this.type = type; + this.service = service; + } + + /** + * Returns the type. + * + * @return the type + */ + public ResponseType type() { + return type; + } + + /** + * Gets the service. + * + * @return the service + */ + public V1Service service() { + return service; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(Components.objectName(this)).append(" [") + .append(service.getMetadata().getName()).append(' ').append(type); + if (channels() != null) { + builder.append(", channels=").append(Channel.toString(channels())); + } + builder.append(']'); + return builder.toString(); + } +} 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 d4a1aa9..5e93790 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 @@ -83,8 +83,7 @@ public class VmDefChanged extends Event { builder.append(Components.objectName(this)).append(" [") .append(vmDef.getMetadata().getName()).append(' ').append(type); if (channels() != null) { - builder.append(", channels="); - builder.append(Channel.toString(channels())); + builder.append(", channels=").append(Channel.toString(channels())); } builder.append(']'); return builder.toString(); diff --git a/org.jdrupes.vmoperator.manager/build.gradle b/org.jdrupes.vmoperator.manager/build.gradle index 3018d0a..7dc07a2 100644 --- a/org.jdrupes.vmoperator.manager/build.gradle +++ b/org.jdrupes.vmoperator.manager/build.gradle @@ -32,6 +32,7 @@ dependencies { runtimeOnly 'org.apache.logging.log4j:log4j-to-jul:2.20.0' runtimeOnly project(':org.jdrupes.vmoperator.vmconlet') + runtimeOnly project(':org.jdrupes.vmoperator.vmviewer') } application { diff --git a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml index 451a465..7679a68 100644 --- a/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml +++ b/org.jdrupes.vmoperator.manager/resources/org/jdrupes/vmoperator/manager/runnerConfig.ftl.yaml @@ -202,7 +202,7 @@ data: ticket: "${ cr.spec.vm.display.spice.ticket.asString }" <#if cr.spec.vm.display.spice.streamingVideo??> - ticket: "${ cr.spec.vm.display.spice.streamingVideo.asString }" + streaming-video: "${ cr.spec.vm.display.spice.streamingVideo.asString }" usbRedirects: ${ cr.spec.vm.display.spice.usbRedirects.asInt?c } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java index 0fbb3a7..0c5f0cd 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ConfigMapReconciler.java @@ -111,7 +111,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; .list(newCm.getMetadata().getNamespace(), listOpts).getObject(); // If the VM is being created, the pod may not exist yet. - if (pods == null || pods.getItems().size() == 0) { + if (pods == null || pods.getItems().isEmpty()) { return; } var pod = pods.getItems().get(0); diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java index 17e74bf..a7b84a3 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/Constants.java @@ -21,6 +21,7 @@ package org.jdrupes.vmoperator.manager; /** * Some constants. */ +@SuppressWarnings("PMD.DataClass") public class Constants extends org.jdrupes.vmoperator.common.Constants { /** The Constant COMP_DISPLAY_SECRET. */ 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 cf47f9c..89b5eac 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 @@ -85,6 +85,7 @@ public class Controller extends Component { /** * Creates a new instance. */ + @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") public Controller(Channel componentChannel) { super(componentChannel); // Prepare component tree @@ -100,8 +101,11 @@ public class Controller extends Component { } }); attach(new VmMonitor(channel()).channelManager(chanMgr)); - attach(new DisplaySecretsMonitor(channel()) + attach(new DisplayPasswordMonitor(channel()) .channelManager(chanMgr.fixed())); + // Currently, we don't use the IP assigned by the load balancer + // to access the VM's console. Might change in the future. + // attach(new ServiceMonitor(channel()).channelManager(chanMgr)); attach(new Reconciler(channel())); } diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplayPasswordMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplayPasswordMonitor.java new file mode 100644 index 0000000..9959aec --- /dev/null +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplayPasswordMonitor.java @@ -0,0 +1,102 @@ +/* + * 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.manager; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.models.V1Secret; +import io.kubernetes.client.openapi.models.V1SecretList; +import io.kubernetes.client.util.Watch.Response; +import io.kubernetes.client.util.generic.options.ListOptions; +import java.io.IOException; +import static org.jdrupes.vmoperator.common.Constants.APP_NAME; +import org.jdrupes.vmoperator.common.K8sClient; +import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; +import org.jdrupes.vmoperator.common.K8sV1SecretStub; +import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET; +import org.jdrupes.vmoperator.manager.events.DisplayPasswordChanged; +import org.jdrupes.vmoperator.manager.events.GetDisplayPassword; +import org.jdrupes.vmoperator.manager.events.VmChannel; +import org.jgrapes.core.Channel; +import org.jgrapes.core.annotation.Handler; + +/** + * Watches for changes of display secrets. + */ +@SuppressWarnings("PMD.DataflowAnomalyAnalysis") +public class DisplayPasswordMonitor + extends AbstractMonitor { + + /** + * Instantiates a new display secrets monitor. + * + * @param componentChannel the component channel + */ + public DisplayPasswordMonitor(Channel componentChannel) { + super(componentChannel, V1Secret.class, V1SecretList.class); + context(K8sV1SecretStub.CONTEXT); + ListOptions options = new ListOptions(); + options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," + + "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET); + options(options); + } + + @Override + protected void prepareMonitoring() throws IOException, ApiException { + client(new K8sClient()); + } + + @Override + protected void handleChange(K8sClient client, Response change) { + String vmName = change.object.getMetadata().getLabels() + .get("app.kubernetes.io/instance"); + if (vmName == null) { + return; + } + var channel = channel(vmName).orElse(null); + if (channel == null || channel.vmDefinition() == null) { + return; + } + channel.pipeline().fire(new DisplayPasswordChanged( + ResponseType.valueOf(change.type), change.object), channel); + } + + /** + * On get display secrets. + * + * @param event the event + * @param channel the channel + * @throws ApiException the api exception + */ + @Handler + @SuppressWarnings("PMD.StringInstantiation") + public void onGetDisplaySecrets(GetDisplayPassword event, VmChannel channel) + throws ApiException { + ListOptions options = new ListOptions(); + options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," + + "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET + "," + + "app.kubernetes.io/instance=" + event.vmName()); + var stubs = K8sV1SecretStub.list(client(), namespace(), options); + if (stubs.isEmpty()) { + return; + } + stubs.iterator().next().model().map(m -> m.getData()) + .map(m -> m.get("display-password")) + .ifPresent(p -> event.setResult(new String(p))); + } +} 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 e0fdca0..5ba9dc5 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 @@ -224,6 +224,7 @@ public class Reconciler extends Component { return new DynamicKubernetesObject(json); } + @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") private void adjustCdRomPaths(JsonObject json) { var disks = GsonPtr.to(json).to("spec", "vm", "disks").get(JsonArray.class); diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretsMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ServiceMonitor.java similarity index 71% rename from org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretsMonitor.java rename to org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ServiceMonitor.java index de51080..bd5635e 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/DisplaySecretsMonitor.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/ServiceMonitor.java @@ -19,38 +19,36 @@ package org.jdrupes.vmoperator.manager; import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.models.V1Secret; -import io.kubernetes.client.openapi.models.V1SecretList; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceList; import io.kubernetes.client.util.Watch.Response; import io.kubernetes.client.util.generic.options.ListOptions; import java.io.IOException; import static org.jdrupes.vmoperator.common.Constants.APP_NAME; import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; -import org.jdrupes.vmoperator.common.K8sV1SecretStub; -import static org.jdrupes.vmoperator.manager.Constants.COMP_DISPLAY_SECRET; -import org.jdrupes.vmoperator.manager.events.DisplaySecretChanged; +import org.jdrupes.vmoperator.common.K8sV1ServiceStub; +import org.jdrupes.vmoperator.manager.events.ServiceChanged; import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jgrapes.core.Channel; /** - * Watches for changes of display secrets. + * Watches for changes of services. */ @SuppressWarnings("PMD.DataflowAnomalyAnalysis") -public class DisplaySecretsMonitor - extends AbstractMonitor { +public class ServiceMonitor + extends AbstractMonitor { /** * Instantiates a new display secrets monitor. * * @param componentChannel the component channel */ - public DisplaySecretsMonitor(Channel componentChannel) { - super(componentChannel, V1Secret.class, V1SecretList.class); - context(K8sV1SecretStub.CONTEXT); + public ServiceMonitor(Channel componentChannel) { + super(componentChannel, V1Service.class, V1ServiceList.class); + context(K8sV1ServiceStub.CONTEXT); ListOptions options = new ListOptions(); - options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME + "," - + "app.kubernetes.io/component=" + COMP_DISPLAY_SECRET); + options.setLabelSelector("app.kubernetes.io/name=" + APP_NAME); options(options); } @@ -60,7 +58,7 @@ public class DisplaySecretsMonitor } @Override - protected void handleChange(K8sClient client, Response change) { + protected void handleChange(K8sClient client, Response change) { String vmName = change.object.getMetadata().getLabels() .get("app.kubernetes.io/instance"); if (vmName == null) { @@ -70,8 +68,7 @@ public class DisplaySecretsMonitor if (channel == null || channel.vmDefinition() == null) { return; } - channel.pipeline().fire(new DisplaySecretChanged( + channel.pipeline().fire(new ServiceChanged( ResponseType.valueOf(change.type), change.object), channel); } - } 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 8812a93..baf833c 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 @@ -82,14 +82,15 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; // or not running. var stsStub = K8sV1StatefulSetStub.get(channel.client(), metadata.getNamespace(), metadata.getName()); - stsStub.model().ifPresent(sts -> { - var current = sts.getSpec().getReplicas(); + var stsModel = stsStub.model().orElse(null); + if (stsModel != null) { + var current = stsModel.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(); diff --git a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java index b12dc48..7027808 100644 --- a/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java +++ b/org.jdrupes.vmoperator.manager/src/org/jdrupes/vmoperator/manager/VmMonitor.java @@ -150,6 +150,7 @@ public class VmMonitor private void addDynamicData(K8sClient client, K8sDynamicModel vmState) { var rootNode = GsonPtr.to(vmState.data()).get(JsonObject.class); rootNode.addProperty("nodeName", ""); + rootNode.addProperty("nodeAddress", ""); // VM definition status changes before the pod terminates. // This results in pod information being shown for a stopped @@ -172,8 +173,17 @@ public class VmMonitor var podList = K8sV1PodStub.list(client, namespace(), podSearch); for (var podStub : podList) { - rootNode.addProperty("nodeName", - podStub.model().get().getSpec().getNodeName()); + var nodeName = podStub.model().get().getSpec().getNodeName(); + rootNode.addProperty("nodeName", nodeName); + logger.fine(() -> "Added node name " + nodeName + + " to VM info for " + vmState.getMetadata().getName()); + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + var addrs = new JsonArray(); + podStub.model().get().getStatus().getPodIPs().stream() + .map(ip -> ip.getIp()).forEach(addrs::add); + rootNode.add("nodeAddresses", addrs); + logger.fine(() -> "Added node addresses " + addrs + + " to VM info for " + vmState.getMetadata().getName()); } } catch (ApiException e) { logger.log(Level.WARNING, e, diff --git a/org.jdrupes.vmoperator.runner.qemu/password-expiry b/org.jdrupes.vmoperator.runner.qemu/password-expiry new file mode 100644 index 0000000..c42fe6e --- /dev/null +++ b/org.jdrupes.vmoperator.runner.qemu/password-expiry @@ -0,0 +1 @@ ++30 \ No newline at end of file diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java index b37d2c5..d6d5219 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Configuration.java @@ -108,7 +108,7 @@ public class Configuration implements Dto { * Subsection "vm". */ @SuppressWarnings({ "PMD.ShortClassName", "PMD.TooManyFields", - "PMD.DataClass" }) + "PMD.DataClass", "PMD.AvoidDuplicateLiterals" }) public static class Vm implements Dto { /** The name. */ @@ -196,6 +196,7 @@ public class Configuration implements Dto { /** * Subsection "network". */ + @SuppressWarnings("PMD.DataClass") public static class Network implements Dto { /** The type. */ @@ -217,6 +218,7 @@ public class Configuration implements Dto { /** * Subsection "drive". */ + @SuppressWarnings("PMD.DataClass") public static class Drive implements Dto { /** The type. */ @@ -247,6 +249,7 @@ public class Configuration implements Dto { /** * Subsection "spice". */ + @SuppressWarnings("PMD.DataClass") public static class Spice implements Dto { /** The port. */ diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java index 3071e42..857b14e 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/CpuController.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import org.jdrupes.vmoperator.runner.qemu.commands.QmpAddCpu; @@ -170,7 +171,7 @@ public class CpuController extends Component { private void checkCpus() { if (suspendedConfigure != null && desiredCpus != null - && currentCpus == desiredCpus.intValue()) { + && Objects.equals(currentCpus, desiredCpus)) { suspendedConfigure.resumeHandling(); suspendedConfigure = null; } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java index 882b85b..304ea04 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/DisplayController.java @@ -24,6 +24,7 @@ import java.nio.file.Path; import java.util.Objects; import java.util.logging.Level; import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetDisplayPassword; +import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetPasswordExpiry; import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu; import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand; import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.State; @@ -40,6 +41,7 @@ import org.jgrapes.util.events.WatchFile; public class DisplayController extends Component { public static final String DISPLAY_PASSWORD_FILE = "display-password"; + public static final String PASSWORD_EXPIRY_FILE = "password-expiry"; private String currentPassword; private String protocol; private final Path configDir; @@ -50,7 +52,8 @@ public class DisplayController extends Component { * @param componentChannel the component channel * @param configDir */ - @SuppressWarnings("PMD.AssignmentToNonFinalStatic") + @SuppressWarnings({ "PMD.AssignmentToNonFinalStatic", + "PMD.ConstructorCallsOverridableMethod" }) public DisplayController(Channel componentChannel, Path configDir) { super(componentChannel); this.configDir = configDir; @@ -90,7 +93,12 @@ public class DisplayController extends Component { if (protocol == null) { return; } + if (setDisplayPassword()) { + setPasswordExpiry(); + } + } + private boolean setDisplayPassword() { String password; Path dpPath = configDir.resolve(DISPLAY_PASSWORD_FILE); if (dpPath.toFile().canRead()) { @@ -100,18 +108,37 @@ public class DisplayController extends Component { } catch (IOException e) { logger.log(Level.WARNING, e, () -> "Cannot read display" + " password: " + e.getMessage()); - return; + return false; } } else { logger.finer(() -> "No display password"); - return; + return false; } if (Objects.equals(this.currentPassword, password)) { - return; + return false; } logger.fine(() -> "Updating display password"); fire(new MonitorCommand(new QmpSetDisplayPassword(protocol, password))); + return true; + } + + private void setPasswordExpiry() { + Path pePath = configDir.resolve(PASSWORD_EXPIRY_FILE); + if (!pePath.toFile().canRead()) { + return; + } + logger.finer(() -> "Found expiry time"); + String expiry; + try { + expiry = Files.readString(pePath); + } catch (IOException e) { + logger.log(Level.WARNING, e, () -> "Cannot read expiry" + + " time: " + e.getMessage()); + return; + } + logger.fine(() -> "Updating expiry time"); + fire(new MonitorCommand(new QmpSetPasswordExpiry(protocol, expiry))); } } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuMonitor.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuMonitor.java index 84a99a4..f59375c 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuMonitor.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/QemuMonitor.java @@ -90,7 +90,8 @@ public class QemuMonitor extends Component { * @param configDir the config dir * @throws IOException Signals that an I/O exception has occurred. */ - @SuppressWarnings("PMD.AssignmentToNonFinalStatic") + @SuppressWarnings({ "PMD.AssignmentToNonFinalStatic", + "PMD.ConstructorCallsOverridableMethod" }) public QemuMonitor(Channel componentChannel, Path configDir) throws IOException { super(componentChannel); @@ -155,6 +156,7 @@ public class QemuMonitor extends Component { * @param event the event * @param channel the channel */ + @SuppressWarnings("resource") @Handler public void onClientConnected(ClientConnected event, SocketIOChannel channel) { @@ -276,7 +278,7 @@ public class QemuMonitor extends Component { writer.append(asText).append('\n').flush(); } catch (IOException e) { // Cannot happen, but... - logger.log(Level.WARNING, e, () -> e.getMessage()); + logger.log(Level.WARNING, e, e::getMessage); } }); } diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java index 624d949..e0baa4f 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/Runner.java @@ -186,7 +186,8 @@ import org.jgrapes.util.events.WatchFile; * */ @SuppressWarnings({ "PMD.ExcessiveImports", "PMD.AvoidPrintStackTrace", - "PMD.DataflowAnomalyAnalysis", "PMD.TooManyMethods" }) + "PMD.DataflowAnomalyAnalysis", "PMD.TooManyMethods", + "PMD.CouplingBetweenObjects" }) public class Runner extends Component { private static final String QEMU = "qemu"; @@ -232,7 +233,8 @@ public class Runner extends Component { * @param cmdLine the cmd line * @throws IOException Signals that an I/O exception has occurred. */ - @SuppressWarnings("PMD.SystemPrintln") + @SuppressWarnings({ "PMD.SystemPrintln", + "PMD.ConstructorCallsOverridableMethod" }) public Runner(CommandLine cmdLine) throws IOException { yamlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); @@ -495,27 +497,27 @@ public class Runner extends Component { try { var cloudInitDir = config.dataDir.resolve("cloud-init"); cloudInitDir.toFile().mkdir(); - var metaOut - = Files.newBufferedWriter(cloudInitDir.resolve("meta-data")); - if (config.cloudInit.metaData != null) { - yamlMapper.writer().writeValue(metaOut, - config.cloudInit.metaData); + try (var metaOut + = Files.newBufferedWriter(cloudInitDir.resolve("meta-data"))) { + if (config.cloudInit.metaData != null) { + yamlMapper.writer().writeValue(metaOut, + config.cloudInit.metaData); + } } - metaOut.close(); - var userOut - = Files.newBufferedWriter(cloudInitDir.resolve("user-data")); - userOut.write("#cloud-config\n"); - if (config.cloudInit.userData != null) { - yamlMapper.writer().writeValue(userOut, - config.cloudInit.userData); + try (var userOut + = Files.newBufferedWriter(cloudInitDir.resolve("user-data"))) { + userOut.write("#cloud-config\n"); + if (config.cloudInit.userData != null) { + yamlMapper.writer().writeValue(userOut, + config.cloudInit.userData); + } } - userOut.close(); if (config.cloudInit.networkConfig != null) { - var networkConfig = Files.newBufferedWriter( - cloudInitDir.resolve("network-config")); - yamlMapper.writer().writeValue(networkConfig, - config.cloudInit.networkConfig); - networkConfig.close(); + try (var networkConfig = Files.newBufferedWriter( + cloudInitDir.resolve("network-config"))) { + yamlMapper.writer().writeValue(networkConfig, + config.cloudInit.networkConfig); + } } startProcess(cloudInitImgDefinition); } catch (IOException e) { @@ -545,7 +547,6 @@ public class Runner extends Component { && event.path().equals(config.swtpmSocket)) { // swtpm running, maybe start qemu mayBeStartQemu(QemuPreps.Tpm); - return; } } @@ -690,6 +691,7 @@ public class Runner extends Component { "The VM has been shut down")); } + @SuppressWarnings("PMD.ConfusingArgumentToVarargsMethod") private void shutdown() { if (!Set.of(State.TERMINATING, State.STOPPED).contains(state)) { fire(new Stop()); 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 bb15639..06ed64c 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 @@ -80,6 +80,7 @@ public class StatusUpdater extends Component { * * @param componentChannel the component channel */ + @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") public StatusUpdater(Channel componentChannel) { super(componentChannel); try { @@ -91,7 +92,6 @@ public class StatusUpdater extends Component { () -> "Cannot access events API, terminating."); fire(new Exit(1)); } - } /** @@ -179,6 +179,7 @@ public class StatusUpdater extends Component { * @throws ApiException */ @Handler + @SuppressWarnings("PMD.AvoidDuplicateLiterals") public void onConfigureQemu(ConfigureQemu event) throws ApiException { guestShutdownStops = event.configuration().guestShutdownStops; @@ -189,14 +190,22 @@ public class StatusUpdater extends Component { } // A change of the runner configuration is typically caused - // by a new version of the CR. So we observe the new CR. + // by a new version of the CR. So we update only if we have + // a new version of the CR. There's one exception: the display + // password is configured by a file, not by the CR. var vmDef = vmStub.model(); if (vmDef.isPresent() - && vmDef.get().metadata().getGeneration() == observedGeneration) { + && vmDef.get().metadata().getGeneration() == observedGeneration + && (event.configuration().hasDisplayPassword + || vmDef.get().status().getAsJsonPrimitive( + "displayPasswordSerial").getAsInt() == -1)) { return; } vmStub.updateStatus(vmDef.get(), from -> { JsonObject status = from.status(); + if (!event.configuration().hasDisplayPassword) { + status.addProperty("displayPasswordSerial", -1); + } status.getAsJsonArray("conditions").asList().stream() .map(cond -> (JsonObject) cond).filter(cond -> "Running" .equals(cond.get("type").getAsString())) @@ -213,7 +222,8 @@ public class StatusUpdater extends Component { * @throws ApiException */ @Handler - @SuppressWarnings("PMD.AssignmentInOperand") + @SuppressWarnings({ "PMD.AssignmentInOperand", + "PMD.AvoidLiteralsInIfCondition" }) public void onRunnerStateChanged(RunnerStateChange event) throws ApiException { K8sDynamicModel vmDef; diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpAddCpu.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpAddCpu.java index e77a984..86d92f6 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpAddCpu.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpAddCpu.java @@ -47,7 +47,7 @@ public class QmpAddCpu extends QmpCommand { cmd.put("execute", "device_add"); ObjectNode args = mapper.createObjectNode(); cmd.set("arguments", args); - args.setAll((ObjectNode) (unused.get("props"))); + args.setAll((ObjectNode) unused.get("props")); args.set("driver", unused.get("type")); args.put("id", cpuId); return cmd; diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetPasswordExpiry.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetPasswordExpiry.java new file mode 100644 index 0000000..672767d --- /dev/null +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/commands/QmpSetPasswordExpiry.java @@ -0,0 +1,66 @@ +/* + * VM-Operator + * Copyright (C) 2023 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.runner.qemu.commands; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; + +/** + * A {@link QmpCommand} that sets the password expiry. + */ +public class QmpSetPasswordExpiry extends QmpCommand { + + private final String protocol; + private final String expiry; + + /** + * Instantiates a new command. + * + * @param protocol the protocol + * @param expiry the expiry time + */ + public QmpSetPasswordExpiry(String protocol, String expiry) { + this.protocol = protocol; + this.expiry = expiry; + } + + @Override + public JsonNode toJson() { + ObjectNode cmd = mapper.createObjectNode(); + cmd.put("execute", "expire_password"); + ObjectNode args = mapper.createObjectNode(); + cmd.set("arguments", args); + args.set("protocol", new TextNode(protocol)); + args.set("time", new TextNode(expiry)); + return cmd; + } + + @Override + public String toString() { + try { + var json = toJson(); + return mapper.writeValueAsString(json); + } catch (JsonProcessingException e) { + return "(no string representation)"; + } + } + +} diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/HotpluggableCpuStatus.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/HotpluggableCpuStatus.java index 68641c9..2ab2c5a 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/HotpluggableCpuStatus.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/HotpluggableCpuStatus.java @@ -30,7 +30,9 @@ import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand; */ public class HotpluggableCpuStatus extends MonitorResult { + @SuppressWarnings("PMD.ImmutableField") private List usedCpus = new ArrayList<>(); + @SuppressWarnings("PMD.ImmutableField") private List unusedCpus = new ArrayList<>(); /** diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorCommand.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorCommand.java index 36d5b40..d2b5e8c 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorCommand.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorCommand.java @@ -55,8 +55,7 @@ public class MonitorCommand extends Event { builder.append(Components.objectName(this)) .append(" [").append(command); if (channels() != null) { - builder.append(", channels="); - builder.append(Channel.toString(channels())); + builder.append(", channels=").append(Channel.toString(channels())); } builder.append(']'); return builder.toString(); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorResult.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorResult.java index 9352ab2..6d7278c 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorResult.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/MonitorResult.java @@ -152,8 +152,7 @@ public class MonitorResult extends Event { builder.append(Components.objectName(this)) .append(" [").append(executed).append(", ").append(successful()); if (channels() != null) { - builder.append(", channels="); - builder.append(Channel.toString(channels())); + builder.append(", channels=").append(Channel.toString(channels())); } builder.append(']'); return builder.toString(); diff --git a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java index 5d5bffd..4a7ef08 100644 --- a/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java +++ b/org.jdrupes.vmoperator.runner.qemu/src/org/jdrupes/vmoperator/runner/qemu/events/RunnerStateChange.java @@ -109,15 +109,14 @@ public class RunnerStateChange extends Event { @Override public String toString() { - StringBuilder builder = new StringBuilder(); + StringBuilder builder = new StringBuilder(50); builder.append(Components.objectName(this)) .append(" [").append(state).append(": ").append(reason); if (failed) { builder.append(" (failed)"); } if (channels() != null) { - builder.append(", channels="); - builder.append(Channel.toString(channels())); + builder.append(", channels=").append(Channel.toString(channels())); } builder.append(']'); return builder.toString(); diff --git a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n.properties b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n.properties index 9a0efcf..880369b 100644 --- a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n.properties +++ b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n.properties @@ -1,4 +1,4 @@ -conletName = VM Viewer +conletName = VM Infos VMsSummary = VMs (running/total) diff --git a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_de.properties b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_de.properties index 1804b81..7e1d95e 100644 --- a/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_de.properties +++ b/org.jdrupes.vmoperator.vmconlet/resources/org/jdrupes/vmoperator/vmconlet/l10n_de.properties @@ -1,4 +1,4 @@ -conletName = VM Anzeige +conletName = VM-Informationen VMsSummary = VMs (gestartet/gesamt) 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 025ce3c..1882173 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 @@ -86,6 +86,7 @@ public class VmConlet extends FreeMarkerConlet { * on by default and that {@link Manager#fire(Event, Channel...)} * sends the event to */ + @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") public VmConlet(Channel componentChannel) { super(componentChannel); setPeriodicRefresh(Duration.ofMinutes(1), () -> new Update()); @@ -138,7 +139,7 @@ public class VmConlet extends FreeMarkerConlet { .setRenderAs( RenderMode.Preview.addModifiers(event.renderAs())) .setSupportedModes(MODES)); - renderedAs.add(RenderMode.View); + renderedAs.add(RenderMode.Preview); channel.respond(new NotifyConletView(type(), conletId, "summarySeries", summarySeries.entries())); var summary = evaluateSummary(false); @@ -181,7 +182,8 @@ public class VmConlet extends FreeMarkerConlet { */ @Handler(namedChannels = "manager") @SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity", - "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals" }) + "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals", + "PMD.ConfusingArgumentToVarargsMethod" }) public void onVmDefChanged(VmDefChanged event, VmChannel channel) throws JsonDecodeException, IOException { var vmName = event.vmDefinition().getMetadata().getName(); diff --git a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-style.scss b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-style.scss index 8f85eb6..3649bff 100644 --- a/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-style.scss +++ b/org.jdrupes.vmoperator.vmconlet/src/org/jdrupes/vmoperator/vmconlet/browser/VmConlet-style.scss @@ -97,7 +97,11 @@ .jdrupes-vmoperator-vmconlet-view-action-list { white-space: nowrap; - [role=button]:not(:last-child) { - margin-right: 0.5em; + [role=button] { + padding: 0.25rem; + + &:not([aria-disabled]):hover { + box-shadow: var(--darkening); + } } } diff --git a/org.jdrupes.vmoperator.vmviewer/.checkstyle b/org.jdrupes.vmoperator.vmviewer/.checkstyle new file mode 100644 index 0000000..7f2c604 --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/.checkstyle @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/org.jdrupes.vmoperator.vmviewer/.eclipse-pmd b/org.jdrupes.vmoperator.vmviewer/.eclipse-pmd new file mode 100644 index 0000000..5d69caa --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/.eclipse-pmd @@ -0,0 +1,7 @@ + + + + + + + diff --git a/org.jdrupes.vmoperator.vmviewer/.eslintignore b/org.jdrupes.vmoperator.vmviewer/.eslintignore new file mode 100644 index 0000000..139d3ee --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/.eslintignore @@ -0,0 +1 @@ +rollup.config.mjs diff --git a/org.jdrupes.vmoperator.vmviewer/.eslintrc.json b/org.jdrupes.vmoperator.vmviewer/.eslintrc.json new file mode 100644 index 0000000..e4f80f1 --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/.eslintrc.json @@ -0,0 +1,15 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { "project": ["./tsconfig.json"] }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "constructor-super": "off" + } +} + diff --git a/org.jdrupes.vmoperator.vmviewer/.gitignore b/org.jdrupes.vmoperator.vmviewer/.gitignore new file mode 100644 index 0000000..a53e74c --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/.gitignore @@ -0,0 +1,4 @@ +/bin/ +/bin_test/ +/generated/ +/build/ diff --git a/org.jdrupes.vmoperator.vmviewer/.settings/org.eclipse.buildship.core.prefs b/org.jdrupes.vmoperator.vmviewer/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..641c156 --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,10 @@ +build.commands=org.eclipse.jdt.core.javabuilder +connection.arguments= +connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) +connection.java.home=null +connection.jvm.arguments= +connection.project.dir=.. +derived.resources=.gradle,generated +eclipse.preferences.version=1 +natures=org.eclipse.jdt.groovy.core.groovyNature,org.eclipse.jdt.core.javanature +project.path=\:org.jgrapes.osgi.conlets.services diff --git a/org.jdrupes.vmoperator.vmviewer/.settings/org.eclipse.core.resources.prefs b/org.jdrupes.vmoperator.vmviewer/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..99f26c0 --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/org.jdrupes.vmoperator.vmviewer/.settings/org.eclipse.core.runtime.prefs b/org.jdrupes.vmoperator.vmviewer/.settings/org.eclipse.core.runtime.prefs new file mode 100644 index 0000000..5a0ad22 --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/.settings/org.eclipse.core.runtime.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +line.separator=\n diff --git a/org.jdrupes.vmoperator.vmviewer/.settings/org.eclipse.jdt.ui.prefs b/org.jdrupes.vmoperator.vmviewer/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 0000000..784d01f --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,63 @@ +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +formatter_profile=_JGrapes +formatter_settings_version=13 +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=true +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=false +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.convert_functional_interfaces=false +sp_cleanup.convert_to_enhanced_for_loop=false +sp_cleanup.correct_indentation=false +sp_cleanup.format_source_code=true +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.insert_inferred_type_arguments=false +sp_cleanup.make_local_variable_final=true +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=false +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.on_save_use_additional_actions=false +sp_cleanup.organize_imports=false +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=false +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_redundant_type_arguments=false +sp_cleanup.remove_trailing_whitespaces=false +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_casts=true +sp_cleanup.remove_unnecessary_nls_tags=false +sp_cleanup.remove_unused_imports=false +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.use_anonymous_class_creation=false +sp_cleanup.use_blocks=false +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_lambda=true +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_this_for_non_static_field_access=false +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true +sp_jautodoc.cleanup.add_header=false +sp_jautodoc.cleanup.replace_header=false diff --git a/org.jdrupes.vmoperator.vmviewer/build.gradle b/org.jdrupes.vmoperator.vmviewer/build.gradle new file mode 100644 index 0000000..ab667f5 --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/build.gradle @@ -0,0 +1,57 @@ +plugins { + id 'org.jdrupes.vmoperator.java-library-conventions' +} + +dependencies { + implementation project(':org.jdrupes.vmoperator.manager.events') + + implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.3.0,2)' + implementation 'org.jgrapes:org.jgrapes.webconsole.provider.vue:[1,2)' + implementation 'org.jgrapes:org.jgrapes.webconsole.provider.jgwcvuecomponents:[1.2,2)' + implementation 'org.jgrapes:org.jgrapes.webconsole.provider.chartjs:[1.2,2)' + +} + +apply plugin: 'com.github.node-gradle.node' + +node { + download = true +} + +task extractDependencies(type: Copy) { + from configurations.compileClasspath + .findAll{ it.name.contains('.provider.') + || it.name.contains('org.jgrapes.webconsole.base') + } + .collect{ zipTree (it) } + exclude '*.class' + into 'build/unpacked' + duplicatesStrategy 'include' + } + +task compileTs(type: NodeTask) { + dependsOn ':npmInstall' + dependsOn extractDependencies + inputs.dir project.file('src') + inputs.file project.file('tsconfig.json') + inputs.file project.file('rollup.config.mjs') + outputs.dir project.file('build/generated/resources') + script = file("${rootProject.rootDir}/node_modules/rollup/dist/bin/rollup") + args = ["-c"] +} + +sourceSets { + main { + resources { + srcDir project.file('build/generated/resources') + } + } +} + +processResources { + dependsOn compileTs +} + +eclipse { + autoBuildTasks compileTs +} diff --git a/org.jdrupes.vmoperator.vmviewer/package.json b/org.jdrupes.vmoperator.vmviewer/package.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/package.json @@ -0,0 +1 @@ +{} diff --git a/org.jdrupes.vmoperator.vmviewer/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory b/org.jdrupes.vmoperator.vmviewer/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory new file mode 100644 index 0000000..ebe4408 --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/resources/META-INF/services/org.jgrapes.webconsole.base.ConletComponentFactory @@ -0,0 +1 @@ +org.jdrupes.vmoperator.vmviewer.VmViewerFactory diff --git a/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/VmViewer-edit.ftl.html b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/VmViewer-edit.ftl.html new file mode 100644 index 0000000..d4e86ca --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/VmViewer-edit.ftl.html @@ -0,0 +1,20 @@ +
+
+
+ {{ localize("Select VM") }} +

+ +

+
+
+
diff --git a/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/VmViewer-l10nBundles.ftl.js b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/VmViewer-l10nBundles.ftl.js new file mode 100644 index 0000000..96928ef --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/VmViewer-l10nBundles.ftl.js @@ -0,0 +1,31 @@ +/* + * 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 . + */ + +"use strict"; + +const l10nBundles = new Map(); +let entries = null; +// <#list supportedLanguages() as l> +entries = new Map(); +l10nBundles.set("${l.locale.toLanguageTag()}", entries); +// <#list l.l10nBundle.keys as key> +entries.set("${key}", "${l.l10nBundle.getString(key)}"); +// +// + +export default l10nBundles; diff --git a/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/VmViewer-preview.ftl.html b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/VmViewer-preview.ftl.html new file mode 100644 index 0000000..1cd0392 --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/VmViewer-preview.ftl.html @@ -0,0 +1,6 @@ +
+
diff --git a/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/computer-off.svg b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/computer-off.svg new file mode 100644 index 0000000..27c11ae --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/computer-off.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + diff --git a/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/computer.svg b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/computer.svg new file mode 100644 index 0000000..f7a6b94 --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/computer.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + diff --git a/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/l10n.properties b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/l10n.properties new file mode 100644 index 0000000..96b0219 --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/l10n.properties @@ -0,0 +1,3 @@ +conletName = VM Console + +okayLabel = Apply and Close diff --git a/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/l10n_de.properties b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/l10n_de.properties new file mode 100644 index 0000000..e81a0fe --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/l10n_de.properties @@ -0,0 +1,7 @@ +conletName = VM-Konsole + +okayLabel = Anwenden und Schließen +Select\ VM = VM auswählen + +Start\ VM = VM Starten +Stop\ VM = VM Anhalten diff --git a/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/l10n_en.properties b/org.jdrupes.vmoperator.vmviewer/resources/org/jdrupes/vmoperator/vmviewer/l10n_en.properties new file mode 100644 index 0000000..e69de29 diff --git a/org.jdrupes.vmoperator.vmviewer/rollup.config.mjs b/org.jdrupes.vmoperator.vmviewer/rollup.config.mjs new file mode 100644 index 0000000..f00a51f --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/rollup.config.mjs @@ -0,0 +1,35 @@ +import typescript from 'rollup-plugin-typescript2'; +import postcss from 'rollup-plugin-postcss'; + +let packagePath = "org/jdrupes/vmoperator/vmviewer"; +let baseName = "VmViewer" +let module = "build/generated/resources/" + packagePath + + "/" + baseName + "-functions.js"; + +let pathsMap = { + "aash-plugin": "../../page-resource/aash-vue-components/lib/aash-vue-components.js", + "jgconsole": "../../console-base-resource/jgconsole.js", + "jgwc": "../../page-resource/jgwc-vue-components/jgwc-components.js", + "l10nBundles": "./" + baseName + "-l10nBundles.ftl.js", + "vue": "../../page-resource/vue/vue.esm-browser.js" +} + +export default { + external: ['aash-plugin', 'jgconsole', 'jgwc', 'l10nBundles', 'vue', 'chartjs'], + input: "src/" + packagePath + "/browser/" + baseName + "-functions.ts", + output: [ + { + format: "esm", + file: module, + sourcemap: true, + sourcemapPathTransform: (relativeSourcePath, _sourcemapPath) => { + return relativeSourcePath.replace(/^([^/]*\/){12}/, "./"); + }, + paths: pathsMap + } + ], + plugins: [ + typescript(), + postcss() + ] +}; diff --git a/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/VmViewer.java b/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/VmViewer.java new file mode 100644 index 0000000..b97ff6f --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/VmViewer.java @@ -0,0 +1,454 @@ +/* + * VM-Operator + * Copyright (C) 2023 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.vmviewer; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.gson.JsonPrimitive; +import freemarker.core.ParseException; +import freemarker.template.MalformedTemplateNameException; +import freemarker.template.Template; +import freemarker.template.TemplateNotFoundException; +import io.kubernetes.client.util.Strings; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Base64; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.logging.Level; +import org.jdrupes.json.JsonBeanDecoder; +import org.jdrupes.json.JsonDecodeException; +import org.jdrupes.vmoperator.common.K8sDynamicModel; +import org.jdrupes.vmoperator.common.K8sObserver; +import org.jdrupes.vmoperator.manager.events.ChannelCache; +import org.jdrupes.vmoperator.manager.events.GetDisplayPassword; +import org.jdrupes.vmoperator.manager.events.ModifyVm; +import org.jdrupes.vmoperator.manager.events.VmChannel; +import org.jdrupes.vmoperator.manager.events.VmDefChanged; +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.annotation.Handler; +import org.jgrapes.http.Session; +import org.jgrapes.util.events.ConfigurationUpdate; +import org.jgrapes.util.events.KeyValueStoreQuery; +import org.jgrapes.util.events.KeyValueStoreUpdate; +import org.jgrapes.webconsole.base.Conlet.RenderMode; +import org.jgrapes.webconsole.base.ConletBaseModel; +import org.jgrapes.webconsole.base.ConsoleConnection; +import org.jgrapes.webconsole.base.ConsoleUser; +import org.jgrapes.webconsole.base.WebConsoleUtils; +import org.jgrapes.webconsole.base.events.AddConletType; +import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource; +import org.jgrapes.webconsole.base.events.ConletDeleted; +import org.jgrapes.webconsole.base.events.ConsoleReady; +import org.jgrapes.webconsole.base.events.DeleteConlet; +import org.jgrapes.webconsole.base.events.NotifyConletModel; +import org.jgrapes.webconsole.base.events.NotifyConletView; +import org.jgrapes.webconsole.base.events.OpenModalDialog; +import org.jgrapes.webconsole.base.events.RenderConlet; +import org.jgrapes.webconsole.base.events.RenderConletRequestBase; +import org.jgrapes.webconsole.base.events.SetLocale; +import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet; + +/** + * The Class VmConlet. + */ +@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports", + "PMD.CouplingBetweenObjects" }) +public class VmViewer extends FreeMarkerConlet { + + private static final Set MODES = RenderMode.asSet( + RenderMode.Preview, RenderMode.Edit); + private final ChannelCache channelManager = new ChannelCache<>(); + private static ObjectMapper objectMapper + = new ObjectMapper().registerModule(new JavaTimeModule()); + private Class preferredIpVersion = Inet4Address.class; + + /** + * The periodically generated update event. + */ + public static class Update extends Event { + } + + /** + * Creates a new component with its channel set to the given channel. + * + * @param componentChannel the channel that the component's handlers listen + * on by default and that {@link Manager#fire(Event, Channel...)} + * sends the event to + */ + public VmViewer(Channel componentChannel) { + super(componentChannel); + } + + /** + * Configure the component. + * + * @param event the event + */ + @Handler + public void onConfigurationUpdate(ConfigurationUpdate event) { + event.structured(componentPath()).ifPresent(c -> { + @SuppressWarnings("unchecked") + var dispRes = (Map) c + .getOrDefault("displayResource", Collections.emptyMap()); + switch ((String) dispRes.getOrDefault("preferredIpVersion", "")) { + case "ipv6": + preferredIpVersion = Inet6Address.class; + break; + case "ipv4": + default: + preferredIpVersion = Inet4Address.class; + break; + } + }); + } + + /** + * On {@link ConsoleReady}, fire the {@link AddConletType}. + * + * @param event the event + * @param channel the channel + * @throws TemplateNotFoundException the template not found exception + * @throws MalformedTemplateNameException the malformed template name + * exception + * @throws ParseException the parse exception + * @throws IOException Signals that an I/O exception has occurred. + */ + @Handler + public void onConsoleReady(ConsoleReady event, ConsoleConnection channel) + throws TemplateNotFoundException, MalformedTemplateNameException, + ParseException, IOException { + // Add conlet resources to page + channel.respond(new AddConletType(type()) + .setDisplayNames( + localizations(channel.supportedLocales(), "conletName")) + .addRenderMode(RenderMode.Preview) + .addScript(new ScriptResource().setScriptType("module") + .setScriptUri(event.renderSupport().conletResource( + type(), "VmViewer-functions.js")))); + } + + private String storagePath(Session session, String conletId) { + return "/" + WebConsoleUtils.userFromSession(session) + .map(ConsoleUser::getName).orElse("") + + "/" + VmViewer.class.getName() + "/" + conletId; + } + + @Override + protected Optional createStateRepresentation(Event event, + ConsoleConnection connection, String conletId) throws Exception { + var model = new ViewerModel(conletId); + String jsonState = objectMapper.writeValueAsString(model); + connection.respond(new KeyValueStoreUpdate().update( + storagePath(connection.session(), model.getConletId()), jsonState)); + return Optional.of(model); + } + + @Override + @SuppressWarnings("PMD.EmptyCatchBlock") + protected Optional recreateState(Event event, + ConsoleConnection channel, String conletId) throws Exception { + KeyValueStoreQuery query = new KeyValueStoreQuery( + storagePath(channel.session(), conletId), channel); + newEventPipeline().fire(query, channel); + try { + if (!query.results().isEmpty()) { + var json = query.results().get(0).values().stream().findFirst() + .get(); + ViewerModel model + = objectMapper.readValue(json, ViewerModel.class); + return Optional.of(model); + } + } catch (InterruptedException e) { + // Means we have no result. + } + + // Fall back to creating default state. + return createStateRepresentation(event, channel, conletId); + } + + @Override + @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") + protected Set doRenderConlet(RenderConletRequestBase event, + ConsoleConnection channel, String conletId, ViewerModel conletState) + throws Exception { + ResourceBundle resourceBundle = resourceBundle(channel.locale()); + Set renderedAs = new HashSet<>(); + if (event.renderAs().contains(RenderMode.Preview)) { + Template tpl + = freemarkerConfig().getTemplate("VmViewer-preview.ftl.html"); + channel.respond(new RenderConlet(type(), conletId, + processTemplate(event, tpl, + fmModel(event, channel, conletId, conletState))) + .setRenderAs( + RenderMode.Preview.addModifiers(event.renderAs())) + .setSupportedModes(MODES)); + renderedAs.add(RenderMode.Preview); + if (!Strings.isNullOrEmpty(conletState.vmName())) { + updateConfig(channel, conletState); + } + } + if (event.renderAs().contains(RenderMode.Edit)) { + Template tpl = freemarkerConfig() + .getTemplate("VmViewer-edit.ftl.html"); + var fmModel = fmModel(event, channel, conletId, conletState); + fmModel.put("vmNames", + channelManager.keys().stream().sorted().toList()); + channel.respond(new OpenModalDialog(type(), conletId, + processTemplate(event, tpl, fmModel)) + .addOption("cancelable", true) + .addOption("okayLabel", + resourceBundle.getString("okayLabel"))); + } + return renderedAs; + } + + private void updateConfig(ConsoleConnection channel, ViewerModel model) { + channel.respond(new NotifyConletView(type(), + model.getConletId(), "updateConfig", model.vmName())); + updateVmDef(channel, model); + } + + private void updateVmDef(ConsoleConnection channel, ViewerModel model) { + if (Strings.isNullOrEmpty(model.vmName())) { + return; + } + channelManager.associated(model.vmName()).ifPresent(vmDef -> { + try { + var def = JsonBeanDecoder.create(vmDef.data().toString()) + .readObject(); + channel.respond(new NotifyConletView(type(), + model.getConletId(), "updateVmDefinition", def)); + } catch (JsonDecodeException e) { + logger.log(Level.SEVERE, e, + () -> "Failed to serialize VM definition"); + } + }); + } + + @Override + protected void doConletDeleted(ConletDeleted event, + ConsoleConnection channel, String conletId, ViewerModel conletState) + throws Exception { + if (event.renderModes().isEmpty()) { + channel.respond(new KeyValueStoreUpdate().delete( + storagePath(channel.session(), conletId))); + } + } + + /** + * Track the VM definitions. + * + * @param event the event + * @param channel the channel + * @throws JsonDecodeException the json decode exception + * @throws IOException + */ + @Handler(namedChannels = "manager") + @SuppressWarnings({ "PMD.ConfusingTernary", "PMD.CognitiveComplexity", + "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDuplicateLiterals", + "PMD.ConfusingArgumentToVarargsMethod" }) + public void onVmDefChanged(VmDefChanged event, VmChannel channel) + throws JsonDecodeException, IOException { + var vmDef = new K8sDynamicModel(channel.client().getJSON() + .getGson(), event.vmDefinition().data()); + var vmName = vmDef.getMetadata().getName(); + if (event.type() == K8sObserver.ResponseType.DELETED) { + channelManager.remove(vmName); + } else { + channelManager.put(vmName, channel, vmDef); + } + for (var entry : conletIdsByConsoleConnection().entrySet()) { + var connection = entry.getKey(); + for (var conletId : entry.getValue()) { + var model = stateFromSession(connection.session(), conletId); + if (model.isEmpty() || !model.get().vmName().equals(vmName)) { + continue; + } + if (event.type() == K8sObserver.ResponseType.DELETED) { + connection.respond( + new DeleteConlet(conletId, Collections.emptySet())); + } else { + updateVmDef(connection, model.get()); + } + } + } + } + + @Override + @SuppressWarnings({ "PMD.AvoidDecimalLiteralsInBigDecimalConstructor", + "PMD.ConfusingArgumentToVarargsMethod" }) + protected void doUpdateConletState(NotifyConletModel event, + ConsoleConnection channel, ViewerModel model) + throws Exception { + event.stop(); + var vmName = event.params().asString(0); + var vmChannel = channelManager.channel(vmName).orElse(null); + if (vmChannel == null) { + return; + } + switch (event.method()) { + case "selectedVm": + model.setVmName(event.params().asString(0)); + String jsonState = objectMapper.writeValueAsString(model); + channel.respond(new KeyValueStoreUpdate().update(storagePath( + channel.session(), model.getConletId()), jsonState)); + updateConfig(channel, model); + break; + case "start": + fire(new ModifyVm(vmName, "state", "Running", vmChannel)); + break; + case "stop": + fire(new ModifyVm(vmName, "state", "Stopped", vmChannel)); + break; + case "openConsole": + channelManager.channel(vmName).ifPresent( + vc -> fire(Event.onCompletion(new GetDisplayPassword(vmName), + ds -> openConsole(vmName, channel, model, ds)), vc)); + break; + default:// ignore + break; + } + } + + private void openConsole(String vmName, ConsoleConnection connection, + ViewerModel model, GetDisplayPassword pwQuery) { + var vmDef = channelManager.associated(vmName).orElse(null); + if (vmDef == null) { + return; + } + var addr = displayIp(vmDef); + if (addr.isEmpty()) { + logger.severe(() -> "Failed to find display IP for " + vmName); + return; + } + var port = GsonPtr.to(vmDef.data()).get(JsonPrimitive.class, "spec", + "vm", "display", "spice", "port"); + if (port.isEmpty()) { + logger.severe(() -> "No port defined for display of " + vmName); + return; + } + var proxyUrl = GsonPtr.to(vmDef.data()).get(JsonPrimitive.class, "spec", + "vm", "display", "spice", "proxyUrl"); + StringBuffer data = new StringBuffer(100) + .append("[virt-viewer]\ntype=spice\nhost=") + .append(addr.get().getHostAddress()).append("\nport=") + .append(Integer.toString(port.get().getAsInt())).append('\n'); + pwQuery.password().ifPresent(p -> { + data.append("password=").append(p).append('\n'); + }); + proxyUrl.map(JsonPrimitive::getAsString).ifPresent(u -> { + if (!Strings.isNullOrEmpty(u)) { + data.append("proxy=").append(u).append('\n'); + } + }); + connection.respond(new NotifyConletView(type(), + model.getConletId(), "openConsole", "application/x-virt-viewer", + Base64.getEncoder().encodeToString(data.toString().getBytes()))); + } + + private Optional displayIp(K8sDynamicModel vmDef) { + var server = GsonPtr.to(vmDef.data()).get(JsonPrimitive.class, "spec", + "vm", "display", "spice", "server"); + if (server.isPresent()) { + var srv = server.get().getAsString(); + try { + var addr = InetAddress.getByName(srv); + logger.fine(() -> "Using IP address from CRD for " + + vmDef.getMetadata().getName() + ": " + addr); + return Optional.of(addr); + } catch (UnknownHostException e) { + logger.log(Level.SEVERE, e, () -> "Invalid server address " + + srv + ": " + e.getMessage()); + return Optional.empty(); + } + } + var addrs = GsonPtr.to(vmDef.data()).getAsListOf(JsonPrimitive.class, + "nodeAddresses").stream().map(JsonPrimitive::getAsString) + .map(a -> { + try { + return InetAddress.getByName(a); + } catch (UnknownHostException e) { + logger.warning(() -> "Invalid IP address: " + a); + return null; + } + }).filter(a -> a != null).toList(); + logger.fine(() -> "Known IP addresses for " + + vmDef.getMetadata().getName() + ": " + addrs); + return addrs.stream() + .filter(a -> preferredIpVersion.isAssignableFrom(a.getClass())) + .findFirst().or(() -> addrs.stream().findFirst()); + } + + @Override + protected boolean doSetLocale(SetLocale event, ConsoleConnection channel, + String conletId) throws Exception { + return true; + } + + /** + * The Class VmsModel. + */ + public static class ViewerModel extends ConletBaseModel { + + private String vmName; + + /** + * Instantiates a new vms model. + * + * @param conletId the conlet id + */ + public ViewerModel(@JsonProperty("conletId") String conletId) { + super(conletId); + } + + /** + * Gets the vm name. + * + * @return the vmName + */ + @JsonGetter("vmName") + public String vmName() { + return vmName; + } + + /** + * Sets the vm name. + * + * @param vmName the vmName to set + */ + public void setVmName(String vmName) { + this.vmName = vmName; + } + + } +} diff --git a/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/VmViewerFactory.java b/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/VmViewerFactory.java new file mode 100644 index 0000000..6748f47 --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/VmViewerFactory.java @@ -0,0 +1,54 @@ +/* + * VM-Operator + * Copyright (C) 2023 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.vmviewer; + +import java.util.Map; +import java.util.Optional; +import org.jgrapes.core.Channel; +import org.jgrapes.core.ComponentType; +import org.jgrapes.webconsole.base.ConletComponentFactory; + +/** + * The factory service for {@link VmViewer}s. + */ +public class VmViewerFactory implements ConletComponentFactory { + + /* + * (non-Javadoc) + * + * @see org.jgrapes.core.ComponentFactory#componentType() + */ + @Override + public Class componentType() { + return VmViewer.class; + } + + /* + * (non-Javadoc) + * + * @see org.jgrapes.core.ComponentFactory#create(org.jgrapes.core.Channel, + * java.util.Map) + */ + @Override + public Optional create(Channel componentChannel, + Map properties) { + return Optional.of(new VmViewer(componentChannel)); + } + +} diff --git a/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/browser/VmViewer-functions.ts b/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/browser/VmViewer-functions.ts new file mode 100644 index 0000000..2c66aeb --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/browser/VmViewer-functions.ts @@ -0,0 +1,206 @@ +/* + * 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 . + */ + +import { + reactive, ref, createApp, computed, watch +} from "vue"; +import JGConsole from "jgconsole"; +import JgwcPlugin, { JGWC } from "jgwc"; +import { provideApi, getApi } from "aash-plugin"; +import l10nBundles from "l10nBundles"; + +import "./VmViewer-style.scss"; + +// For global access +declare global { + interface Window { + orgJDrupesVmOperatorVmViewer: { + initPreview?: (previewDom: HTMLElement, isUpdate: boolean) => void, + initEdit?: (viewDom: HTMLElement, isUpdate: boolean) => void + applyEdit?: (viewDom: HTMLElement, apply: boolean) => void + } + } +} + +window.orgJDrupesVmOperatorVmViewer = {}; + +interface Api { + /* eslint-disable @typescript-eslint/no-explicit-any */ + vmName: string; + vmDefinition: any; +} + +const localize = (key: string) => { + return JGConsole.localize( + l10nBundles, JGWC.lang(), key); +}; + +window.orgJDrupesVmOperatorVmViewer.initPreview = (previewDom: HTMLElement, + _isUpdate: boolean) => { + const app = createApp({ + setup(_props: object) { + const conletId = (previewDom.closest( + "[data-conlet-id]")!).dataset["conletId"]!; + const resourceBase = (previewDom.closest( + "*[data-conlet-resource-base]")!).dataset.conletResourceBase; + + const previewApi: Api = reactive({ + vmName: "", + vmDefinition: {} + }); + const vmDef = computed(() => previewApi.vmDefinition); + + watch(() => previewApi.vmName, (name: string) => { + if (name !== "") { + JGConsole.instance.updateConletTitle(conletId, name); + } + }); + + provideApi(previewDom, previewApi); + + const vmAction = (vmName: string, action: string) => { + JGConsole.notifyConletModel(conletId, action, vmName); + }; + + return { localize, resourceBase, vmDef, vmAction }; + }, + template: ` + + + + + + + + +
+ + + + + +
` + }); + app.use(JgwcPlugin, []); + app.config.globalProperties.window = window; + app.mount(previewDom); +}; + +JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmviewer.VmViewer", + "updateConfig", function(conletId: string, vmName: string) { + const conlet = JGConsole.findConletPreview(conletId); + if (!conlet) { + return; + } + const api = getApi(conlet.element().querySelector( + ":scope .jdrupes-vmoperator-vmviewer-preview"))!; + api.vmName = vmName; + }); + +JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmviewer.VmViewer", + "updateVmDefinition", function(conletId: string, vmDefinition: any) { + const conlet = JGConsole.findConletPreview(conletId); + if (!conlet) { + return; + } + const api = getApi(conlet.element().querySelector( + ":scope .jdrupes-vmoperator-vmviewer-preview"))!; + // Add some short-cuts for rendering + vmDefinition.name = vmDefinition.metadata.name; + vmDefinition.currentCpus = vmDefinition.status.cpus; + vmDefinition.currentRam = Number(vmDefinition.status.ram); + for (const condition of vmDefinition.status.conditions) { + if (condition.type === "Running") { + vmDefinition.running = condition.status === "True"; + vmDefinition.runningConditionSince + = new Date(condition.lastTransitionTime); + break; + } + } + api.vmDefinition = vmDefinition; + }); + +JGConsole.registerConletFunction("org.jdrupes.vmoperator.vmviewer.VmViewer", + "openConsole", function(_conletId: string, mimeType: string, data: string) { + let target = document.getElementById( + "org.jdrupes.vmoperator.vmviewer.VmViewer.target"); + if (!target) { + target = document.createElement("iframe"); + target.id = "org.jdrupes.vmoperator.vmviewer.VmViewer.target"; + target.setAttribute("name", target.id); + target.setAttribute("style", "display: none;"); + document.querySelector("body")!.append(target); + } + const url = "data:" + mimeType + ";base64," + data; + window.open(url, target.id); + }); + +window.orgJDrupesVmOperatorVmViewer.initEdit = (dialogDom: HTMLElement, + isUpdate: boolean) => { + if (isUpdate) { + return; + } + const app = createApp({ + setup() { + const formId = (dialogDom + .closest("*[data-conlet-id]")!).id + "-form"; + + const localize = (key: string) => { + return JGConsole.localize( + l10nBundles, JGWC.lang()!, key); + }; + + const vmNameInput = ref(""); + const conletId = (dialogDom.closest( + "[data-conlet-id]")!).dataset["conletId"]!; + const conlet = JGConsole.findConletPreview(conletId); + if (conlet) { + const api = getApi(conlet.element().querySelector( + ":scope .jdrupes-vmoperator-vmviewer-preview"))!; + vmNameInput.value = api.vmName; + } + + provideApi(dialogDom, vmNameInput); + + return { formId, localize, vmNameInput }; + } + }); + app.use(JgwcPlugin); + app.mount(dialogDom); +} + +window.orgJDrupesVmOperatorVmViewer.applyEdit = + (dialogDom: HTMLElement, apply: boolean) => { + if (!apply) { + return; + } + const conletId = (dialogDom.closest("[data-conlet-id]")!) + .dataset["conletId"]!; + const vmName = getApi>(dialogDom!)!.value; + JGConsole.notifyConletModel(conletId, "selectedVm", vmName); +} diff --git a/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/browser/VmViewer-style.scss b/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/browser/VmViewer-style.scss new file mode 100644 index 0000000..8508ba5 --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/browser/VmViewer-style.scss @@ -0,0 +1,42 @@ +/* + * VM-Operator + * Copyright (C) 2023 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 . + */ + +/* + * Conlet specific styles. + */ +.jdrupes-vmoperator-vmviewer-preview img { + height: 3em; + padding: 0.25rem; + + &:hover { + box-shadow: var(--darkening); + } +} + +.jdrupes-vmoperator-vmviewer-preview-action-list { + white-space: nowrap; + + [role=button] { + padding: 0.25rem; + + &:not([aria-disabled]):hover { + box-shadow: var(--darkening); + } + } +} + diff --git a/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/browser/l10nBundles-stub.d.ts b/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/browser/l10nBundles-stub.d.ts new file mode 100644 index 0000000..8ca03f3 --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/browser/l10nBundles-stub.d.ts @@ -0,0 +1 @@ +export default new Map>(); diff --git a/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/package-info.java b/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/package-info.java new file mode 100644 index 0000000..9a4045a --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/src/org/jdrupes/vmoperator/vmviewer/package-info.java @@ -0,0 +1,19 @@ +/* + * VM-Operator + * Copyright (C) 2023 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.vmviewer; \ No newline at end of file diff --git a/org.jdrupes.vmoperator.vmviewer/tsconfig.json b/org.jdrupes.vmoperator.vmviewer/tsconfig.json new file mode 100644 index 0000000..6418f59 --- /dev/null +++ b/org.jdrupes.vmoperator.vmviewer/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "es2015", + "module": "es2015", + "sourceMap": true, + "inlineSources": true, + "declaration": true, + "importHelpers": true, + "strict": true, + "moduleResolution": "node", + "experimentalDecorators": true, + "lib": ["DOM", "ES2020"], + "paths": { + "aash-plugin": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/aash-vue-components/lib/AashPlugin"], + "jgconsole": ["./build/unpacked/org/jgrapes/webconsole/base/JGConsole"], + "jgwc": ["./build/unpacked/org/jgrapes/webconsole/provider/jgwcvuecomponents/jgwc-vue-components/jgwc-components"], + "l10nBundles": ["./src/org/jdrupes/vmoperator/vmviewer/browser/l10nBundles-stub"], + "vue": ["./build/unpacked/org/jgrapes/webconsole/provider/vue/vue/vue"] + } + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "l10nBundles-stub.ts"] +} diff --git a/settings.gradle b/settings.gradle index cf075af..64f3056 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,6 +13,7 @@ rootProject.name = 'VM-Operator' include 'org.jdrupes.vmoperator.manager' include 'org.jdrupes.vmoperator.manager.events' include 'org.jdrupes.vmoperator.vmconlet' +include 'org.jdrupes.vmoperator.vmviewer' include 'org.jdrupes.vmoperator.runner.qemu' include 'org.jdrupes.vmoperator.common' include 'org.jdrupes.vmoperator.util'