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

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


The following commit(s) were added to refs/heads/main by this push:
     new f3b42bc  Serverless mode prototype (#113)
f3b42bc is described below

commit f3b42bc43758ba67af829a99c865bcd23afb512a
Author: Marat Gubaidullin <marat.gubaidul...@gmail.com>
AuthorDate: Sun Nov 21 19:56:12 2021 -0500

    Serverless mode prototype (#113)
---
 karavan-app/pom.xml                                |  10 ++
 .../camel/karavan/api/IntegrationResource.java     |  69 +++++++++-----
 .../camel/karavan/service/CamelKService.java       | 106 +++++++++++++++++++++
 .../src/main/resources/application.properties      |   4 +-
 karavan-app/src/main/webapp/src/Main.tsx           |  15 +--
 karavan-app/src/main/webapp/src/api/KaravanApi.tsx |   1 +
 karavan-app/src/main/webapp/src/index.css          |  19 ++++
 .../main/webapp/src/integrations/DesignerPage.tsx  |  21 ++--
 .../webapp/src/integrations/IntegrationCard.tsx    |  16 ++--
 .../webapp/src/integrations/IntegrationPage.tsx    |  11 ++-
 10 files changed, 224 insertions(+), 48 deletions(-)

diff --git a/karavan-app/pom.xml b/karavan-app/pom.xml
index 5182b97..2eea591 100644
--- a/karavan-app/pom.xml
+++ b/karavan-app/pom.xml
@@ -33,6 +33,7 @@
         <surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
         <version.camel-quarkus>2.4.0</version.camel-quarkus>
         <version.camel-kamelet>0.5.0</version.camel-kamelet>
+        <version.camel-k-client>5.10.1</version.camel-k-client>
     </properties>
     <dependencyManagement>
         <dependencies>
@@ -68,6 +69,15 @@
         </dependency>
         <dependency>
             <groupId>io.quarkus</groupId>
+            <artifactId>quarkus-kubernetes-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.fabric8</groupId>
+            <artifactId>camel-k-client</artifactId>
+            <version>${version.camel-k-client}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.quarkus</groupId>
             <artifactId>quarkus-smallrye-openapi</artifactId>
         </dependency>
         <dependency>
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/api/IntegrationResource.java
 
b/karavan-app/src/main/java/org/apache/camel/karavan/api/IntegrationResource.java
index d458f93..2da60f8 100644
--- 
a/karavan-app/src/main/java/org/apache/camel/karavan/api/IntegrationResource.java
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/api/IntegrationResource.java
@@ -17,6 +17,7 @@
 package org.apache.camel.karavan.api;
 
 import io.vertx.core.Vertx;
+import org.apache.camel.karavan.service.CamelKService;
 import org.apache.camel.karavan.service.FileSystemService;
 import org.apache.camel.karavan.service.GitService;
 import org.eclipse.jgit.api.errors.GitAPIException;
@@ -28,11 +29,14 @@ import javax.ws.rs.core.MediaType;
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 @Path("/integration")
 public class IntegrationResource {
 
-    private static final String CLOUD_MODE = "cloud";
+    private static final String GITOPS_MODE = "gitops";
+    private static final String SERVERLESS_MODE = "serverless";
 
     @ConfigProperty(name = "karavan.mode", defaultValue = "local")
     String mode;
@@ -46,26 +50,35 @@ public class IntegrationResource {
     @Inject
     FileSystemService fileSystemService;
 
+    @Inject
+    CamelKService camelKService;
+
     @GET
     @Produces(MediaType.APPLICATION_JSON)
-    public List<String> getList(@HeaderParam("username") String username) 
throws GitAPIException {
-        if (mode.equals(CLOUD_MODE)){
-            String dir = gitService.pullIntegrations(username);
-            return fileSystemService.getIntegrationList(dir);
-        } else {
-            return fileSystemService.getIntegrationList();
+    public Map<String, String> getList(@HeaderParam("username") String 
username) throws GitAPIException, IOException {
+        switch (mode){
+            case SERVERLESS_MODE:
+                return camelKService.getIntegrationList();
+            case GITOPS_MODE:
+                String dir = gitService.pullIntegrations(username);
+                return 
fileSystemService.getIntegrationList(dir).stream().collect(Collectors.toMap(s 
-> s, s-> ""));
+            default:
+                return 
fileSystemService.getIntegrationList().stream().collect(Collectors.toMap(s -> 
s, s-> ""));
         }
     }
 
     @GET
     @Produces(MediaType.TEXT_PLAIN)
     @Path("/{name}")
-    public String getYaml(@HeaderParam("username") String username, 
@PathParam("name") String name) throws GitAPIException {
-        if (mode.equals(CLOUD_MODE)){
-            String dir = gitService.pullIntegrations(username);
-            return fileSystemService.getFile(dir, name);
-        } else {
-            return fileSystemService.getIntegrationsFile(name);
+    public String getYaml(@HeaderParam("username") String username, 
@PathParam("name") String name) throws GitAPIException, IOException {
+        switch (mode){
+            case SERVERLESS_MODE:
+                return camelKService.getIntegration(name);
+            case GITOPS_MODE:
+                String dir = gitService.pullIntegrations(username);
+                return fileSystemService.getFile(dir, name);
+            default:
+                return fileSystemService.getIntegrationsFile(name);
         }
     }
 
@@ -74,10 +87,16 @@ public class IntegrationResource {
     @Consumes(MediaType.TEXT_PLAIN)
     @Path("/{name}")
     public String save(@HeaderParam("username") String username, 
@PathParam("name") String name, String yaml) throws GitAPIException, 
IOException, URISyntaxException {
-        if (mode.equals(CLOUD_MODE)){
-            gitService.save(username, name, yaml);
-        } else {
-            fileSystemService.saveIntegrationsFile(name, yaml);
+        switch (mode){
+            case SERVERLESS_MODE:
+                camelKService.applyIntegration(name, yaml);
+                break;
+            case GITOPS_MODE:
+                gitService.save(username, name, yaml);
+                break;
+            default:
+                fileSystemService.saveIntegrationsFile(name, yaml);
+                break;
         }
         return yaml;
     }
@@ -87,7 +106,7 @@ public class IntegrationResource {
     @Consumes(MediaType.TEXT_PLAIN)
     @Path("/{name}")
     public String publish(@HeaderParam("username") String username, 
@PathParam("name") String name, String yaml) throws GitAPIException, 
IOException, URISyntaxException {
-        if (mode.equals(CLOUD_MODE)) {
+        if (mode.equals(GITOPS_MODE)) {
             gitService.save(username, name, yaml);
             gitService.publish(username, name);
         }
@@ -97,10 +116,16 @@ public class IntegrationResource {
     @DELETE
     @Path("/{name}")
     public void delete(@HeaderParam("username") String username, 
@PathParam("name") String name) throws GitAPIException, IOException, 
URISyntaxException {
-        if (mode.equals(CLOUD_MODE)){
-            gitService.delete(username, name);
-        } else {
-            fileSystemService.deleteIntegration(name);
+        switch (mode){
+            case SERVERLESS_MODE:
+                camelKService.deleteIntegration(name);
+                break;
+            case GITOPS_MODE:
+                gitService.delete(username, name);
+                break;
+            default:
+                fileSystemService.deleteIntegration(name);
+                break;
         }
     }
 }
\ No newline at end of file
diff --git 
a/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelKService.java 
b/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelKService.java
new file mode 100644
index 0000000..89a2e4a
--- /dev/null
+++ 
b/karavan-app/src/main/java/org/apache/camel/karavan/service/CamelKService.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.karavan.service;
+
+import io.fabric8.camelk.client.CamelKClient;
+import io.fabric8.camelk.client.DefaultCamelKClient;
+import io.fabric8.camelk.v1.Integration;
+import io.fabric8.camelk.v1.IntegrationList;
+import io.fabric8.camelk.v1.SourceSpec;
+import io.vertx.core.Vertx;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.jboss.logging.Logger;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@ApplicationScoped
+public class CamelKService {
+
+    private CamelKClient camelk = new DefaultCamelKClient();
+
+    private static final String header =
+            "apiVersion: camel.apache.org/v1\n" +
+            "kind: Integration\n" +
+            "metadata:\n" +
+            "  name: %s\n" +
+            "spec:\n" +
+            "  flows:\n";
+
+    @Inject
+    Vertx vertx;
+
+    private static final Logger LOGGER = 
Logger.getLogger(CamelKService.class.getName());
+
+    public Map<String, String> getIntegrationList() throws IOException {
+        IntegrationList list = 
camelk.v1().integrations().inNamespace(getNamespace()).list();
+        return list.getItems().stream()
+                .collect(Collectors.toMap(i -> i.getMetadata().getName(),i -> 
i.getStatus().getPhase()));
+    }
+
+    public String getIntegration(String name) throws IOException {
+        StringBuilder result = new StringBuilder(String.format(header, name));
+        Integration i = 
camelk.v1().integrations().inNamespace(getNamespace()).withName(name).get();
+        Optional<SourceSpec> spec = 
i.getStatus().getGeneratedSources().stream()
+                .filter(s -> s.getContent() != null && 
!s.getContent().isEmpty())
+                .findFirst();
+        if (spec.isPresent()){
+            String content = spec.get().getContent().lines().map(s -> "    
".concat(s)).collect(Collectors.joining("\n"));
+            result.append(content);
+        }
+        System.out.println(result.toString());
+        return result.toString();
+    }
+
+    public String getNamespace() throws IOException {
+        try {
+            return 
Files.readString(Paths.get("/var/run/secrets/kubernetes.io/serviceaccount/namespace"));
+        } catch (Exception e){
+            return "karavan";
+        }
+    }
+
+    public List<String> getIntegrationList(String folder) {
+        return vertx.fileSystem().readDirBlocking(Paths.get(folder).toString())
+                .stream()
+                .filter(s -> s.endsWith(".yaml"))
+                .map(s -> {
+                    String[] parts = s.split("/");
+                    return parts[parts.length - 1];
+                }).collect(Collectors.toList());
+    }
+
+    public void applyIntegration(String name, String yaml) throws 
GitAPIException, IOException {
+        Integration i = camelk.v1().integrations().load(new 
ByteArrayInputStream(yaml.getBytes())).get();
+        camelk.v1().integrations().createOrReplace(i);
+    }
+
+    public Boolean deleteIntegration(String name) throws IOException {
+        Integration i = 
camelk.v1().integrations().inNamespace(getNamespace()).withName(name).get();
+        return camelk.v1().integrations().delete(i);
+    }
+
+
+}
diff --git a/karavan-app/src/main/resources/application.properties 
b/karavan-app/src/main/resources/application.properties
index 11ff56d..9035ce1 100644
--- a/karavan-app/src/main/resources/application.properties
+++ b/karavan-app/src/main/resources/application.properties
@@ -6,7 +6,9 @@ karavan.folder.components=components
 karavan.mode=local
 karavan.folder.integrations=integrations
 
-#karavan.mode=cloud
+#karavan.mode=serverless
+
+#karavan.mode=gitops
 karavan.git.uri=https://localhost:3000/git/karavan
 karavan.git.username=git
 karavan.git.password=gitgit
diff --git a/karavan-app/src/main/webapp/src/Main.tsx 
b/karavan-app/src/main/webapp/src/Main.tsx
index ed33daf..6e7d8a6 100644
--- a/karavan-app/src/main/webapp/src/Main.tsx
+++ b/karavan-app/src/main/webapp/src/Main.tsx
@@ -55,10 +55,10 @@ interface Props {
 
 interface State {
     version: string,
-    mode: 'local' | 'cloud',
+    mode: 'local' | 'gitops' | 'serverless',
     isNavOpen: boolean,
     pageId: 'integrations' | 'configuration' | 'kamelets' | 'designer'
-    integrations: [],
+    integrations: Map<string,string>,
     integration: Integration,
     isModalOpen: boolean,
     nameToDelete: string,
@@ -73,7 +73,7 @@ export class Main extends React.Component<Props, State> {
         mode: 'local',
         isNavOpen: true,
         pageId: "integrations",
-        integrations: [],
+        integrations: new Map<string,string>(),
         integration: Integration.createNew(),
         isModalOpen: false,
         nameToDelete: '',
@@ -218,7 +218,7 @@ export class Main extends React.Component<Props, State> {
                 const i = CamelYaml.yamlToIntegration(filename, code);
                 this.setState({isNavOpen: false, pageId: 'designer', 
integration: i});
             } else {
-                this.toast("Error", res.statusText, "danger");
+                this.toast("Error", res.status + ", " + res.statusText, 
"danger");
             }
         });
     };
@@ -228,10 +228,11 @@ export class Main extends React.Component<Props, State> {
     };
 
     onGetIntegrations() {
-        KaravanApi.getIntegrations((integrations: []) =>
+        KaravanApi.getIntegrations((integrations: {}) => {
+            const map:Map<string, string> = new 
Map(Object.entries(integrations));
             this.setState({
-                integrations: integrations, request: uuidv4()
-            }));
+                integrations: map, request: uuidv4()
+            })});
     };
 
     render() {
diff --git a/karavan-app/src/main/webapp/src/api/KaravanApi.tsx 
b/karavan-app/src/main/webapp/src/api/KaravanApi.tsx
index 348e887..5c1e43b 100644
--- a/karavan-app/src/main/webapp/src/api/KaravanApi.tsx
+++ b/karavan-app/src/main/webapp/src/api/KaravanApi.tsx
@@ -30,6 +30,7 @@ export const KaravanApi = {
         axios.get('/integration/' + name,
             {headers: {'Accept': 'text/plain', 'username': 'cameleer'}})
             .then(res => {
+                console.log(res.data);
                 after(res);
             }).catch(err => {
             after(err);
diff --git a/karavan-app/src/main/webapp/src/index.css 
b/karavan-app/src/main/webapp/src/index.css
index b6c27e6..e9d351c 100644
--- a/karavan-app/src/main/webapp/src/index.css
+++ b/karavan-app/src/main/webapp/src/index.css
@@ -40,3 +40,22 @@
 .logo .pf-c-title {
   color: #e97826;
 }
+
+.karavan .integration-card .pf-c-card__footer {
+  text-align: end;
+}
+
+.karavan .integration-card .running{
+  color: green;
+  font-weight: bold;
+}
+
+.karavan .integration-card .error{
+  color: red;
+  font-weight: bold;
+}
+
+.karavan .integration-card .normal{
+  color: initial;
+  font-weight: initial;
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webapp/src/integrations/DesignerPage.tsx 
b/karavan-app/src/main/webapp/src/integrations/DesignerPage.tsx
index 5b33193..d5c9cb7 100644
--- a/karavan-app/src/main/webapp/src/integrations/DesignerPage.tsx
+++ b/karavan-app/src/main/webapp/src/integrations/DesignerPage.tsx
@@ -17,7 +17,7 @@ import FileSaver from "file-saver";
 
 interface Props {
     integration: Integration,
-    mode: 'local' | 'cloud',
+    mode: 'local' | 'gitops' | 'serverless',
 }
 
 interface State {
@@ -80,7 +80,7 @@ export class DesignerPage extends React.Component<Props, 
State> {
     }
 
     save = (name: string, yaml: string) => {
-        this.setState({name: name, yaml:yaml})
+        this.setState({name: name, yaml: yaml})
     }
 
     download = () => {
@@ -91,20 +91,27 @@ export class DesignerPage extends React.Component<Props, 
State> {
     tools = (view: "design" | "code") => (
         <Toolbar id="toolbar-group-types">
             <ToolbarContent>
-                {this.props.mode === 'cloud' &&
-                <ToolbarItem>
-                    <Button variant="secondary" icon={<PublishIcon/>} 
onClick={e => this.publish()}>Publish</Button>
-                </ToolbarItem>
-                }
                 <ToolbarItem>
                     <Button variant="secondary" icon={<CopyIcon/>} onClick={e 
=> this.copy()}>Copy</Button>
                 </ToolbarItem>
                 <ToolbarItem>
                     <Button variant="secondary" icon={<DownloadIcon/>} 
onClick={e => this.download()}>Download</Button>
                 </ToolbarItem>
+                {this.props.mode === 'gitops' &&
+                <ToolbarItem>
+                    <Button variant="secondary" icon={<PublishIcon/>} 
onClick={e => this.publish()}>Publish</Button>
+                </ToolbarItem>
+                }
+                {this.props.mode === 'serverless' &&
+                <ToolbarItem>
+                    <Button variant="primary" icon={<PublishIcon/>} onClick={e 
=> this.post()}>Apply</Button>
+                </ToolbarItem>
+                }
+                {this.props.mode !== 'serverless' &&
                 <ToolbarItem>
                     <Button variant="secondary" icon={<SaveIcon/>} onClick={e 
=> this.post()}>Save</Button>
                 </ToolbarItem>
+                }
             </ToolbarContent>
         </Toolbar>);
 
diff --git a/karavan-app/src/main/webapp/src/integrations/IntegrationCard.tsx 
b/karavan-app/src/main/webapp/src/integrations/IntegrationCard.tsx
index 4467772..0409807 100644
--- a/karavan-app/src/main/webapp/src/integrations/IntegrationCard.tsx
+++ b/karavan-app/src/main/webapp/src/integrations/IntegrationCard.tsx
@@ -8,41 +8,43 @@ import {CamelUi} from "../designer/api/CamelUi";
 
 interface Props {
     name: string,
+    status?: string,
     onClick: any
     onDelete: any
 }
 
 interface State {
-    name: string,
 }
 
 export class IntegrationCard extends React.Component<Props, State> {
 
     public state: State = {
-        name: this.props.name
     };
 
     private click(evt: React.MouseEvent) {
         evt.stopPropagation();
-        this.props.onClick.call(this, this.state.name)
+        this.props.onClick.call(this, this.props.name)
     }
 
     private delete(evt: React.MouseEvent) {
         evt.stopPropagation();
-        this.props.onDelete.call(this, this.state.name);
+        this.props.onDelete.call(this, this.props.name);
     }
 
     render() {
         return (
-            <Card isHoverable isCompact key={this.state.name} 
className="integration-card" onClick={event => this.click(event)}>
+            <Card isHoverable isCompact key={this.props.name} 
className="integration-card" onClick={event => this.click(event)}>
                 <CardHeader>
                     <img src={CamelUi.getIconForName("camel")} alt='icon' 
className="icon"/>
                     <CardActions>
                         <Button variant="link" className="delete-button" 
onClick={e => this.delete(e)}><DeleteIcon/></Button>
                     </CardActions>
                 </CardHeader>
-                <CardTitle>{CamelUi.titleFromName(this.state.name)}</CardTitle>
-                <CardBody>{this.state.name}</CardBody>
+                <CardTitle>{CamelUi.titleFromName(this.props.name)}</CardTitle>
+                <CardBody>{this.props.name}</CardBody>
+                <CardFooter className={this.props.status === 'Running' ? 
'running' : (this.props.status === 'Error' ? 'error' : 'normal')}>
+                    {this.props.status}
+                </CardFooter>
             </Card>
         );
     }
diff --git a/karavan-app/src/main/webapp/src/integrations/IntegrationPage.tsx 
b/karavan-app/src/main/webapp/src/integrations/IntegrationPage.tsx
index 28dfe4b..1977e2f 100644
--- a/karavan-app/src/main/webapp/src/integrations/IntegrationPage.tsx
+++ b/karavan-app/src/main/webapp/src/integrations/IntegrationPage.tsx
@@ -19,7 +19,7 @@ import {Integration} from "../designer/model/CamelModel";
 import {CamelUi} from "../designer/api/CamelUi";
 
 interface Props {
-    integrations: []
+    integrations: Map<string,string>
     onSelect: any
     onCreate: any
     onDelete: any
@@ -29,7 +29,7 @@ interface Props {
 interface State {
     repository: string,
     path: string,
-    integrations: [],
+    integrations: Map<string,string>,
     isModalOpen: boolean,
     newName: string
     crd: boolean
@@ -83,8 +83,11 @@ export class IntegrationPage extends React.Component<Props, 
State> {
                 <MainToolbar title={this.title()} tools={this.tools()}/>
                 <PageSection isFilled className="integration-page">
                     <Gallery hasGutter>
-                        {this.state.integrations.map(value => (
-                            <IntegrationCard key={value} name={value} 
onDelete={this.props.onDelete}
+                        {Array.from(this.state.integrations.keys()).map(key => 
(
+                             <IntegrationCard key={key}
+                                              name={key}
+                                              
status={this.state.integrations.get(key)}
+                                              onDelete={this.props.onDelete}
                                              onClick={this.props.onSelect}/>
                         ))}
                     </Gallery>

Reply via email to