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>