This is an automated email from the ASF dual-hosted git repository.

mgubaidullin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-karavan.git

commit a773db372eab9f180110ad6129d58004a1bce571
Author: Marat Gubaidullin <[email protected]>
AuthorDate: Fri Aug 28 09:30:45 2026 -0400

    KubernetesService cleanup
---
 docs/install/karavan-helm/.helmignore              |  23 ----
 docs/install/karavan-helm/Chart.yaml               |   5 -
 .../install/karavan-helm/templates/deployment.yaml | 105 ---------------
 docs/install/karavan-helm/templates/ingress.yaml   |  28 ----
 docs/install/karavan-helm/templates/nodePort.yaml  |  15 ---
 .../karavan-helm/templates/role-binding.yaml       |  25 ----
 docs/install/karavan-helm/templates/role.yaml      |  29 ----
 docs/install/karavan-helm/templates/secret.yaml    |  20 ---
 .../karavan-helm/templates/service-account.yaml    |   4 -
 docs/install/karavan-helm/templates/service.yaml   |  18 ---
 docs/install/karavan-helm/values.yaml              |  52 -------
 docs/install/karavan-kubernetes/role-binding.yaml  |  13 --
 docs/install/karavan-kubernetes/role.yaml          |  63 ++++++---
 .../camel/karavan/api/InfrastructureResource.java  |   7 +-
 .../karavan/kubernetes/KubernetesService.java      | 150 ++++++++++++++-------
 .../src/main/resources/application.properties      |   4 +
 16 files changed, 158 insertions(+), 403 deletions(-)

diff --git a/docs/install/karavan-helm/.helmignore 
b/docs/install/karavan-helm/.helmignore
deleted file mode 100644
index 0e8a0eb3..00000000
--- a/docs/install/karavan-helm/.helmignore
+++ /dev/null
@@ -1,23 +0,0 @@
-# Patterns to ignore when building packages.
-# This supports shell glob matching, relative path matching, and
-# negation (prefixed with !). Only one pattern per line.
-.DS_Store
-# Common VCS dirs
-.git/
-.gitignore
-.bzr/
-.bzrignore
-.hg/
-.hgignore
-.svn/
-# Common backup files
-*.swp
-*.bak
-*.tmp
-*.orig
-*~
-# Various IDEs
-.project
-.idea/
-*.tmproj
-.vscode/
diff --git a/docs/install/karavan-helm/Chart.yaml 
b/docs/install/karavan-helm/Chart.yaml
deleted file mode 100644
index 96d99b83..00000000
--- a/docs/install/karavan-helm/Chart.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-apiVersion: v2
-name: karavan
-version: 0.1.0
-description: Chart Helm for deploy Karavan
-appVersion: "4.18.1" 
\ No newline at end of file
diff --git a/docs/install/karavan-helm/templates/deployment.yaml 
b/docs/install/karavan-helm/templates/deployment.yaml
deleted file mode 100644
index c058629e..00000000
--- a/docs/install/karavan-helm/templates/deployment.yaml
+++ /dev/null
@@ -1,105 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: {{ .Release.Name }}
-  labels:
-    app: {{ .Release.Name }}
-spec:
-  replicas: {{ .Values.replicaCount }}
-  selector:
-    matchLabels:
-      app: {{ .Release.Name }}
-  template:
-    metadata:
-      labels:
-        app: {{ .Release.Name }}
-    spec:
-      containers:
-      - name: {{ .Release.Name }}
-        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
-        imagePullPolicy: {{ .Values.image.pullPolicy }}
-        env:
-        - name: KARAVAN_CONTAINER_STATUS_INTERVAL
-          value: {{ .Values.env.KARAVAN_CONTAINER_STATUS_INTERVAL | quote }}
-        - name: KARAVAN_CONTAINER_STATISTICS_INTERVAL
-          value: {{ .Values.env.KARAVAN_CONTAINER_STATISTICS_INTERVAL | quote 
}}
-        - name: KARAVAN_CAMEL_STATUS_INTERVAL
-          value: {{ .Values.env.KARAVAN_CAMEL_STATUS_INTERVAL | quote }}
-        - name: KARAVAN_DEVMODE_IMAGE
-          value: {{ .Values.env.KARAVAN_DEVMODE_IMAGE | quote }}
-        - name: KARAVAN_GIT_REPOSITORY
-          valueFrom:
-            secretKeyRef:
-              key: karavan.git.repository
-              name: {{ .Values.secret.existingSecret | default .Release.Name }}
-        - name: KARAVAN_GIT_USERNAME
-          valueFrom:
-            secretKeyRef:
-              key: karavan.git.username
-              name: {{ .Values.secret.existingSecret | default .Release.Name }}
-        - name: KARAVAN_GIT_PASSWORD
-          valueFrom:
-            secretKeyRef:
-              key: karavan.git.password
-              name: {{ .Values.secret.existingSecret | default .Release.Name }}
-        - name: KARAVAN_GIT_BRANCH
-          valueFrom:
-            secretKeyRef:
-              key: karavan.git.branch
-              name: {{ .Values.secret.existingSecret | default .Release.Name }}
-        - name: KARAVAN_KEYCLOAK_URL
-          valueFrom:
-            secretKeyRef:
-              key: karavan.keycloak.url
-              name: {{ .Values.secret.existingSecret | default .Release.Name }}
-        - name: KARAVAN_KEYCLOAK_REALM
-          valueFrom:
-            secretKeyRef:
-              key: karavan.keycloak.realm
-              name: {{ .Values.secret.existingSecret | default .Release.Name }}
-        - name: KARAVAN_KEYCLOAK_FRONTEND_CLIENTID
-          valueFrom:
-            secretKeyRef:
-              key: karavan.keycloak.frontend.clientId
-              name: {{ .Values.secret.existingSecret | default .Release.Name }}
-        - name: KARAVAN_KEYCLOAK_BACKEND_CLIENTID
-          valueFrom:
-            secretKeyRef:
-              key: karavan.keycloak.backend.clientId
-              name: {{ .Values.secret.existingSecret | default .Release.Name }}
-        - name: KARAVAN_KEYCLOAK_BACKEND_SECRET
-          valueFrom:
-            secretKeyRef:
-              key: karavan.keycloak.backend.secret
-              name: {{ .Values.secret.existingSecret | default .Release.Name }}
-        - name: KARAVAN_CONTAINER_IMAGE_REGISTRY
-          valueFrom:
-            secretKeyRef:
-              key: karavan.container-image.registry
-              name: {{ .Values.secret.existingSecret | default .Release.Name }}
-        - name: KARAVAN_CONTAINER_IMAGE_GROUP
-          valueFrom:
-            secretKeyRef:
-              key: karavan.container-image.group
-              name: {{ .Values.secret.existingSecret | default .Release.Name }}
-        - name: KARAVAN_CONTAINER_IMAGE_REGISTRY_USERNAME
-          valueFrom:
-            secretKeyRef:
-              key: karavan.container-image.registry-username
-              name: {{ .Values.secret.existingSecret | default .Release.Name }}
-        - name: KARAVAN_CONTAINER_IMAGE_REGISTRY_PASSWORD
-          valueFrom:
-            secretKeyRef:
-              key: karavan.container-image.registry-password
-              name: {{ .Values.secret.existingSecret | default .Release.Name }}
-        - name: KUBERNETES_NAMESPACE
-          valueFrom:
-            fieldRef:
-              fieldPath: metadata.namespace
-        ports:
-        - containerPort: {{ .Values.service.targetPort }}
-          name: karavan
-        resources:
-          requests:
-            memory: {{ .Values.resources.requests.memory | quote }}
-      serviceAccountName: {{ .Release.Name }} 
\ No newline at end of file
diff --git a/docs/install/karavan-helm/templates/ingress.yaml 
b/docs/install/karavan-helm/templates/ingress.yaml
deleted file mode 100644
index 36866b72..00000000
--- a/docs/install/karavan-helm/templates/ingress.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-{{- if .Values.ingress.enabled }}
-apiVersion: networking.k8s.io/v1
-kind: Ingress
-metadata:
-  name: {{ .Release.Name }}
-  annotations:
-    {{- with .Values.ingress.annotations }}
-    {{- toYaml . | nindent 4 }}
-    {{- end }}
-spec:
-  {{- if .Values.ingress.tls }}
-  tls:
-    - hosts:
-        - {{ .Values.ingress.host }}
-      secretName: {{ .Values.ingress.tlsSecretName }}
-  {{- end }}
-  rules:
-    - host: {{ .Values.ingress.host }}
-      http:
-        paths:
-          - path: /
-            pathType: Prefix
-            backend:
-              service:
-                name: {{ .Release.Name }}
-                port:
-                  number: {{ .Values.service.port }}
-{{- end }} 
\ No newline at end of file
diff --git a/docs/install/karavan-helm/templates/nodePort.yaml 
b/docs/install/karavan-helm/templates/nodePort.yaml
deleted file mode 100644
index e0655fcd..00000000
--- a/docs/install/karavan-helm/templates/nodePort.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-{{- if .Values.enableNodePort }}
-apiVersion: v1
-kind: Service
-metadata:
-  name: {{ .Release.Name }}
-spec:
-  type: ClusterIP
-  ports:
-    - name: http
-      port: {{ .Values.service.port }}
-      targetPort: {{ .Values.service.targetPort }}
-      protocol: TCP
-  selector:
-    app: {{ .Release.Name }} 
-{{- end }} 
\ No newline at end of file
diff --git a/docs/install/karavan-helm/templates/role-binding.yaml 
b/docs/install/karavan-helm/templates/role-binding.yaml
deleted file mode 100644
index 89bca84c..00000000
--- a/docs/install/karavan-helm/templates/role-binding.yaml
+++ /dev/null
@@ -1,25 +0,0 @@
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: {{ .Release.Name }}-role-binding
-roleRef:
-  kind: Role
-  apiGroup: rbac.authorization.k8s.io
-  name: {{ .Release.Name }}
-subjects:
-- kind: ServiceAccount
-  apiGroup: ""
-  name: {{ .Release.Name }}
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: {{ .Release.Name }}-cluster-role-binding
-roleRef:
-  kind: ClusterRole
-  apiGroup: rbac.authorization.k8s.io
-  name: view
-subjects:
-- kind: ServiceAccount
-  apiGroup: ""
-  name: {{ .Release.Name }} 
\ No newline at end of file
diff --git a/docs/install/karavan-helm/templates/role.yaml 
b/docs/install/karavan-helm/templates/role.yaml
deleted file mode 100644
index f72a73d7..00000000
--- a/docs/install/karavan-helm/templates/role.yaml
+++ /dev/null
@@ -1,29 +0,0 @@
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: {{ .Release.Name }}
-rules:
-- apiGroups: [""]
-  resources: ["secrets", "configmaps", "serviceaccounts"]
-  verbs: ["*"]
-- apiGroups: [""]
-  resources: ["persistentvolumes", "persistentvolumeclaims"]
-  verbs: ["*"]
-- apiGroups: [""]
-  resources: ["pods", "services", "replicationcontrollers"]
-  verbs: ["*"]
-- apiGroups: ["route.openshift.io"]
-  resources: ["routes"]
-  verbs:  ["*"]
-- apiGroups: ["apps"]
-  resources: ["deployments"]
-  verbs: ["*"]
-- apiGroups: ["networking.k8s.io"]
-  resources: ["ingresses"]
-  verbs: ["*"]
-- apiGroups: ["image.openshift.io"]
-  resources: ["imagestreams/layers"]
-  verbs: ["update", "get"]
-- apiGroups: [""]
-  resources: ["pods/exec"]
-  verbs: ["create", "get"] 
\ No newline at end of file
diff --git a/docs/install/karavan-helm/templates/secret.yaml 
b/docs/install/karavan-helm/templates/secret.yaml
deleted file mode 100644
index 67a02484..00000000
--- a/docs/install/karavan-helm/templates/secret.yaml
+++ /dev/null
@@ -1,20 +0,0 @@
-{{- if not .Values.secret.existingSecret }}
-apiVersion: v1
-kind: Secret
-metadata:
-  name: {{ .Release.Name }}
-stringData:
-  karavan.keycloak.url: {{ .Values.secret.keycloak.url | quote }}
-  karavan.keycloak.realm: {{ .Values.secret.keycloak.realm | quote }}
-  karavan.keycloak.frontend.clientId: {{ 
.Values.secret.keycloak.frontendClientId | quote }}
-  karavan.keycloak.backend.clientId: {{ 
.Values.secret.keycloak.backendClientId | quote }}
-  karavan.keycloak.backend.secret: {{ .Values.secret.keycloak.backendSecret | 
quote }}
-  karavan.git.repository: {{ .Values.secret.git.repository | quote }}
-  karavan.git.username: {{ .Values.secret.git.username | quote }}
-  karavan.git.password: {{ .Values.secret.git.password | quote }}
-  karavan.git.branch: {{ .Values.secret.git.branch | quote }}
-  karavan.container-image.registry: {{ .Values.secret.containerImage.registry 
| quote }}
-  karavan.container-image.group: {{ .Values.secret.containerImage.group | 
quote }}
-  karavan.container-image.registry-username: {{ 
.Values.secret.containerImage.registryUsername | quote }}
-  karavan.container-image.registry-password: {{ 
.Values.secret.containerImage.registryPassword | quote }}
-{{- end }}
\ No newline at end of file
diff --git a/docs/install/karavan-helm/templates/service-account.yaml 
b/docs/install/karavan-helm/templates/service-account.yaml
deleted file mode 100644
index 7e1bad4c..00000000
--- a/docs/install/karavan-helm/templates/service-account.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: {{ .Release.Name }} 
\ No newline at end of file
diff --git a/docs/install/karavan-helm/templates/service.yaml 
b/docs/install/karavan-helm/templates/service.yaml
deleted file mode 100644
index 9643e52b..00000000
--- a/docs/install/karavan-helm/templates/service.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: {{ .Release.Name }}
-spec:
-  type: {{ .Values.service.type }}
-  ports:
-    - name: http
-      port: {{ .Values.service.port }}
-      targetPort: {{ .Values.service.targetPort }}
-      {{- if and (eq .Values.service.type "NodePort") 
.Values.service.enableNodePort }}
-      nodePort: {{ .Values.service.nodePort }}
-      {{- end }}
-    - name: https
-      port: {{ .Values.service.httpsPort }}
-      targetPort: {{ .Values.service.targetPort }}
-  selector:
-    app: {{ .Release.Name }} 
\ No newline at end of file
diff --git a/docs/install/karavan-helm/values.yaml 
b/docs/install/karavan-helm/values.yaml
deleted file mode 100644
index 96995368..00000000
--- a/docs/install/karavan-helm/values.yaml
+++ /dev/null
@@ -1,52 +0,0 @@
-replicaCount: 1
-
-image:
-  repository: ghcr.io/apache/camel-karavan
-  tag: "4.18.1"
-  pullPolicy: Always
-
-env:
-  KARAVAN_CONTAINER_STATUS_INTERVAL: "disabled"
-  KARAVAN_CONTAINER_STATISTICS_INTERVAL: "disabled"
-  KARAVAN_CAMEL_STATUS_INTERVAL: "3s"
-  KARAVAN_DEVMODE_IMAGE: "ghcr.io/apache/camel-karavan-devmode:4.18.1"
-
-service:
-  type: ClusterIP
-  port: 80
-  targetPort: 8080
-  nodePort: 30777
-  enableNodePort: false
-  httpsPort: 443
-
-resources:
-  requests:
-    memory: "1024Mi"
-
-secret:
-  # Set the following if you want to use an existing Secret. In this case 
everything set below will be ignored.
-  # existingSecret: karavan
-  keycloak:
-    url: "https://localhost";
-    realm: "karavan"
-    frontendClientId: "karavan"
-    backendClientId: "karavan"
-    backendSecret: "secret"
-  git:
-    repository: "http://reporitory/karavan/karavan.git";
-    username: "karavan"
-    password: "karavan"
-    branch: "main"
-  containerImage:
-    registry: "registry:5000"
-    group: "karavan"
-    registryUsername: ""
-    registryPassword: ""
-
-ingress:
-  enabled: true
-  host: "hostname.local"
-  annotations: 
-    kubernetes.io/ingress.class: nginx
-  tls: false
-  tlsSecretName: "" 
\ No newline at end of file
diff --git a/docs/install/karavan-kubernetes/role-binding.yaml 
b/docs/install/karavan-kubernetes/role-binding.yaml
index 46767c0a..0f4c1f2b 100644
--- a/docs/install/karavan-kubernetes/role-binding.yaml
+++ b/docs/install/karavan-kubernetes/role-binding.yaml
@@ -8,19 +8,6 @@ roleRef:
   apiGroup: "rbac.authorization.k8s.io"
   name: "karavan"
 subjects:
-- kind: "ServiceAccount"
-  apiGroup: ""
-  name: "karavan"
----
-apiVersion: "rbac.authorization.k8s.io/v1"
-kind: "RoleBinding"
-metadata:
-  name: "karavan-cluster-role-binding"
-roleRef:
-  kind: "ClusterRole"
-  apiGroup: "rbac.authorization.k8s.io"
-  name: "view"
-subjects:
 - kind: "ServiceAccount"
   apiGroup: ""
   name: "karavan"
\ No newline at end of file
diff --git a/docs/install/karavan-kubernetes/role.yaml 
b/docs/install/karavan-kubernetes/role.yaml
index 2807523f..c62cd8a8 100644
--- a/docs/install/karavan-kubernetes/role.yaml
+++ b/docs/install/karavan-kubernetes/role.yaml
@@ -4,27 +4,52 @@ kind: "Role"
 metadata:
   name: "karavan"
 rules:
+# Karavan secret/configmap management + configuration of deployed integrations
 - apiGroups: [""]
-  resources: ["secrets", "configmaps", "serviceaccounts"]
-  verbs: ["*"]
+  resources: ["secrets", "configmaps"]
+  verbs: ["get", "list", "create", "update", "patch", "delete"]
+# Read-only: resolving the ServiceAccount referenced by generated pod specs
 - apiGroups: [""]
-  resources: ["persistentvolumes", "persistentvolumeclaims"]
-  verbs: ["*"]
+  resources: ["serviceaccounts"]
+  verbs: ["get", "list"]
+# Maven cache volume for devmode pods (karavan.devmode.createm2)
 - apiGroups: [""]
-  resources: ["pods", "services", "replicationcontrollers"]
-  verbs: ["*"]
-- apiGroups: ["route.openshift.io"]
-  resources: ["routes"]
-  verbs:  ["*"]
+  resources: ["persistentvolumeclaims"]
+  verbs: ["get", "create", "patch", "delete"]
+# devmode and builder pods + pod status informer
+- apiGroups: [""]
+  resources: ["pods"]
+  verbs: ["get", "list", "watch", "create", "patch", "delete"]
+# Streaming container logs to the UI
+- apiGroups: [""]
+  resources: ["pods/log"]
+  verbs: ["get"]
+# Services for devmode pods and deployed integrations + service status informer
+- apiGroups: [""]
+  resources: ["services"]
+  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
+# Pod events shown in the UI
+- apiGroups: ["", "events.k8s.io"]
+  resources: ["events"]
+  verbs: ["get", "list", "watch"]
+# Deployed integrations, rollout restart + deployment status informer
 - apiGroups: ["apps"]
   resources: ["deployments"]
-  verbs: ["*"]
-- apiGroups: ["networking.k8s.io"]
-  resources: ["ingresses"]
-  verbs: ["*"]
-- apiGroups: ["image.openshift.io"]
-  resources: ["imagestreams/layers"]
-  verbs: ["update", "get"]
-- apiGroups: [""]
-  resources: ["pods/exec"]
-  verbs: ["create", "get"]
\ No newline at end of file
+  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
+
+# --- OpenShift only -------------------------------------------------------
+# Uncomment when running on OpenShift and integrations are exposed through
+# Routes and/or images are pushed to the internal image registry.
+# - apiGroups: ["route.openshift.io"]
+#   resources: ["routes"]
+#   verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
+# - apiGroups: ["image.openshift.io"]
+#   resources: ["imagestreams/layers"]
+#   verbs: ["get", "update"]
+
+# --- Optional -------------------------------------------------------------
+# Uncomment only if integrations are exposed through Ingress resources
+# (JKube creates them only when -Djkube.createExternalUrls=true).
+# - apiGroups: ["networking.k8s.io"]
+#   resources: ["ingresses"]
+#   verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java
 
b/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java
index 409e2221..c55cddf1 100644
--- 
a/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java
@@ -96,7 +96,12 @@ public class InfrastructureResource {
         if (resources == null) {
             return Response.status(Response.Status.NOT_FOUND).entity("Resource 
file " + KUBERNETES_YAML_FILENAME + " not found").build();
         }
-        kubernetesService.startDeployment(resources.getCode(), 
Map.of(LABEL_TYPE, ContainerType.packaged.name()));
+        try {
+            kubernetesService.startDeployment(resources.getCode(), 
Map.of(LABEL_TYPE, ContainerType.packaged.name()));
+        } catch (IllegalArgumentException e) {
+            LOGGER.error("Rejected " + name + " of project " + projectId + ": 
" + e.getMessage());
+            return 
Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
+        }
         return Response.ok().build();
     }
 
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/KubernetesService.java
 
b/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/KubernetesService.java
index 7f3516ff..b11ecb2a 100644
--- 
a/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/KubernetesService.java
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/kubernetes/KubernetesService.java
@@ -20,7 +20,6 @@ import io.fabric8.kubernetes.api.model.*;
 import io.fabric8.kubernetes.api.model.apps.Deployment;
 import io.fabric8.kubernetes.client.KubernetesClient;
 import io.fabric8.kubernetes.client.KubernetesClientBuilder;
-import io.fabric8.kubernetes.client.dsl.ExecWatch;
 import io.fabric8.kubernetes.client.dsl.LogWatch;
 import io.fabric8.kubernetes.client.utils.Serialization;
 import io.quarkus.runtime.LaunchMode;
@@ -39,10 +38,11 @@ import org.apache.camel.karavan.service.ConfigService;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.jboss.logging.Logger;
 
-import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import static org.apache.camel.karavan.KaravanConstants.*;
 import static 
org.apache.camel.karavan.service.CodeService.CAMEL_OBSERVABILITY_PORT;
@@ -53,6 +53,10 @@ public class KubernetesService {
 
     private static final Logger LOGGER = 
Logger.getLogger(KubernetesService.class.getName());
 
+    // Kinds accepted in the user editable kubernetes.yaml resource file of a 
project.
+    // Anything else (Pod, DaemonSet, ClusterRoleBinding, ...) is rejected 
before reaching the API server.
+    public static final String DEFAULT_ALLOWED_DEPLOYMENT_KINDS = 
"Deployment,Service,ConfigMap,Secret";
+
     public static final Map<String, Quantity> DEFAULT_CONTAINER_RESOURCES = 
Map.of(
             "requests.memory", new Quantity("256Mi"),
             "requests.cpu", new Quantity("500m"),
@@ -97,6 +101,15 @@ public class KubernetesService {
     @ConfigProperty(name = "karavan.openshift")
     Optional<Boolean> isOpenShift;
 
+    // Extend only together with the matching Kubernetes RBAC permissions, 
e.g. "Route" on OpenShift
+    @ConfigProperty(name = "karavan.deployment.allowed-kinds", defaultValue = 
DEFAULT_ALLOWED_DEPLOYMENT_KINDS)
+    String allowedDeploymentKinds;
+
+    private Set<String> getAllowedDeploymentKinds() {
+        return Arrays.stream(allowedDeploymentKinds.split(","))
+                .map(String::trim).filter(s -> 
!s.isEmpty()).collect(Collectors.toSet());
+    }
+
     public void runBuildProject(String projectId, String podFragment, 
Map<String, String> envVars) {
         try (KubernetesClient client = kubernetesClient()) {
             String containerName = projectId + BUILDER_SUFFIX;
@@ -247,15 +260,33 @@ public class KubernetesService {
     }
 
     public void startDeployment(String resources, Map<String, String> labels) {
+        KubernetesList list;
+        try {
+            list = Serialization.unmarshal(resources, KubernetesList.class);
+        } catch (Exception e) {
+            throw new IllegalArgumentException("Invalid Kubernetes resources: 
" + e.getMessage());
+        }
+        if (list == null || list.getItems() == null || 
list.getItems().isEmpty()) {
+            throw new IllegalArgumentException("No Kubernetes resources to 
apply");
+        }
+        // The resource file is user editable, so it can not be trusted: only 
the resource kinds
+        // an integration is made of are accepted, and pod specs must not ask 
for host level access.
+        Set<String> allowedKinds = getAllowedDeploymentKinds();
+        list.getItems().forEach(item -> validateDeploymentResource(item, 
allowedKinds));
         try (KubernetesClient client = kubernetesClient()) {
-            KubernetesList list = Serialization.unmarshal(resources, 
KubernetesList.class);
             list.getItems().forEach(item -> {
-                if (labels != null ) {
-                    item.getMetadata().getLabels().putAll(labels);
-                    if (item instanceof Deployment deployment) {
-                        
deployment.getSpec().getTemplate().getMetadata().getLabels().putAll(labels);
+                if (labels != null) {
+                    putLabels(item.getMetadata(), labels);
+                    if (item instanceof Deployment deployment && 
deployment.getSpec() != null && deployment.getSpec().getTemplate() != null) {
+                        var template = deployment.getSpec().getTemplate();
+                        if (template.getMetadata() == null) {
+                            template.setMetadata(new ObjectMeta());
+                        }
+                        putLabels(template.getMetadata(), labels);
                     }
                 }
+                // Pin the namespace: a resource must never be applied outside 
of Karavan's namespace
+                item.getMetadata().setNamespace(getNamespace());
                 
client.resource(item).inNamespace(getNamespace()).serverSideApply();
             });
         } catch (Exception ex) {
@@ -263,6 +294,72 @@ public class KubernetesService {
         }
     }
 
+    private static void putLabels(ObjectMeta meta, Map<String, String> labels) 
{
+        if (meta.getLabels() == null) {
+            meta.setLabels(new HashMap<>());
+        }
+        meta.getLabels().putAll(labels);
+    }
+
+    static void validateDeploymentResource(HasMetadata item, Set<String> 
allowedKinds) {
+        String kind = item.getKind();
+        if (!allowedKinds.contains(kind)) {
+            throw new IllegalArgumentException("Resource kind is not allowed: 
" + kind
+                    + ". Allowed kinds: " + String.join(", ", allowedKinds));
+        }
+        if (item.getMetadata() == null || item.getMetadata().getName() == 
null) {
+            throw new IllegalArgumentException("Resource of kind " + kind + " 
has no metadata.name");
+        }
+        if (item instanceof Deployment deployment && deployment.getSpec() != 
null && deployment.getSpec().getTemplate() != null) {
+            validatePodSpec(deployment.getSpec().getTemplate().getSpec());
+        }
+    }
+
+    private static void validatePodSpec(PodSpec spec) {
+        if (spec == null) {
+            return;
+        }
+        if (Boolean.TRUE.equals(spec.getHostNetwork())) {
+            throw new IllegalArgumentException("hostNetwork is not allowed");
+        }
+        if (Boolean.TRUE.equals(spec.getHostPID())) {
+            throw new IllegalArgumentException("hostPID is not allowed");
+        }
+        if (Boolean.TRUE.equals(spec.getHostIPC())) {
+            throw new IllegalArgumentException("hostIPC is not allowed");
+        }
+        if (spec.getVolumes() != null) {
+            spec.getVolumes().stream().filter(v -> v.getHostPath() != 
null).findFirst().ifPresent(v -> {
+                throw new IllegalArgumentException("hostPath volume is not 
allowed: " + v.getName());
+            });
+        }
+        Stream.concat(
+                spec.getContainers() != null ? spec.getContainers().stream() : 
Stream.empty(),
+                spec.getInitContainers() != null ? 
spec.getInitContainers().stream() : Stream.empty()
+        ).forEach(KubernetesService::validateContainer);
+    }
+
+    private static void validateContainer(Container container) {
+        SecurityContext securityContext = container.getSecurityContext();
+        if (securityContext != null) {
+            if (Boolean.TRUE.equals(securityContext.getPrivileged())) {
+                throw new IllegalArgumentException("privileged container is 
not allowed: " + container.getName());
+            }
+            if 
(Boolean.TRUE.equals(securityContext.getAllowPrivilegeEscalation())) {
+                throw new IllegalArgumentException("allowPrivilegeEscalation 
is not allowed: " + container.getName());
+            }
+            if (securityContext.getCapabilities() != null && 
securityContext.getCapabilities().getAdd() != null
+                    && !securityContext.getCapabilities().getAdd().isEmpty()) {
+                throw new IllegalArgumentException("adding Linux capabilities 
is not allowed: " + container.getName());
+            }
+        }
+        if (container.getPorts() != null) {
+            container.getPorts().stream().filter(p -> p.getHostPort() != 
null).findFirst().ifPresent(p -> {
+                throw new IllegalArgumentException("hostPort is not allowed: " 
+ container.getName());
+            });
+        }
+    }
+
     public void deleteDeployment(String name) {
         try (KubernetesClient client = kubernetesClient()) {
             LOGGER.info("Delete deployment: " + name + " in the namespace: " + 
getNamespace());
@@ -731,43 +828,4 @@ public class KubernetesService {
         }
         return list;
     }
-
-    public void execCommandInDeployment(String deploymentName, String command) 
{
-        try (KubernetesClient client = kubernetesClient()) {
-            Deployment deployment = 
client.apps().deployments().inNamespace(getNamespace()).withName(deploymentName).get();
-
-            if (deployment != null && deployment.getSpec().getSelector() != 
null) {
-                Map<String, String> matchLabels = 
deployment.getSpec().getSelector().getMatchLabels();
-                List<Pod> pods = 
client.pods().inNamespace(getNamespace()).withLabels(matchLabels).list().getItems();
-
-                if (!pods.isEmpty()) {
-                    Pod pod = pods.getFirst();
-                    String podName = pod.getMetadata().getName();
-                    String containerName = 
pod.getSpec().getContainers().getFirst().getName();
-
-                    LOGGER.info("Executing command in pod " + podName + " 
(container: " + containerName + ")");
-
-                    try (ExecWatch watch = 
client.pods().inNamespace(getNamespace()).withName(podName)
-                            .inContainer(containerName)
-                            .writingOutput(OutputStream.nullOutputStream())
-                            .writingError(OutputStream.nullOutputStream())
-                            .exec("sh", "-c", command)) {
-
-                        // Block the thread until the command finishes (or 
times out after 30s)
-                        // This keeps the WebSocket open long enough for the 
command to run.
-                        watch.exitCode().get(30, TimeUnit.SECONDS);
-
-                    } catch (Exception e) {
-                        LOGGER.error("Exec failed or timed out: " + 
e.getMessage());
-                    }
-                } else {
-                    LOGGER.warn("No pods found for deployment: " + 
deploymentName);
-                }
-            } else {
-                LOGGER.warn("Deployment not found or has no selector: " + 
deploymentName);
-            }
-        } catch (Exception e) {
-            LOGGER.error("Error executing command in container for deployment 
" + deploymentName + ": " + e.getMessage(), e);
-        }
-    }
 }
diff --git a/karavan-app/src/main/resources/application.properties 
b/karavan-app/src/main/resources/application.properties
index d841ed32..765d6a34 100644
--- a/karavan-app/src/main/resources/application.properties
+++ b/karavan-app/src/main/resources/application.properties
@@ -31,6 +31,10 @@ karavan.builder.service.account=karavan
 karavan.secret.name=karavan
 karavan.docker.network=karavan
 
+# Resource kinds accepted in a project kubernetes.yaml file. Extend only 
together
+# with the matching Kubernetes RBAC permissions, e.g. add "Route" on OpenShift.
+deployment.allowed-kinds=Deployment,Service,ConfigMap,Secret,Ingress
+
 # Keycloak configuration
 karavan.keycloak.url=http://localhost:8079
 karavan.keycloak.realm=karavan

Reply via email to