From 92ed4b90cb8f8875dd704f492bf93370138aeb87 Mon Sep 17 00:00:00 2001 From: Lucas Koehler Date: Wed, 2 Aug 2023 17:54:00 +0200 Subject: [PATCH] #200: Initial crash loop prevention for custom resources (#216) NOTE: Status handling for app definitions is commented out as there are still some issues - Catch all exceptions when adding session, workspace, appdefinition resources and add error state - Extend the resource clients for app definitions, sessions, and workspaces to be able to handle the status subresource. - Add basic status for our CRDs to track handling state of the operator - Prevent crashloops by never handling resources that are in error or handling state. The latter indicates an unexcepted crash during handling (e.g. NPE). - NOTE: The previous step was only dony for LAZY handlers. EAGER handlers did not change - Exemplary sub steps for volume claim an attachment for workspace resources - Increase resource versions for all CRs - Move API, KIND, and CRD_NAME constants from Spec to resource classes (e.g. from SessionSpec to Session) because they belong to the resource itself. This became apparent when adding the Status to the resources Part of #200 --- .../client/AppDefinitionResourceClient.java | 7 +- .../k8s/client/CustomResourceClient.java | 29 +++-- .../DefaultAppDefinitionResourceClient.java | 6 ++ .../client/DefaultSessionResourceClient.java | 6 ++ .../DefaultWorkspaceResourceClient.java | 8 +- .../common/k8s/client/ResourceClient.java | 9 ++ .../k8s/client/SessionResourceClient.java | 6 +- .../k8s/client/WorkspaceResourceClient.java | 6 +- .../common/k8s/resource/AppDefinition.java | 7 +- .../k8s/resource/AppDefinitionSpec.java | 4 - .../k8s/resource/AppDefinitionStatus.java | 26 +++++ .../common/k8s/resource/OperatorStatus.java | 37 +++++++ .../common/k8s/resource/ResourceStatus.java | 44 ++++++++ .../cloud/common/k8s/resource/Session.java | 9 +- .../common/k8s/resource/SessionSpec.java | 4 - .../common/k8s/resource/SessionStatus.java | 26 +++++ .../cloud/common/k8s/resource/StatusStep.java | 59 ++++++++++ .../cloud/common/k8s/resource/Workspace.java | 10 +- .../common/k8s/resource/WorkspaceSpec.java | 4 - .../common/k8s/resource/WorkspaceStatus.java | 45 ++++++++ .../cloud/common/util/CustomResourceUtil.java | 11 +- .../di/AbstractTheiaCloudOperatorModule.java | 12 +-- .../EagerStartAppDefinitionAddedHandler.java | 8 +- .../handler/impl/LazySessionHandler.java | 101 ++++++++++++++++-- .../impl/LazyStartAppDefinitionHandler.java | 47 +++++++- .../handler/impl/LazyWorkspaceHandler.java | 65 ++++++++++- .../handler/util/TheiaCloudHandlerUtil.java | 3 +- .../handler/util/TheiaCloudIngressUtil.java | 3 +- 28 files changed, 533 insertions(+), 69 deletions(-) create mode 100644 java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/AppDefinitionStatus.java create mode 100644 java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/OperatorStatus.java create mode 100644 java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/ResourceStatus.java create mode 100644 java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/SessionStatus.java create mode 100644 java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/StatusStep.java create mode 100644 java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/WorkspaceStatus.java diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/AppDefinitionResourceClient.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/AppDefinitionResourceClient.java index a8bd136d..1b538cc8 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/AppDefinitionResourceClient.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/AppDefinitionResourceClient.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2022 EclipseSource and others. + * Copyright (C) 2022-2023 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -18,7 +18,8 @@ import org.eclipse.theia.cloud.common.k8s.resource.AppDefinition; import org.eclipse.theia.cloud.common.k8s.resource.AppDefinitionSpec; import org.eclipse.theia.cloud.common.k8s.resource.AppDefinitionSpecResourceList; +import org.eclipse.theia.cloud.common.k8s.resource.AppDefinitionStatus; -public interface AppDefinitionResourceClient - extends CustomResourceClient { +public interface AppDefinitionResourceClient extends + CustomResourceClient { } diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/CustomResourceClient.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/CustomResourceClient.java index 10005965..a376a1f8 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/CustomResourceClient.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/CustomResourceClient.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2022 EclipseSource and others. + * Copyright (C) 2022-2023 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -18,6 +18,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.eclipse.theia.cloud.common.k8s.resource.UserScopedSpec; @@ -25,25 +26,41 @@ import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.CustomResource; -public interface CustomResourceClient, L extends KubernetesResourceList> +public interface CustomResourceClient, L extends KubernetesResourceList> extends ResourceClient { - T create(String correlationId, S spec); + T create(String correlationId, SPEC spec); - default Optional spec(String name) { + default Optional spec(String name) { return get(name).map(T::getSpec); } + default Optional status(String name) { + return get(name).map(T::getStatus); + } + default List list(String user) { return list().stream().filter(item -> Objects.equals(UserScopedSpec.getUser(item.getSpec()), user)) .collect(Collectors.toList()); } - default List specs() { + default List specs() { return list().stream().map(item -> item.getSpec()).collect(Collectors.toList()); } - default List specs(String user) { + default List specs(String user) { return list(user).stream().map(item -> item.getSpec()).collect(Collectors.toList()); } + + default boolean updateStatus(String correlationId, T resource, Consumer editOperation) { + trace(correlationId, "Update Status of " + resource); + final String name = resource.getMetadata().getName(); + return (editStatus(correlationId, name, res -> { + STATUS status = Optional.ofNullable(res.getStatus()).orElse(createDefaultStatus()); + res.setStatus(status); + editOperation.accept(status); + }) != null); + } + + STATUS createDefaultStatus(); } diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultAppDefinitionResourceClient.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultAppDefinitionResourceClient.java index 2ee6a31c..2494b51a 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultAppDefinitionResourceClient.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultAppDefinitionResourceClient.java @@ -18,6 +18,7 @@ import org.eclipse.theia.cloud.common.k8s.resource.AppDefinition; import org.eclipse.theia.cloud.common.k8s.resource.AppDefinitionSpec; import org.eclipse.theia.cloud.common.k8s.resource.AppDefinitionSpecResourceList; +import org.eclipse.theia.cloud.common.k8s.resource.AppDefinitionStatus; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.client.NamespacedKubernetesClient; @@ -42,4 +43,9 @@ public AppDefinition create(String correlationId, AppDefinitionSpec spec) { return operation().create(appDefinition); } + @Override + public AppDefinitionStatus createDefaultStatus() { + return new AppDefinitionStatus(); + } + } diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultSessionResourceClient.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultSessionResourceClient.java index ddae5e24..ff0f28c6 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultSessionResourceClient.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultSessionResourceClient.java @@ -22,6 +22,7 @@ import org.eclipse.theia.cloud.common.k8s.resource.Session; import org.eclipse.theia.cloud.common.k8s.resource.SessionSpec; import org.eclipse.theia.cloud.common.k8s.resource.SessionSpecResourceList; +import org.eclipse.theia.cloud.common.k8s.resource.SessionStatus; import org.eclipse.theia.cloud.common.util.TheiaCloudError; import io.fabric8.kubernetes.api.model.ObjectMeta; @@ -102,4 +103,9 @@ public boolean reportActivity(String correlationId, String name) { }) != null; } + @Override + public SessionStatus createDefaultStatus() { + return new SessionStatus(); + } + } diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultWorkspaceResourceClient.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultWorkspaceResourceClient.java index 21e496fd..4eda643e 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultWorkspaceResourceClient.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/DefaultWorkspaceResourceClient.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2022 EclipseSource and others. + * Copyright (C) 2022-2023 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -20,6 +20,7 @@ import org.eclipse.theia.cloud.common.k8s.resource.Workspace; import org.eclipse.theia.cloud.common.k8s.resource.WorkspaceSpec; import org.eclipse.theia.cloud.common.k8s.resource.WorkspaceSpecResourceList; +import org.eclipse.theia.cloud.common.k8s.resource.WorkspaceStatus; import org.eclipse.theia.cloud.common.util.TheiaCloudError; import io.fabric8.kubernetes.api.model.ObjectMeta; @@ -88,4 +89,9 @@ protected boolean isWorkspaceComplete(String correlationId, WorkspaceSpec create } return false; } + + @Override + public WorkspaceStatus createDefaultStatus() { + return new WorkspaceStatus(); + } } diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/ResourceClient.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/ResourceClient.java index 08e0d1ab..d1f71945 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/ResourceClient.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/ResourceClient.java @@ -62,6 +62,15 @@ default T edit(String correlationId, String name, Consumer consumer) { return resource.edit(JavaUtil.toUnary(consumer)); } + default T editStatus(String correlationId, String name, Consumer consumer) { + trace(correlationId, "Edit status of " + name); + Resource resource = resource(name); + if (resource.get() == null) { + return null; + } + return resource.editStatus(JavaUtil.toUnary(consumer)); + } + Optional loadAndCreate(String correlationId, String yaml, Consumer customization); default Optional loadAndCreate(String correlationId, String yaml) { diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/SessionResourceClient.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/SessionResourceClient.java index b47e4cae..4fb829e3 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/SessionResourceClient.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/SessionResourceClient.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2022 EclipseSource and others. + * Copyright (C) 2022-2023 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -20,8 +20,10 @@ import org.eclipse.theia.cloud.common.k8s.resource.Session; import org.eclipse.theia.cloud.common.k8s.resource.SessionSpec; import org.eclipse.theia.cloud.common.k8s.resource.SessionSpecResourceList; +import org.eclipse.theia.cloud.common.k8s.resource.SessionStatus; -public interface SessionResourceClient extends CustomResourceClient { +public interface SessionResourceClient + extends CustomResourceClient { Session launch(String correlationId, SessionSpec spec, long timeout, TimeUnit unit); default Session launch(String correlationId, SessionSpec spec, int timeout) { diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/WorkspaceResourceClient.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/WorkspaceResourceClient.java index 77ef7b5b..7d8628e7 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/WorkspaceResourceClient.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/client/WorkspaceResourceClient.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2022 EclipseSource and others. + * Copyright (C) 2022-2023 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -20,14 +20,14 @@ import org.eclipse.theia.cloud.common.k8s.resource.Workspace; import org.eclipse.theia.cloud.common.k8s.resource.WorkspaceSpec; import org.eclipse.theia.cloud.common.k8s.resource.WorkspaceSpecResourceList; +import org.eclipse.theia.cloud.common.k8s.resource.WorkspaceStatus; public interface WorkspaceResourceClient - extends CustomResourceClient { + extends CustomResourceClient { Workspace launch(String correlationId, WorkspaceSpec spec, long timeout, TimeUnit unit); default Workspace launch(String correlationId, WorkspaceSpec spec) { return launch(correlationId, spec, 1, TimeUnit.MINUTES); } - } diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/AppDefinition.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/AppDefinition.java index 1cf4ef36..d0ae6d84 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/AppDefinition.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/AppDefinition.java @@ -25,13 +25,16 @@ import io.fabric8.kubernetes.model.annotation.Singular; import io.fabric8.kubernetes.model.annotation.Version; -@Version("v6beta") +@Version("v7beta") @Group("theia.cloud") @Singular("appdefinition") @Plural("appdefinitions") -public class AppDefinition extends CustomResource implements Namespaced { +public class AppDefinition extends CustomResource implements Namespaced { private static final long serialVersionUID = 8749670583218521755L; + public static final String API = "theia.cloud/v7beta"; + public static final String KIND = "AppDefinition"; + public static final String CRD_NAME = "appdefinitions.theia.cloud"; @Override public String toString() { diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/AppDefinitionSpec.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/AppDefinitionSpec.java index 4d891154..fcb14388 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/AppDefinitionSpec.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/AppDefinitionSpec.java @@ -22,10 +22,6 @@ @JsonDeserialize() public class AppDefinitionSpec { - public static final String API = "theia.cloud/v6beta"; - public static final String KIND = "AppDefinition"; - public static final String CRD_NAME = "appdefinitions.theia.cloud"; - @JsonProperty("name") private String name; diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/AppDefinitionStatus.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/AppDefinitionStatus.java new file mode 100644 index 00000000..f9df3ceb --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/AppDefinitionStatus.java @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (C) 2023 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.common.k8s.resource; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize +public class AppDefinitionStatus extends ResourceStatus { + // This class is empty as only the common properties of the super class are + // used. Already define a specific class to allow easier extension, properly + // type the resources and resource clients. + // It is planned to extend this later with AppDefinition specific status steps. +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/OperatorStatus.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/OperatorStatus.java new file mode 100644 index 00000000..2d16e750 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/OperatorStatus.java @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (C) 2023 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.common.k8s.resource; + +/** + * Constant values to describe resource handling. + */ +public interface OperatorStatus { + + /** + * The default status describing that the resource is new and was not handled + * before. + */ + String NEW = "NEW"; + + /** The operator tried to handle this resource but an error occurred. */ + String ERROR = "ERROR"; + + /** The operator started handling this resource. */ + String HANDLING = "HANDLING"; + + /** The operator successfully finished handling this resource. */ + String HANDLED = "HANDLED"; +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/ResourceStatus.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/ResourceStatus.java new file mode 100644 index 00000000..aa8f8767 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/ResourceStatus.java @@ -0,0 +1,44 @@ +/******************************************************************************** + * Copyright (C) 2023 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.common.k8s.resource; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public abstract class ResourceStatus { + + @JsonProperty() + private String operatorStatus; + + @JsonProperty() + private String operatorMessage; + + public String getOperatorStatus() { + return operatorStatus; + } + + public void setOperatorStatus(String operatorStatus) { + this.operatorStatus = operatorStatus; + } + + public String getOperatorMessage() { + return operatorMessage; + } + + public void setOperatorMessage(String operatorMessage) { + this.operatorMessage = operatorMessage; + } + +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/Session.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/Session.java index 1efc90b3..0a1b1855 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/Session.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/Session.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2022 EclipseSource, Lockular, Ericsson, STMicroelectronics and + * Copyright (C) 2022-2023 EclipseSource, Lockular, Ericsson, STMicroelectronics and * others. * * This program and the accompanying materials are made available under the @@ -25,13 +25,16 @@ import io.fabric8.kubernetes.model.annotation.Singular; import io.fabric8.kubernetes.model.annotation.Version; -@Version("v4beta") +@Version("v5beta") @Group("theia.cloud") @Singular("session") @Plural("sessions") -public class Session extends CustomResource implements Namespaced { +public class Session extends CustomResource implements Namespaced { private static final long serialVersionUID = 4518092300237069237L; + public static final String API = "theia.cloud/v5beta"; + public static final String KIND = "Session"; + public static final String CRD_NAME = "sessions.theia.cloud"; @Override public String toString() { diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/SessionSpec.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/SessionSpec.java index 1ab4d6c0..7e5cadf7 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/SessionSpec.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/SessionSpec.java @@ -28,10 +28,6 @@ @JsonDeserialize() public class SessionSpec implements UserScopedSpec { - public static final String API = "theia.cloud/v4beta"; - public static final String KIND = "Session"; - public static final String CRD_NAME = "sessions.theia.cloud"; - @JsonProperty("name") private String name; diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/SessionStatus.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/SessionStatus.java new file mode 100644 index 00000000..128f1366 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/SessionStatus.java @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (C) 2023 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.common.k8s.resource; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize +public class SessionStatus extends ResourceStatus { + // This class is empty as only the common properties of the super class are + // used. Already define a specific class to allow easier extension, properly + // type the resources and resource clients. + // It is planned to extend this later with Session specific status steps. +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/StatusStep.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/StatusStep.java new file mode 100644 index 00000000..e8f27132 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/StatusStep.java @@ -0,0 +1,59 @@ +/******************************************************************************** + * Copyright (C) 2023 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.common.k8s.resource; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class StatusStep { + @JsonProperty("status") + private String status; + + @JsonProperty("message") + private String message; + + public StatusStep() { + } + + public StatusStep(String status) { + this(status, ""); + } + + public StatusStep(String status, String message) { + this.status = status; + this.message = message; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + @Override + public String toString() { + return "StatusStep [status=" + status + ", message=" + message + "]"; + } +} \ No newline at end of file diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/Workspace.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/Workspace.java index 032302dd..d26e5144 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/Workspace.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/Workspace.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2022 EclipseSource and others. + * Copyright (C) 2022-2023 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -24,11 +24,15 @@ import io.fabric8.kubernetes.model.annotation.Singular; import io.fabric8.kubernetes.model.annotation.Version; -@Version("v1beta") +@Version("v2beta") @Group("theia.cloud") @Singular("workspace") @Plural("workspaces") -public class Workspace extends CustomResource implements Namespaced { +public class Workspace extends CustomResource implements Namespaced { + + public static final String API = "theia.cloud/v2beta"; + public static final String CRD_NAME = "workspaces.theia.cloud"; + public static final String KIND = "Workspace"; private static final long serialVersionUID = 6437279756051357397L; diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/WorkspaceSpec.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/WorkspaceSpec.java index d3cdd815..1c62af39 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/WorkspaceSpec.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/WorkspaceSpec.java @@ -24,10 +24,6 @@ @JsonDeserialize() public class WorkspaceSpec implements UserScopedSpec { - public static final String API = "theia.cloud/v1beta"; - public static final String KIND = "Workspace"; - public static final String CRD_NAME = "workspaces.theia.cloud"; - @JsonProperty("name") private String name; diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/WorkspaceStatus.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/WorkspaceStatus.java new file mode 100644 index 00000000..33b9b460 --- /dev/null +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/k8s/resource/WorkspaceStatus.java @@ -0,0 +1,45 @@ +/******************************************************************************** + * Copyright (C) 2023 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +package org.eclipse.theia.cloud.common.k8s.resource; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize +public class WorkspaceStatus extends ResourceStatus { + + @JsonProperty("volumeClaim") + private StatusStep volumeClaim; + + @JsonProperty("volumeAttach") + private StatusStep volumeAttach; + + public StatusStep getVolumeClaim() { + return volumeClaim; + } + + public void setVolumeClaim(StatusStep volumeClaim) { + this.volumeClaim = volumeClaim; + } + + public StatusStep getVolumeAttach() { + return volumeAttach; + } + + public void setVolumeAttach(StatusStep volumeAttach) { + this.volumeAttach = volumeAttach; + } +} diff --git a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/CustomResourceUtil.java b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/CustomResourceUtil.java index 8d03f75b..ae0d5402 100644 --- a/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/CustomResourceUtil.java +++ b/java/common/org.eclipse.theia.cloud.common/src/main/java/org/eclipse/theia/cloud/common/util/CustomResourceUtil.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2022 EclipseSource, Lockular, Ericsson, STMicroelectronics and + * Copyright (C) 2022-2023 EclipseSource, Lockular, Ericsson, STMicroelectronics and * others. * * This program and the accompanying materials are made available under the @@ -21,11 +21,8 @@ import org.eclipse.theia.cloud.common.k8s.client.DefaultTheiaCloudClient; import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; import org.eclipse.theia.cloud.common.k8s.resource.AppDefinition; -import org.eclipse.theia.cloud.common.k8s.resource.AppDefinitionSpec; import org.eclipse.theia.cloud.common.k8s.resource.Session; -import org.eclipse.theia.cloud.common.k8s.resource.SessionSpec; import org.eclipse.theia.cloud.common.k8s.resource.Workspace; -import org.eclipse.theia.cloud.common.k8s.resource.WorkspaceSpec; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResource; @@ -61,15 +58,15 @@ public static NamespacedKubernetesClient createClient(Config config) { } public static void registerSessionResource(NamespacedKubernetesClient client) { - registerCustomResource(client, Session.class, SessionSpec.KIND, SessionSpec.CRD_NAME); + registerCustomResource(client, Session.class, Session.KIND, Session.CRD_NAME); } public static void registerWorkspaceResource(NamespacedKubernetesClient client) { - registerCustomResource(client, Workspace.class, WorkspaceSpec.KIND, WorkspaceSpec.CRD_NAME); + registerCustomResource(client, Workspace.class, Workspace.KIND, Workspace.CRD_NAME); } public static void registerAppDefinitionResource(NamespacedKubernetesClient client) { - registerCustomResource(client, AppDefinition.class, AppDefinitionSpec.KIND, AppDefinitionSpec.CRD_NAME); + registerCustomResource(client, AppDefinition.class, AppDefinition.KIND, AppDefinition.CRD_NAME); } public static void registerCustomResource(NamespacedKubernetesClient client, diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/di/AbstractTheiaCloudOperatorModule.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/di/AbstractTheiaCloudOperatorModule.java index cc2063d5..779f2988 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/di/AbstractTheiaCloudOperatorModule.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/di/AbstractTheiaCloudOperatorModule.java @@ -20,9 +20,9 @@ import org.eclipse.theia.cloud.common.k8s.client.DefaultTheiaCloudClient; import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; -import org.eclipse.theia.cloud.common.k8s.resource.AppDefinitionSpec; -import org.eclipse.theia.cloud.common.k8s.resource.SessionSpec; -import org.eclipse.theia.cloud.common.k8s.resource.WorkspaceSpec; +import org.eclipse.theia.cloud.common.k8s.resource.AppDefinition; +import org.eclipse.theia.cloud.common.k8s.resource.Session; +import org.eclipse.theia.cloud.common.k8s.resource.Workspace; import org.eclipse.theia.cloud.common.util.CustomResourceUtil; import org.eclipse.theia.cloud.operator.TheiaCloud; import org.eclipse.theia.cloud.operator.TheiaCloudImpl; @@ -124,9 +124,9 @@ protected void configureTimeoutStrategies(final MultiBinding bi @Singleton protected NamespacedKubernetesClient provideKubernetesClient() { NamespacedKubernetesClient client = CustomResourceUtil.createClient(); - CustomResourceUtil.validateCustomResource(client, SessionSpec.CRD_NAME); - CustomResourceUtil.validateCustomResource(client, WorkspaceSpec.CRD_NAME); - CustomResourceUtil.validateCustomResource(client, AppDefinitionSpec.CRD_NAME); + CustomResourceUtil.validateCustomResource(client, Session.CRD_NAME); + CustomResourceUtil.validateCustomResource(client, Workspace.CRD_NAME); + CustomResourceUtil.validateCustomResource(client, AppDefinition.CRD_NAME); return client; } diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/impl/EagerStartAppDefinitionAddedHandler.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/impl/EagerStartAppDefinitionAddedHandler.java index 0c19985c..21329893 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/impl/EagerStartAppDefinitionAddedHandler.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/impl/EagerStartAppDefinitionAddedHandler.java @@ -172,7 +172,7 @@ protected void createAndApplyService(NamespacedKubernetesClient client, String n return; } K8sUtil.loadAndCreateServiceWithOwnerReference(client, namespace, correlationId, serviceYaml, - AppDefinitionSpec.API, AppDefinitionSpec.KIND, appDefinitionResourceName, appDefinitionResourceUID, 0); + AppDefinition.API, AppDefinition.KIND, appDefinitionResourceName, appDefinitionResourceUID, 0); } protected void createAndApplyDeployment(NamespacedKubernetesClient client, String namespace, String correlationId, @@ -192,7 +192,7 @@ protected void createAndApplyDeployment(NamespacedKubernetesClient client, Strin return; } K8sUtil.loadAndCreateDeploymentWithOwnerReference(client, namespace, correlationId, deploymentYaml, - AppDefinitionSpec.API, AppDefinitionSpec.KIND, appDefinitionResourceName, appDefinitionResourceUID, 0, + AppDefinition.API, AppDefinition.KIND, appDefinitionResourceName, appDefinitionResourceUID, 0, deployment -> { bandwidthLimiter.limit(deployment, appDefinition.getSpec().getDownlinkLimit(), appDefinition.getSpec().getUplinkLimit(), correlationId); @@ -220,7 +220,7 @@ protected void createAndApplyProxyConfigMap(NamespacedKubernetesClient client, S return; } K8sUtil.loadAndCreateConfigMapWithOwnerReference(client, namespace, correlationId, configMapYaml, - AppDefinitionSpec.API, AppDefinitionSpec.KIND, appDefinitionResourceName, appDefinitionResourceUID, 0, + AppDefinition.API, AppDefinition.KIND, appDefinitionResourceName, appDefinitionResourceUID, 0, configMap -> { String host = arguments.getInstancesHost() + ingressPathProvider.getPath(appDefinition, instance); int port = appDefinition.getSpec().getPort(); @@ -244,7 +244,7 @@ protected void createAndApplyEmailConfigMap(NamespacedKubernetesClient client, S return; } K8sUtil.loadAndCreateConfigMapWithOwnerReference(client, namespace, correlationId, configMapYaml, - AppDefinitionSpec.API, AppDefinitionSpec.KIND, appDefinitionResourceName, appDefinitionResourceUID, 0); + AppDefinition.API, AppDefinition.KIND, appDefinitionResourceName, appDefinitionResourceUID, 0); } } diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/impl/LazySessionHandler.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/impl/LazySessionHandler.java index d97a2041..50651f0b 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/impl/LazySessionHandler.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/impl/LazySessionHandler.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2022 EclipseSource, Lockular, Ericsson, STMicroelectronics and + * Copyright (C) 2022-2023 EclipseSource, Lockular, Ericsson, STMicroelectronics and * others. * * This program and the accompanying materials are made available under the @@ -31,8 +31,11 @@ import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; import org.eclipse.theia.cloud.common.k8s.resource.AppDefinition; import org.eclipse.theia.cloud.common.k8s.resource.AppDefinitionSpec; +import org.eclipse.theia.cloud.common.k8s.resource.OperatorStatus; +import org.eclipse.theia.cloud.common.k8s.resource.ResourceStatus; import org.eclipse.theia.cloud.common.k8s.resource.Session; import org.eclipse.theia.cloud.common.k8s.resource.SessionSpec; +import org.eclipse.theia.cloud.common.k8s.resource.SessionStatus; import org.eclipse.theia.cloud.common.k8s.resource.Workspace; import org.eclipse.theia.cloud.common.util.TheiaCloudError; import org.eclipse.theia.cloud.common.util.WorkspaceUtil; @@ -87,9 +90,47 @@ public class LazySessionHandler implements SessionHandler { @Override public boolean sessionAdded(Session session, String correlationId) { + try { + return doSessionAdded(session, correlationId); + } catch (Throwable ex) { + LOGGER.error(formatLogMessage(correlationId, + "An unexpected exception occurred while adding Session: " + session), ex); + client.sessions().updateStatus(correlationId, session, status -> { + status.setOperatorStatus(OperatorStatus.ERROR); + status.setOperatorMessage( + "Unexpected error. Please check the logs for correlationId: " + correlationId); + }); + return false; + } + } + + protected boolean doSessionAdded(Session session, String correlationId) { /* session information */ String sessionResourceName = session.getMetadata().getName(); String sessionResourceUID = session.getMetadata().getUid(); + + // Check current session status and ignore if handling failed or finished before + Optional status = Optional.ofNullable(session.getStatus()); + String operatorStatus = status.map(ResourceStatus::getOperatorStatus).orElse(OperatorStatus.NEW); + if (OperatorStatus.HANDLED.equals(operatorStatus)) { + LOGGER.trace(formatLogMessage(correlationId, + "Session was successfully handled before and is skipped now. Session: " + session)); + return true; + } + if (OperatorStatus.ERROR.equals(operatorStatus) || OperatorStatus.HANDLING.equals(operatorStatus)) { + // TODO In the HANDLING case we should not return but continue where we left + // off. + LOGGER.warn(formatLogMessage(correlationId, + "Session could not be handled before and is skipped now. Current status: " + operatorStatus + + ". Session: " + session)); + return false; + } + + // Set session status to being handled + client.sessions().updateStatus(correlationId, session, s -> { + s.setOperatorStatus(OperatorStatus.HANDLING); + }); + SessionSpec sessionSpec = session.getSpec(); /* find app definition for session */ @@ -97,20 +138,36 @@ public boolean sessionAdded(Session session, String correlationId) { Optional optionalAppDefinition = client.appDefinitions().get(appDefinitionID); if (optionalAppDefinition.isEmpty()) { LOGGER.error(formatLogMessage(correlationId, "No App Definition with name " + appDefinitionID + " found.")); + client.sessions().updateStatus(correlationId, session, s -> { + s.setOperatorStatus(OperatorStatus.ERROR); + s.setOperatorMessage("App Definition not found."); + }); return false; } AppDefinition appDefinition = optionalAppDefinition.get(); if (hasMaxInstancesReached(appDefinition, session, correlationId)) { + client.sessions().updateStatus(correlationId, session, s -> { + s.setOperatorStatus(OperatorStatus.ERROR); + s.setOperatorMessage("Max instances reached."); + }); return false; } if (hasMaxSessionsReached(session, correlationId)) { + client.sessions().updateStatus(correlationId, session, s -> { + s.setOperatorStatus(OperatorStatus.ERROR); + s.setOperatorMessage("Max sessions reached."); + }); return false; } Optional ingress = getIngress(appDefinition, correlationId); if (ingress.isEmpty()) { + client.sessions().updateStatus(correlationId, session, s -> { + s.setOperatorStatus(OperatorStatus.ERROR); + s.setOperatorMessage("Ingress not available."); + }); return false; } @@ -122,6 +179,12 @@ public boolean sessionAdded(Session session, String correlationId) { if (!existingServices.isEmpty()) { LOGGER.warn(formatLogMessage(correlationId, "Existing service for " + sessionSpec + ". Session already running?")); + client.sessions().updateStatus(correlationId, session, s -> { + s.setOperatorStatus(OperatorStatus.HANDLED); + s.setOperatorMessage("Service already exists."); + }); + // TODO do not return true if the sessions was in handling state at the start of + // this handler return true; } @@ -129,6 +192,10 @@ public boolean sessionAdded(Session session, String correlationId) { session, appDefinition.getSpec(), arguments.isUseKeycloak()); if (serviceToUse.isEmpty()) { LOGGER.error(formatLogMessage(correlationId, "Unable to create service for session " + sessionSpec)); + client.sessions().updateStatus(correlationId, session, s -> { + s.setOperatorStatus(OperatorStatus.ERROR); + s.setOperatorMessage("Failed to create service."); + }); return false; } @@ -139,6 +206,12 @@ public boolean sessionAdded(Session session, String correlationId) { if (!existingConfigMaps.isEmpty()) { LOGGER.warn(formatLogMessage(correlationId, "Existing configmaps for " + sessionSpec + ". Session already running?")); + client.sessions().updateStatus(correlationId, session, s -> { + s.setOperatorStatus(OperatorStatus.HANDLED); + s.setOperatorMessage("Configmaps already exist."); + }); + // TODO do not return true if the sessions was in handling state at the start of + // this handler return true; } createAndApplyEmailConfigMap(correlationId, sessionResourceName, sessionResourceUID, session); @@ -152,6 +225,10 @@ public boolean sessionAdded(Session session, String correlationId) { if (!existingDeployments.isEmpty()) { LOGGER.warn(formatLogMessage(correlationId, "Existing deployments for " + sessionSpec + ". Session already running?")); + client.sessions().updateStatus(correlationId, session, s -> { + s.setOperatorStatus(OperatorStatus.HANDLED); + s.setOperatorMessage("Deployment already exists."); + }); return true; } @@ -166,6 +243,10 @@ public boolean sessionAdded(Session session, String correlationId) { } catch (KubernetesClientException e) { LOGGER.error(formatLogMessage(correlationId, "Error while editing ingress " + ingress.get().getMetadata().getName()), e); + client.sessions().updateStatus(correlationId, session, s -> { + s.setOperatorStatus(OperatorStatus.ERROR); + s.setOperatorMessage("Failed to edit ingress"); + }); return false; } @@ -177,9 +258,16 @@ public boolean sessionAdded(Session session, String correlationId) { LOGGER.error( formatLogMessage(correlationId, "Error while editing session " + session.getMetadata().getName()), e); + client.sessions().updateStatus(correlationId, session, s -> { + s.setOperatorStatus(OperatorStatus.ERROR); + s.setOperatorMessage("Failed to set session URL."); + }); return false; } + client.sessions().updateStatus(correlationId, session, s -> { + s.setOperatorStatus(OperatorStatus.HANDLED); + }); return true; } @@ -273,7 +361,7 @@ protected Optional createAndApplyService(String correlationId, String s return Optional.empty(); } return K8sUtil.loadAndCreateServiceWithOwnerReference(client.kubernetes(), client.namespace(), correlationId, - serviceYaml, SessionSpec.API, SessionSpec.KIND, sessionResourceName, sessionResourceUID, 0); + serviceYaml, Session.API, Session.KIND, sessionResourceName, sessionResourceUID, 0); } protected void createAndApplyEmailConfigMap(String correlationId, String sessionResourceName, @@ -289,8 +377,7 @@ protected void createAndApplyEmailConfigMap(String correlationId, String session return; } K8sUtil.loadAndCreateConfigMapWithOwnerReference(client.kubernetes(), client.namespace(), correlationId, - configMapYaml, SessionSpec.API, SessionSpec.KIND, sessionResourceName, sessionResourceUID, 0, - configmap -> { + configMapYaml, Session.API, Session.KIND, sessionResourceName, sessionResourceUID, 0, configmap -> { configmap.setData(Collections.singletonMap(AddedHandlerUtil.FILENAME_AUTHENTICATED_EMAILS_LIST, session.getSpec().getUser())); }); @@ -309,8 +396,7 @@ protected void createAndApplyProxyConfigMap(String correlationId, String session return; } K8sUtil.loadAndCreateConfigMapWithOwnerReference(client.kubernetes(), client.namespace(), correlationId, - configMapYaml, SessionSpec.API, SessionSpec.KIND, sessionResourceName, sessionResourceUID, 0, - configMap -> { + configMapYaml, Session.API, Session.KIND, sessionResourceName, sessionResourceUID, 0, configMap -> { String host = arguments.getInstancesHost() + ingressPathProvider.getPath(appDefinition, session); int port = appDefinition.getSpec().getPort(); AddedHandlerUtil.updateProxyConfigMap(client.kubernetes(), client.namespace(), configMap, host, @@ -333,8 +419,7 @@ protected void createAndApplyDeployment(String correlationId, String sessionReso return; } K8sUtil.loadAndCreateDeploymentWithOwnerReference(client.kubernetes(), client.namespace(), correlationId, - deploymentYaml, SessionSpec.API, SessionSpec.KIND, sessionResourceName, sessionResourceUID, 0, - deployment -> { + deploymentYaml, Session.API, Session.KIND, sessionResourceName, sessionResourceUID, 0, deployment -> { pvName.ifPresent(name -> addVolumeClaim(deployment, name, appDefinition.getSpec())); bandwidthLimiter.limit(deployment, appDefinition.getSpec().getDownlinkLimit(), appDefinition.getSpec().getUplinkLimit(), correlationId); diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/impl/LazyStartAppDefinitionHandler.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/impl/LazyStartAppDefinitionHandler.java index ee1c27cf..ec7bdeac 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/impl/LazyStartAppDefinitionHandler.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/impl/LazyStartAppDefinitionHandler.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2022 EclipseSource, Lockular, Ericsson, STMicroelectronics and + * Copyright (C) 2022-2023 EclipseSource, Lockular, Ericsson, STMicroelectronics and * others. * * This program and the accompanying materials are made available under the @@ -41,9 +41,40 @@ public class LazyStartAppDefinitionHandler implements AppDefinitionHandler { @Override public boolean appDefinitionAdded(AppDefinition appDefinition, String correlationId) { - AppDefinitionSpec spec = appDefinition.getSpec(); - LOGGER.info(formatLogMessage(correlationId, "Handling " + spec)); + try { + return doAppDefinitionAdded(appDefinition, correlationId); + } catch (Throwable ex) { + LOGGER.error(formatLogMessage(correlationId, + "An unexpected exception occurred while adding AppDefinition: " + appDefinition), ex); + // TODO update status +// client.appDefinitions().updateStatus(correlationId, appDefinition, status -> +// { +// status.setOperatorStatus(OperatorStatus.ERROR); +// status.setOperatorMessage("Unexpected error. Please check the logs for correlationId " + correlationId); +// }); + return false; + } + } + protected boolean doAppDefinitionAdded(AppDefinition appDefinition, String correlationId) { + LOGGER.info(formatLogMessage(correlationId, "Handling " + appDefinition)); + + // TODO Check current session status and ignore if handling failed before +// Optional status = Optional.ofNullable(appDefinition.getStatus()); +// String operatorStatus = status.map(ResourceStatus::getOperatorStatus).orElse(OperatorStatus.NEW); +// if (OperatorStatus.ERROR.equals(operatorStatus) || OperatorStatus.HANDLING.equals(operatorStatus)) { +// LOGGER.warn(formatLogMessage(correlationId, +// "AppDefinition could not be handled before and is skipped now. Current status: " + operatorStatus +// + ". AppDefinition: " + appDefinition)); +// return false; +// } + + // TODO Set app definition status to being handled +// client.appDefinitions().updateStatus(correlationId, appDefinition, s -> { +// s.setOperatorStatus(OperatorStatus.HANDLING); +// }); + + AppDefinitionSpec spec = appDefinition.getSpec(); String appDefinitionResourceName = appDefinition.getMetadata().getName(); /* Create ingress if not existing */ @@ -52,10 +83,20 @@ public boolean appDefinitionAdded(AppDefinition appDefinition, String correlatio LOGGER.error(formatLogMessage(correlationId, "Expected ingress '" + spec.getIngressname() + "' for app definition '" + appDefinitionResourceName + "' does not exist. Abort handling app definition.")); + // TODO update status + // client.appDefinitions().updateStatus(correlationId, appDefinition, s -> { +// s.setOperatorStatus(OperatorStatus.ERROR); +// s.setOperatorMessage("Ingress does not exist."); +// }); return false; } else { LOGGER.trace(formatLogMessage(correlationId, "Ingress available already")); } + + // TODO update status +// client.appDefinitions().updateStatus(correlationId, appDefinition, s -> { +// s.setOperatorStatus(OperatorStatus.HANDLED); +// }); return true; } diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/impl/LazyWorkspaceHandler.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/impl/LazyWorkspaceHandler.java index 61229281..819cd789 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/impl/LazyWorkspaceHandler.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/impl/LazyWorkspaceHandler.java @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2022 EclipseSource and others. + * Copyright (C) 2022-2023 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -17,10 +17,16 @@ import static org.eclipse.theia.cloud.common.util.LogMessageUtil.formatLogMessage; +import java.util.Optional; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.theia.cloud.common.k8s.client.TheiaCloudClient; +import org.eclipse.theia.cloud.common.k8s.resource.OperatorStatus; +import org.eclipse.theia.cloud.common.k8s.resource.ResourceStatus; +import org.eclipse.theia.cloud.common.k8s.resource.StatusStep; import org.eclipse.theia.cloud.common.k8s.resource.Workspace; +import org.eclipse.theia.cloud.common.k8s.resource.WorkspaceStatus; import org.eclipse.theia.cloud.common.util.WorkspaceUtil; import org.eclipse.theia.cloud.operator.handler.PersistentVolumeCreator; import org.eclipse.theia.cloud.operator.handler.WorkspaceHandler; @@ -38,23 +44,78 @@ public class LazyWorkspaceHandler implements WorkspaceHandler { @Override public boolean workspaceAdded(Workspace workspace, String correlationId) { - LOGGER.info(formatLogMessage(correlationId, "Handling " + workspace.getSpec())); + try { + return doWorkspaceAdded(workspace, correlationId); + } catch (Throwable ex) { + LOGGER.error(formatLogMessage(correlationId, + "An unexpected exception occurred while adding Workspace: " + workspace), ex); + client.workspaces().updateStatus(correlationId, workspace, status -> { + status.setOperatorStatus(OperatorStatus.ERROR); + status.setOperatorMessage( + "Unexpected error. Please check the logs for correlationId: " + correlationId); + }); + return false; + } + } + + protected boolean doWorkspaceAdded(Workspace workspace, String correlationId) { + LOGGER.info(formatLogMessage(correlationId, "Handling " + workspace)); + + // Check current session status and ignore if handling failed before + Optional status = Optional.ofNullable(workspace.getStatus()); + String operatorStatus = status.map(ResourceStatus::getOperatorStatus).orElse(OperatorStatus.NEW); + if (OperatorStatus.HANDLED.equals(operatorStatus)) { + LOGGER.trace(formatLogMessage(correlationId, + "Workspace was successfully handled before and is skipped now. Workspace: " + workspace)); + return true; + } + if (OperatorStatus.ERROR.equals(operatorStatus) || OperatorStatus.HANDLING.equals(operatorStatus)) { + // TODO In the HANDLING case we should not return but continue where we left + // off. + LOGGER.warn(formatLogMessage(correlationId, + "Workspace could not be handled before and is skipped now. Current status: " + operatorStatus + + ". Workspace: " + workspace)); + return false; + } + + // Set workspace status to being handled + client.workspaces().updateStatus(correlationId, workspace, s -> { + s.setOperatorStatus(OperatorStatus.HANDLING); + }); String storageName = WorkspaceUtil.getStorageName(workspace); + client.workspaces().updateStatus(correlationId, workspace, s -> s.setVolumeClaim(new StatusStep("started"))); + if (!client.persistentVolumes().has(storageName)) { LOGGER.trace(formatLogMessage(correlationId, "Creating new persistent volume named " + storageName)); persistentVolumeHandler.createAndApplyPersistentVolume(correlationId, workspace); } + client.workspaces().updateStatus(correlationId, workspace, s -> { + s.setVolumeClaim(new StatusStep("finished")); + s.setVolumeAttach(new StatusStep("started")); + }); + if (!client.persistentVolumeClaims().has(storageName)) { LOGGER.trace(formatLogMessage(correlationId, "Creating new persistent volume claim named " + storageName)); persistentVolumeHandler.createAndApplyPersistentVolumeClaim(correlationId, workspace); } + client.workspaces().updateStatus(correlationId, workspace, s -> { + s.setVolumeAttach(new StatusStep("claimed")); + }); + LOGGER.trace(formatLogMessage(correlationId, "Set workspace storage " + storageName)); client.workspaces().edit(correlationId, workspace.getSpec().getName(), toEdit -> toEdit.getSpec().setStorage(storageName)); + client.workspaces().updateStatus(correlationId, workspace, s -> { + s.setVolumeAttach(new StatusStep("finished")); + }); + + client.workspaces().updateStatus(correlationId, workspace, s -> { + s.setOperatorStatus(OperatorStatus.HANDLED); + }); return true; } diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/util/TheiaCloudHandlerUtil.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/util/TheiaCloudHandlerUtil.java index 2908a2a1..de62d339 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/util/TheiaCloudHandlerUtil.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/util/TheiaCloudHandlerUtil.java @@ -29,7 +29,6 @@ import org.apache.logging.log4j.Logger; import org.eclipse.theia.cloud.common.k8s.resource.AppDefinition; import org.eclipse.theia.cloud.common.k8s.resource.Session; -import org.eclipse.theia.cloud.common.k8s.resource.SessionSpec; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.OwnerReference; @@ -77,7 +76,7 @@ public static T addOwnerReferenceToItem(String correlati public static OwnerReference createOwnerReference(String sessionResourceName, String sessionResourceUID) { OwnerReference ownerReference = new OwnerReference(); ownerReference.setApiVersion(HasMetadata.getApiVersion(Session.class)); - ownerReference.setKind(SessionSpec.KIND); + ownerReference.setKind(Session.KIND); ownerReference.setName(sessionResourceName); ownerReference.setUid(sessionResourceUID); return ownerReference; diff --git a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/util/TheiaCloudIngressUtil.java b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/util/TheiaCloudIngressUtil.java index e0e4b356..bb771724 100644 --- a/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/util/TheiaCloudIngressUtil.java +++ b/java/operator/org.eclipse.theia.cloud.operator/src/main/java/org/eclipse/theia/cloud/operator/handler/util/TheiaCloudIngressUtil.java @@ -23,7 +23,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.theia.cloud.common.k8s.resource.AppDefinition; -import org.eclipse.theia.cloud.common.k8s.resource.AppDefinitionSpec; import org.eclipse.theia.cloud.common.util.JavaUtil; import org.eclipse.theia.cloud.operator.handler.impl.AddedHandlerUtil; @@ -54,7 +53,7 @@ public static boolean checkForExistingIngressAndAddOwnerReferencesIfMissing(Name if (ingress.isPresent()) { OwnerReference ownerReference = new OwnerReference(); ownerReference.setApiVersion(HasMetadata.getApiVersion(AppDefinition.class)); - ownerReference.setKind(AppDefinitionSpec.KIND); + ownerReference.setKind(AppDefinition.KIND); ownerReference.setName(appDefinition.getMetadata().getName()); ownerReference.setUid(appDefinition.getMetadata().getUid()); addOwnerReferenceToIngress(client, namespace, ingress.get(), ownerReference);