From 594c7ef1e8fd13c39ff666c1df8d3f84fcd30b48 Mon Sep 17 00:00:00 2001 From: Lucas Koehler Date: Mon, 31 Jul 2023 22:59:09 +0200 Subject: [PATCH] #200: Initial crash loop prevention for custom resources - 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 Note: Status handling can probably be refactored to be a bit cleaner and make use of better code-reuse. 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 | 23 ++++++ .../common/k8s/resource/OperatorStatus.java | 30 ++++++++ .../common/k8s/resource/ResourceStatus.java | 44 ++++++++++++ .../cloud/common/k8s/resource/Session.java | 9 ++- .../common/k8s/resource/SessionSpec.java | 4 -- .../common/k8s/resource/SessionStatus.java | 23 ++++++ .../cloud/common/k8s/resource/StatusStep.java | 55 ++++++++++++++ .../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 | 71 +++++++++++++++++-- .../impl/LazyStartAppDefinitionHandler.java | 32 ++++++++- .../handler/impl/LazyWorkspaceHandler.java | 42 ++++++++++- .../handler/util/TheiaCloudHandlerUtil.java | 3 +- .../handler/util/TheiaCloudIngressUtil.java | 3 +- 28 files changed, 454 insertions(+), 63 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..75848fe3 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..28cda915 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("v8beta") @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/v8beta"; + 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..154c7bb9 --- /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,23 @@ +/******************************************************************************** + * 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 { + +} 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..acf7fb6c --- /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,30 @@ +/******************************************************************************** + * Copyright (C) 2022 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 { + + String NEW = "NEW"; + + String ERROR = "ERROR"; + + String HANDLING = "HANDLING"; + + 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..df73c15d --- /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,23 @@ +/******************************************************************************** + * 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 { + +} 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..be8bf905 --- /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,55 @@ +/******************************************************************************** + * 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, 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..3801f238 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 @@ -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,26 @@ public class LazySessionHandler implements SessionHandler { @Override public boolean sessionAdded(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 before + Optional status = Optional.ofNullable(session.getStatus()); + String operatorStatus = status.map(ResourceStatus::getOperatorStatus).orElse(OperatorStatus.NEW); + if (OperatorStatus.ERROR.equals(operatorStatus) || OperatorStatus.HANDLING.equals(operatorStatus)) { + 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 +117,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 +158,10 @@ 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."); + }); return true; } @@ -129,6 +169,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 +183,10 @@ 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."); + }); return true; } createAndApplyEmailConfigMap(correlationId, sessionResourceName, sessionResourceUID, session); @@ -152,6 +200,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 +218,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 +233,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 +336,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,7 +352,7 @@ protected void createAndApplyEmailConfigMap(String correlationId, String session return; } K8sUtil.loadAndCreateConfigMapWithOwnerReference(client.kubernetes(), client.namespace(), correlationId, - configMapYaml, SessionSpec.API, SessionSpec.KIND, sessionResourceName, sessionResourceUID, 0, + configMapYaml, Session.API, Session.KIND, sessionResourceName, sessionResourceUID, 0, configmap -> { configmap.setData(Collections.singletonMap(AddedHandlerUtil.FILENAME_AUTHENTICATED_EMAILS_LIST, session.getSpec().getUser())); @@ -309,7 +372,7 @@ protected void createAndApplyProxyConfigMap(String correlationId, String session return; } K8sUtil.loadAndCreateConfigMapWithOwnerReference(client.kubernetes(), client.namespace(), correlationId, - configMapYaml, SessionSpec.API, SessionSpec.KIND, sessionResourceName, sessionResourceUID, 0, + configMapYaml, Session.API, Session.KIND, sessionResourceName, sessionResourceUID, 0, configMap -> { String host = arguments.getInstancesHost() + ingressPathProvider.getPath(appDefinition, session); int port = appDefinition.getSpec().getPort(); @@ -333,7 +396,7 @@ protected void createAndApplyDeployment(String correlationId, String sessionReso return; } K8sUtil.loadAndCreateDeploymentWithOwnerReference(client.kubernetes(), client.namespace(), correlationId, - deploymentYaml, SessionSpec.API, SessionSpec.KIND, sessionResourceName, sessionResourceUID, 0, + deploymentYaml, Session.API, Session.KIND, sessionResourceName, sessionResourceUID, 0, deployment -> { pvName.ifPresent(name -> addVolumeClaim(deployment, name, appDefinition.getSpec())); bandwidthLimiter.limit(deployment, appDefinition.getSpec().getDownlinkLimit(), 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..d979e200 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 @@ -18,11 +18,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.AppDefinition; import org.eclipse.theia.cloud.common.k8s.resource.AppDefinitionSpec; +import org.eclipse.theia.cloud.common.k8s.resource.AppDefinitionStatus; +import org.eclipse.theia.cloud.common.k8s.resource.OperatorStatus; +import org.eclipse.theia.cloud.common.k8s.resource.ResourceStatus; import org.eclipse.theia.cloud.operator.handler.AppDefinitionHandler; import org.eclipse.theia.cloud.operator.handler.IngressPathProvider; import org.eclipse.theia.cloud.operator.handler.util.TheiaCloudIngressUtil; @@ -41,9 +46,24 @@ public class LazyStartAppDefinitionHandler implements AppDefinitionHandler { @Override public boolean appDefinitionAdded(AppDefinition appDefinition, String correlationId) { - AppDefinitionSpec spec = appDefinition.getSpec(); - LOGGER.info(formatLogMessage(correlationId, "Handling " + spec)); + LOGGER.info(formatLogMessage(correlationId, "Handling " + appDefinition)); + + // 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; + } + + // 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 +72,18 @@ 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.")); + 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")); } + + 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..9d98ea24 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 @@ -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,57 @@ public class LazyWorkspaceHandler implements WorkspaceHandler { @Override public boolean workspaceAdded(Workspace workspace, String correlationId) { - LOGGER.info(formatLogMessage(correlationId, "Handling " + workspace.getSpec())); + 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.ERROR.equals(operatorStatus) || OperatorStatus.HANDLING.equals(operatorStatus)) { + 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", null))); + 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", null)); + s.setVolumeAttach(new StatusStep("started", null)); + }); + 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", null)); + }); + 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", null)); + }); + + 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);