diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a79a61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +target/ +*.iml +tmp/ +.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd985f1 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# CoolStore Microservices Online Store + +CoolStore is an online store web application built using Spring Boot, WildFly Swarm, Eclipse Vert.x, +Node.js and AngularJS adopting the microservices architecture. + +* **Web**: A Node.js/Angular front-end +* **API Gateway**: vert.x service aggregates API calls to back-end services and provides a condenses REST API for front-end +* **Catalog**: Spring Boot service exposing REST API for the product catalog and product information +* **Inventory**: WildFly Swarm service exposing REST API for product's inventory status +* **Cart**: Spring Boot service exposing REST API for shopping cart + +``` + +-------------+ + | | + | Web | + | | + | Node.js | + | AngularJS | + +------+------+ + | + v + +------+------+ + | | + | API Gateway | + | | + | Vert.x | + | | + +------+------+ + | + +---------+---------+-------------------+ + v v v + +------+------+ +------+------+ +------+------+ + | | | | | | + | Catalog | | Inventory | | Cart | + | | | | | | + | Spring Boot | |WildFly Swarm| | Spring Boot | + | | | | | | + +-------------+ +-------------+ +-------------+ +``` \ No newline at end of file diff --git a/cart-spring-boot/build.gradle b/cart-spring-boot/build.gradle new file mode 100644 index 0000000..7b208ab --- /dev/null +++ b/cart-spring-boot/build.gradle @@ -0,0 +1,37 @@ +plugins { + id "java" + id "io.spring.dependency-management" version "1.0.1.RELEASE" + id 'org.springframework.boot' version '1.5.2.RELEASE' +} + +version = "1.0.0-SNAPSHOT" + +jar { + archiveName = 'cart.jar' +} + +repositories { + mavenLocal() + maven { + url "http://nexus.lab-infra.svc.cluster.local:8081/content/groups/public/" + } + mavenCentral() +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:Camden.SR2" + } +} + +dependencies { + compile("org.springframework.boot:spring-boot-starter-web") + compile("org.springframework.boot:spring-boot-starter-jersey") + compile("org.springframework.boot:spring-boot-starter-actuator") + compile("org.springframework.cloud:spring-cloud-starter-feign") { + exclude group: "com.sun.jersey", module: "jersey-client" + } + + runtime("org.springframework.boot:spring-boot-starter-tomcat") + testCompile("org.springframework.boot:spring-boot-starter-test") +} \ No newline at end of file diff --git a/cart-spring-boot/pom.xml b/cart-spring-boot/pom.xml new file mode 100644 index 0000000..b573309 --- /dev/null +++ b/cart-spring-boot/pom.xml @@ -0,0 +1,142 @@ + + + 4.0.0 + + com.redhat.coolstore + cart + 1.0.0-SNAPSHOT + jar + + cart-service + + + org.springframework.boot + spring-boot-starter-parent + 1.4.2.RELEASE + + + + + + org.springframework.cloud + spring-cloud-dependencies + Camden.SR2 + pom + import + + + + + + UTF-8 + UTF-8 + 1.8 + com.redhat.coolstore.CartServiceApplication + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-jersey + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.cloud + spring-cloud-starter-feign + + + com.sun.jersey + jersey-client + + + + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + io.fabric8 + fabric8-maven-plugin + 3.2.28 + + none + -service + + + + + + + + cart-tests + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + true + + + + + + + + + discount-tests + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + true + + + + + + + + + diff --git a/cart-spring-boot/src/main/config/settings.xml b/cart-spring-boot/src/main/config/settings.xml new file mode 100644 index 0000000..ac4e99d --- /dev/null +++ b/cart-spring-boot/src/main/config/settings.xml @@ -0,0 +1,39 @@ + + + + + + nexus.default + http://nexus.lab-infra.svc.cluster.local:8081/content/groups/public/ + external:* + + + + + + nexus + + + central + http://central + true + true + + + + + central + http://central + true + true + + + + + + + nexus + + diff --git a/cart-spring-boot/src/main/java/com/redhat/coolstore/CartServiceApplication.java b/cart-spring-boot/src/main/java/com/redhat/coolstore/CartServiceApplication.java new file mode 100644 index 0000000..0bacd76 --- /dev/null +++ b/cart-spring-boot/src/main/java/com/redhat/coolstore/CartServiceApplication.java @@ -0,0 +1,14 @@ +package com.redhat.coolstore; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.feign.EnableFeignClients; + +@SpringBootApplication +@EnableFeignClients +public class CartServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(CartServiceApplication.class, args); + } +} diff --git a/cart-spring-boot/src/main/java/com/redhat/coolstore/model/Product.java b/cart-spring-boot/src/main/java/com/redhat/coolstore/model/Product.java new file mode 100644 index 0000000..02ba771 --- /dev/null +++ b/cart-spring-boot/src/main/java/com/redhat/coolstore/model/Product.java @@ -0,0 +1,54 @@ +package com.redhat.coolstore.model; + +import java.io.Serializable; + +public class Product implements Serializable { + + private static final long serialVersionUID = -7304814269819778382L; + private String itemId; + private String name; + private String desc; + private double price; + + public Product() { + + } + + public Product(String itemId, String name, String desc, double price) { + super(); + this.itemId = itemId; + this.name = name; + this.desc = desc; + this.price = price; + } + public String getItemId() { + return itemId; + } + public void setItemId(String itemId) { + this.itemId = itemId; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public String getDesc() { + return desc; + } + public void setDesc(String desc) { + this.desc = desc; + } + public double getPrice() { + return price; + } + public void setPrice(double price) { + this.price = price; + } + + @Override + public String toString() { + return "Product [itemId=" + itemId + ", name=" + name + ", desc=" + + desc + ", price=" + price + "]"; + } +} diff --git a/cart-spring-boot/src/main/java/com/redhat/coolstore/model/Promotion.java b/cart-spring-boot/src/main/java/com/redhat/coolstore/model/Promotion.java new file mode 100644 index 0000000..d0f8faa --- /dev/null +++ b/cart-spring-boot/src/main/java/com/redhat/coolstore/model/Promotion.java @@ -0,0 +1,41 @@ +package com.redhat.coolstore.model; + +public class Promotion { + + private String itemId; + + private double percentOff; + + public Promotion() { + + } + + public Promotion(String itemId, double percentOff) { + super(); + this.itemId = itemId; + this.percentOff = percentOff; + } + + public String getItemId() { + return itemId; + } + + public void setItemId(String itemId) { + this.itemId = itemId; + } + + public double getPercentOff() { + return percentOff; + } + + public void setPercentOff(double percentOff) { + this.percentOff = percentOff; + } + + @Override + public String toString() { + return "Promotion [itemId=" + itemId + ", percentOff=" + percentOff + + "]"; + } + +} diff --git a/cart-spring-boot/src/main/java/com/redhat/coolstore/model/ShoppingCart.java b/cart-spring-boot/src/main/java/com/redhat/coolstore/model/ShoppingCart.java new file mode 100644 index 0000000..efab8e8 --- /dev/null +++ b/cart-spring-boot/src/main/java/com/redhat/coolstore/model/ShoppingCart.java @@ -0,0 +1,113 @@ +package com.redhat.coolstore.model; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class ShoppingCart implements Serializable { + + private static final long serialVersionUID = -1108043957592113528L; + + private double cartItemTotal; + + private double cartItemPromoSavings; + + private double shippingTotal; + + private double shippingPromoSavings; + + private double cartTotal; + + private List shoppingCartItemList = new ArrayList(); + + public ShoppingCart() { + + } + + public List getShoppingCartItemList() { + return shoppingCartItemList; + } + + public void setShoppingCartItemList(List shoppingCartItemList) { + this.shoppingCartItemList = shoppingCartItemList; + } + + public void resetShoppingCartItemList() { + shoppingCartItemList = new ArrayList(); + } + + public void addShoppingCartItem(ShoppingCartItem sci) { + + if ( sci != null ) { + + shoppingCartItemList.add(sci); + + } + + } + + public boolean removeShoppingCartItem(ShoppingCartItem sci) { + + boolean removed = false; + + if ( sci != null ) { + + removed = shoppingCartItemList.remove(sci); + + } + + return removed; + + } + + public double getCartItemTotal() { + return cartItemTotal; + } + + public void setCartItemTotal(double cartItemTotal) { + this.cartItemTotal = cartItemTotal; + } + + public double getShippingTotal() { + return shippingTotal; + } + + public void setShippingTotal(double shippingTotal) { + this.shippingTotal = shippingTotal; + } + + public double getCartTotal() { + return cartTotal; + } + + public void setCartTotal(double cartTotal) { + this.cartTotal = cartTotal; + } + + public double getCartItemPromoSavings() { + return cartItemPromoSavings; + } + + public void setCartItemPromoSavings(double cartItemPromoSavings) { + this.cartItemPromoSavings = cartItemPromoSavings; + } + + public double getShippingPromoSavings() { + return shippingPromoSavings; + } + + public void setShippingPromoSavings(double shippingPromoSavings) { + this.shippingPromoSavings = shippingPromoSavings; + } + + @Override + public String toString() { + return "ShoppingCart [cartItemTotal=" + cartItemTotal + + ", cartItemPromoSavings=" + cartItemPromoSavings + + ", shippingTotal=" + shippingTotal + + ", shippingPromoSavings=" + shippingPromoSavings + + ", cartTotal=" + cartTotal + ", shoppingCartItemList=" + + shoppingCartItemList + "]"; + } + +} diff --git a/cart-spring-boot/src/main/java/com/redhat/coolstore/model/ShoppingCartItem.java b/cart-spring-boot/src/main/java/com/redhat/coolstore/model/ShoppingCartItem.java new file mode 100644 index 0000000..73c40be --- /dev/null +++ b/cart-spring-boot/src/main/java/com/redhat/coolstore/model/ShoppingCartItem.java @@ -0,0 +1,58 @@ +package com.redhat.coolstore.model; + +import java.io.Serializable; + +public class ShoppingCartItem implements Serializable { + + private static final long serialVersionUID = 6964558044240061049L; + + private double price; + private int quantity; + private double promoSavings; + private Product product; + + public ShoppingCartItem() { + + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + public Product getProduct() { + return product; + } + + public void setProduct(Product product) { + this.product = product; + } + + public int getQuantity() { + return quantity; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } + + public double getPromoSavings() { + return promoSavings; + } + + public void setPromoSavings(double promoSavings) { + this.promoSavings = promoSavings; + } + + @Override + public String toString() { + return "ShoppingCartItem [price=" + price + ", quantity=" + quantity + + ", promoSavings=" + promoSavings + ", product=" + product + + "]"; + } + + +} diff --git a/cart-spring-boot/src/main/java/com/redhat/coolstore/rest/CartEndpoint.java b/cart-spring-boot/src/main/java/com/redhat/coolstore/rest/CartEndpoint.java new file mode 100644 index 0000000..7582256 --- /dev/null +++ b/cart-spring-boot/src/main/java/com/redhat/coolstore/rest/CartEndpoint.java @@ -0,0 +1,167 @@ +package com.redhat.coolstore.rest; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; + +import com.redhat.coolstore.model.Product; +import com.redhat.coolstore.model.ShoppingCart; +import com.redhat.coolstore.model.ShoppingCartItem; +import com.redhat.coolstore.service.ShoppingCartService; + + +@RestController +@Scope(scopeName = WebApplicationContext.SCOPE_SESSION) +@Path("/cart") +public class CartEndpoint implements Serializable { + private static final Logger LOG = LoggerFactory.getLogger(CartEndpoint.class); + + /** + * + */ + private static final long serialVersionUID = -7227732980791688773L; + + @Autowired + private ShoppingCartService shoppingCartService; + + @GET + @Path("/{cartId}") + @Produces(MediaType.APPLICATION_JSON) + public ShoppingCart getCart(@PathParam("cartId") String cartId) { + + return shoppingCartService.getShoppingCart(cartId); + } + + @POST + @Path("/{cartId}/{itemId}/{quantity}") + @Produces(MediaType.APPLICATION_JSON) + public ShoppingCart add(@PathParam("cartId") String cartId, + @PathParam("itemId") String itemId, + @PathParam("quantity") int quantity) throws Exception { + ShoppingCart cart = shoppingCartService.getShoppingCart(cartId); + + Product product = shoppingCartService.getProduct(itemId); + + if (product == null) { + LOG.warn("Invalid product {} request to get added to the shopping cart. No product added", itemId); + return cart; + } + + ShoppingCartItem sci = new ShoppingCartItem(); + sci.setProduct(product); + sci.setQuantity(quantity); + sci.setPrice(product.getPrice()); + cart.addShoppingCartItem(sci); + + try { + shoppingCartService.priceShoppingCart(cart); + cart.setShoppingCartItemList(dedupeCartItems(cart.getShoppingCartItemList())); + } catch (Exception ex) { + cart.removeShoppingCartItem(sci); + throw ex; + } + + return cart; + } + + @POST + @Path("/{cartId}/{tmpId}") + @Produces(MediaType.APPLICATION_JSON) + public ShoppingCart set(@PathParam("cartId") String cartId, + @PathParam("tmpId") String tmpId) throws Exception { + + ShoppingCart cart = shoppingCartService.getShoppingCart(cartId); + ShoppingCart tmpCart = shoppingCartService.getShoppingCart(tmpId); + + if (tmpCart != null) { + cart.resetShoppingCartItemList(); + cart.setShoppingCartItemList(tmpCart.getShoppingCartItemList()); + } + + try { + shoppingCartService.priceShoppingCart(cart); + cart.setShoppingCartItemList(dedupeCartItems(cart.getShoppingCartItemList())); + } catch (Exception ex) { + throw ex; + } + + return cart; + } + + @DELETE + @Path("/{cartId}/{itemId}/{quantity}") + @Produces(MediaType.APPLICATION_JSON) + public ShoppingCart delete(@PathParam("cartId") String cartId, + @PathParam("itemId") String itemId, + @PathParam("quantity") int quantity) throws Exception { + + List toRemoveList = new ArrayList<>(); + + ShoppingCart cart = shoppingCartService.getShoppingCart(cartId); + + cart.getShoppingCartItemList().stream() + .filter(sci -> sci.getProduct().getItemId().equals(itemId)) + .forEach(sci -> { + if (quantity >= sci.getQuantity()) { + toRemoveList.add(sci); + } else { + sci.setQuantity(sci.getQuantity() - quantity); + } + }); + + toRemoveList.forEach(cart::removeShoppingCartItem); + + shoppingCartService.priceShoppingCart(cart); + return cart; + } + + @POST + @Path("/checkout/{cartId}") + @Produces(MediaType.APPLICATION_JSON) + public ShoppingCart checkout(@PathParam("cartId") String cartId) { + // TODO: register purchase of shoppingCart items by specific user in session + ShoppingCart cart = shoppingCartService.getShoppingCart(cartId); + cart.resetShoppingCartItemList(); + shoppingCartService.priceShoppingCart(cart); + return cart; + } + + private List dedupeCartItems(List cartItems) { + List result = new ArrayList<>(); + Map quantityMap = new HashMap<>(); + for (ShoppingCartItem sci : cartItems) { + if (quantityMap.containsKey(sci.getProduct().getItemId())) { + quantityMap.put(sci.getProduct().getItemId(), quantityMap.get(sci.getProduct().getItemId()) + sci.getQuantity()); + } else { + quantityMap.put(sci.getProduct().getItemId(), sci.getQuantity()); + } + } + + for (String itemId : quantityMap.keySet()) { + Product p = shoppingCartService.getProduct(itemId); + ShoppingCartItem newItem = new ShoppingCartItem(); + newItem.setQuantity(quantityMap.get(itemId)); + newItem.setPrice(p.getPrice()); + newItem.setProduct(p); + result.add(newItem); + } + return result; + } +} diff --git a/cart-spring-boot/src/main/java/com/redhat/coolstore/rest/JerseyConfig.java b/cart-spring-boot/src/main/java/com/redhat/coolstore/rest/JerseyConfig.java new file mode 100644 index 0000000..d8c60c5 --- /dev/null +++ b/cart-spring-boot/src/main/java/com/redhat/coolstore/rest/JerseyConfig.java @@ -0,0 +1,11 @@ +package com.redhat.coolstore.rest; + +import org.glassfish.jersey.server.ResourceConfig; +import org.springframework.stereotype.Component; + +@Component +public class JerseyConfig extends ResourceConfig { + public JerseyConfig() { + register(CartEndpoint.class); + } +} diff --git a/cart-spring-boot/src/main/java/com/redhat/coolstore/service/CatalogService.java b/cart-spring-boot/src/main/java/com/redhat/coolstore/service/CatalogService.java new file mode 100644 index 0000000..7b9611c --- /dev/null +++ b/cart-spring-boot/src/main/java/com/redhat/coolstore/service/CatalogService.java @@ -0,0 +1,15 @@ +package com.redhat.coolstore.service; + +import java.util.List; + +import org.springframework.cloud.netflix.feign.FeignClient; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import com.redhat.coolstore.model.Product; + +@FeignClient(name = "catalogService", url = "${catalog.endpoint}") +interface CatalogService { + @RequestMapping(method = RequestMethod.GET, value = "/api/products") + List products(); +} \ No newline at end of file diff --git a/cart-spring-boot/src/main/java/com/redhat/coolstore/service/PromoService.java b/cart-spring-boot/src/main/java/com/redhat/coolstore/service/PromoService.java new file mode 100644 index 0000000..1f0ab3c --- /dev/null +++ b/cart-spring-boot/src/main/java/com/redhat/coolstore/service/PromoService.java @@ -0,0 +1,80 @@ +package com.redhat.coolstore.service; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.stereotype.Component; + +import com.redhat.coolstore.model.Promotion; +import com.redhat.coolstore.model.ShoppingCart; +import com.redhat.coolstore.model.ShoppingCartItem; + +@Component +public class PromoService implements Serializable { + + private static final long serialVersionUID = 2088590587856645568L; + + private String name = null; + + private Set promotionSet = null; + + public PromoService() { + promotionSet = new HashSet(); + promotionSet.add(new Promotion("329299", .25)); + } + + public void applyCartItemPromotions(ShoppingCart shoppingCart) { + if ( shoppingCart != null && shoppingCart.getShoppingCartItemList().size() > 0 ) { + Map promoMap = new HashMap(); + for (Promotion promo : getPromotions()) { + promoMap.put(promo.getItemId(), promo); + } + + for ( ShoppingCartItem sci : shoppingCart.getShoppingCartItemList() ) { + String productId = sci.getProduct().getItemId(); + Promotion promo = promoMap.get(productId); + if ( promo != null ) { + sci.setPromoSavings(sci.getProduct().getPrice() * promo.getPercentOff() * -1); + sci.setPrice(sci.getProduct().getPrice() * (1-promo.getPercentOff())); + } + } + } + + } + + public void applyShippingPromotions(ShoppingCart shoppingCart) { + if ( shoppingCart != null ) { + //PROMO: if cart total is greater than 75, free shipping + if ( shoppingCart.getCartItemTotal() >= 75) { + shoppingCart.setShippingPromoSavings(shoppingCart.getShippingTotal() * -1); + shoppingCart.setShippingTotal(0); + } + } + } + + public Set getPromotions() { + if ( promotionSet == null ) { + promotionSet = new HashSet(); + } + + return new HashSet(promotionSet); + } + + public void setPromotions(Set promotionSet) { + if ( promotionSet != null ) { + this.promotionSet = new HashSet(promotionSet); + + } else { + this.promotionSet = new HashSet(); + } + } + + @Override + public String toString() { + return "PromoService [name=" + name + ", promotionSet=" + promotionSet + + "]"; + } +} diff --git a/cart-spring-boot/src/main/java/com/redhat/coolstore/service/ShippingService.java b/cart-spring-boot/src/main/java/com/redhat/coolstore/service/ShippingService.java new file mode 100644 index 0000000..27c7218 --- /dev/null +++ b/cart-spring-boot/src/main/java/com/redhat/coolstore/service/ShippingService.java @@ -0,0 +1,25 @@ +package com.redhat.coolstore.service; + +import org.springframework.stereotype.Component; + +import com.redhat.coolstore.model.ShoppingCart; + +@Component +public class ShippingService { + + public void calculateShipping(ShoppingCart sc) { + if ( sc != null ) { + if ( sc.getCartItemTotal() >= 0 && sc.getCartItemTotal() < 25) { + sc.setShippingTotal(2.99); + } else if ( sc.getCartItemTotal() >= 25 && sc.getCartItemTotal() < 50) { + sc.setShippingTotal(4.99); + } else if ( sc.getCartItemTotal() >= 50 && sc.getCartItemTotal() < 75) { + sc.setShippingTotal(6.99); + } else if ( sc.getCartItemTotal() >= 75 && sc.getCartItemTotal() < 100) { + sc.setShippingTotal(8.99); + } else if ( sc.getCartItemTotal() >= 100 && sc.getCartItemTotal() < 10000) { + sc.setShippingTotal(10.99); + } + } + } +} diff --git a/cart-spring-boot/src/main/java/com/redhat/coolstore/service/ShoppingCartService.java b/cart-spring-boot/src/main/java/com/redhat/coolstore/service/ShoppingCartService.java new file mode 100644 index 0000000..76d6790 --- /dev/null +++ b/cart-spring-boot/src/main/java/com/redhat/coolstore/service/ShoppingCartService.java @@ -0,0 +1,10 @@ +package com.redhat.coolstore.service; + +import com.redhat.coolstore.model.Product; +import com.redhat.coolstore.model.ShoppingCart; + +public interface ShoppingCartService { + public void priceShoppingCart(ShoppingCart sc); + public ShoppingCart getShoppingCart(String cartId); + public Product getProduct(String itemId); +} diff --git a/cart-spring-boot/src/main/java/com/redhat/coolstore/service/ShoppingCartServiceImpl.java b/cart-spring-boot/src/main/java/com/redhat/coolstore/service/ShoppingCartServiceImpl.java new file mode 100644 index 0000000..d02937b --- /dev/null +++ b/cart-spring-boot/src/main/java/com/redhat/coolstore/service/ShoppingCartServiceImpl.java @@ -0,0 +1,114 @@ +package com.redhat.coolstore.service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import com.redhat.coolstore.model.Product; +import com.redhat.coolstore.model.ShoppingCart; +import com.redhat.coolstore.model.ShoppingCartItem; + +@Component +public class ShoppingCartServiceImpl implements ShoppingCartService { + + @Autowired + ShippingService ss; + + @Autowired + CatalogService catalogServie; + + @Autowired + PromoService ps; + + @Value("${application.mode}") + String mode; + + private Map cartDB = new HashMap<>(); + + private Map productMap = new HashMap<>(); + + @PostConstruct + public void init(){ + if ("dev".contentEquals(mode)) { + // cache dummy product + Product dummy = new Product(); + dummy.setItemId("666"); + dummy.setName("Dummy Product"); + dummy.setPrice(50); + dummy.setDesc("Dummy product for testing in DEV mode"); + productMap.put(dummy.getItemId(), dummy); + } + } + + @Override + public ShoppingCart getShoppingCart(String cartId) { + if (!cartDB.containsKey(cartId)) { + ShoppingCart c = new ShoppingCart(); + cartDB.put(cartId, c); + return c; + } else { + return cartDB.get(cartId); + } + } + + @Override + public void priceShoppingCart(ShoppingCart sc) { + if ( sc != null ) { + initShoppingCartForPricing(sc); + + if ( sc.getShoppingCartItemList() != null && sc.getShoppingCartItemList().size() > 0) { + ps.applyCartItemPromotions(sc); + + for (ShoppingCartItem sci : sc.getShoppingCartItemList()) { + sc.setCartItemPromoSavings(sc.getCartItemPromoSavings() + sci.getPromoSavings() * sci.getQuantity()); + sc.setCartItemTotal(sc.getCartItemTotal() + sci.getPrice() * sci.getQuantity()); + } + + ss.calculateShipping(sc); + } + + ps.applyShippingPromotions(sc); + + sc.setCartTotal(sc.getCartItemTotal() + sc.getShippingTotal()); + } + } + + private void initShoppingCartForPricing(ShoppingCart sc) { + sc.setCartItemTotal(0); + sc.setCartItemPromoSavings(0); + sc.setShippingTotal(0); + sc.setShippingPromoSavings(0); + sc.setCartTotal(0); + + for (ShoppingCartItem sci : sc.getShoppingCartItemList()) { + Product p = getProduct(sci.getProduct().getItemId()); + + //if product exist, create new product to reset price + if ( p != null ) { + sci.setProduct(new Product(p.getItemId(), p.getName(), p.getDesc(), p.getPrice())); + sci.setPrice(p.getPrice()); + } + + sci.setPromoSavings(0); + } + } + + @Override + public Product getProduct(String itemId) { + if (!productMap.containsKey(itemId)) { + // Fetch and cache products. TODO: Cache should expire at some point! + List products = catalogServie.products(); + productMap = products.stream().collect(Collectors.toMap(Product::getItemId, Function.identity())); + } + + return productMap.get(itemId); + } +} \ No newline at end of file diff --git a/cart-spring-boot/src/main/resources/application.properties b/cart-spring-boot/src/main/resources/application.properties new file mode 100644 index 0000000..6b163b6 --- /dev/null +++ b/cart-spring-boot/src/main/resources/application.properties @@ -0,0 +1,7 @@ +spring.application.name=cart-service +spring.jersey.application-path=/api + +catalog.endpoint=http://catalog:8080 + +# dev or prod +application.mode=dev diff --git a/catalog-spring-boot/pom.xml b/catalog-spring-boot/pom.xml new file mode 100755 index 0000000..644d641 --- /dev/null +++ b/catalog-spring-boot/pom.xml @@ -0,0 +1,140 @@ + + + 4.0.0 + com.redhat.cloudnative + catalog + 1.0-SNAPSHOT + CoolStore Catalog Service + CoolStore Catalog Service with Spring Boot + + 1.4.1.RELEASE + 1 + 2.3.4 + 0.2.0.RELEASE + 2.4.1 + 3.5.30 + 9.4.1212 + + + + + org.jboss.snowdrop + spring-boot-1.4-bom + ${spring-boot.bom.version} + pom + import + + + org.springframework.cloud + spring-cloud-kubernetes-dependencies + ${spring.k8s.bom.version} + pom + import + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-starter-kubernetes-config + + + io.fabric8 + kubernetes-client + ${k8s.client.version} + + + com.h2database + h2 + + + org.postgresql + postgresql + ${postgresql.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.1 + + 1.8 + 1.8 + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + -Djava.net.preferIPv4Stack=true -Dserver.port=9000 -Dspring.cloud.kubernetes.enabled=false + + + + io.fabric8 + fabric8-maven-plugin + ${fabric8.maven.plugin.version} + + + + resource + build + + + + + + + + + microservice + catalog + + + + + + + spring-boot + + + + redhat-openjdk-18/openjdk18-openshift + + + + + + + /health + + + + + + + + \ No newline at end of file diff --git a/catalog-spring-boot/src/main/java/com/redhat/cloudnative/catalog/CatalogApplication.java b/catalog-spring-boot/src/main/java/com/redhat/cloudnative/catalog/CatalogApplication.java new file mode 100755 index 0000000..5f5944d --- /dev/null +++ b/catalog-spring-boot/src/main/java/com/redhat/cloudnative/catalog/CatalogApplication.java @@ -0,0 +1,12 @@ +package com.redhat.cloudnative.catalog; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CatalogApplication { + + public static void main(String[] args) { + SpringApplication.run(CatalogApplication.class, args); + } +} diff --git a/catalog-spring-boot/src/main/java/com/redhat/cloudnative/catalog/CatalogController.java b/catalog-spring-boot/src/main/java/com/redhat/cloudnative/catalog/CatalogController.java new file mode 100755 index 0000000..ca1d853 --- /dev/null +++ b/catalog-spring-boot/src/main/java/com/redhat/cloudnative/catalog/CatalogController.java @@ -0,0 +1,28 @@ +package com.redhat.cloudnative.catalog; + +import java.util.List; +import java.util.Spliterator; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@RequestMapping(value = "/api/products") +public class CatalogController { + + @Autowired + private ProductRepository repository; + + @ResponseBody + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public List getAll() { + Spliterator products = repository.findAll().spliterator(); + return StreamSupport.stream(products, false).collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/catalog-spring-boot/src/main/java/com/redhat/cloudnative/catalog/Product.java b/catalog-spring-boot/src/main/java/com/redhat/cloudnative/catalog/Product.java new file mode 100644 index 0000000..e22e008 --- /dev/null +++ b/catalog-spring-boot/src/main/java/com/redhat/cloudnative/catalog/Product.java @@ -0,0 +1,62 @@ +package com.redhat.cloudnative.catalog; + +import java.io.Serializable; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +@Entity +@Table(name = "PRODUCT", uniqueConstraints = @UniqueConstraint(columnNames = "itemId")) +public class Product implements Serializable { + + @Id + private String itemId; + + private String name; + + private String description; + + private double price; + + public Product() { + } + + public String getItemId() { + return itemId; + } + + public void setItemId(String itemId) { + this.itemId = itemId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + @Override + public String toString() { + return "Product [itemId=" + itemId + ", name=" + name + ", price=" + price + "]"; + } +} \ No newline at end of file diff --git a/catalog-spring-boot/src/main/java/com/redhat/cloudnative/catalog/ProductRepository.java b/catalog-spring-boot/src/main/java/com/redhat/cloudnative/catalog/ProductRepository.java new file mode 100755 index 0000000..cd35485 --- /dev/null +++ b/catalog-spring-boot/src/main/java/com/redhat/cloudnative/catalog/ProductRepository.java @@ -0,0 +1,6 @@ +package com.redhat.cloudnative.catalog; + +import org.springframework.data.repository.CrudRepository; + +public interface ProductRepository extends CrudRepository { +} diff --git a/catalog-spring-boot/src/main/resources/application.properties b/catalog-spring-boot/src/main/resources/application.properties new file mode 100755 index 0000000..1d1cb56 --- /dev/null +++ b/catalog-spring-boot/src/main/resources/application.properties @@ -0,0 +1,6 @@ +spring.application.name=catalog + +spring.datasource.url=jdbc:h2:mem:fruits;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.username=sa +spring.datasource.password= +spring.datasource.driver-class-name=org.h2.Driver diff --git a/catalog-spring-boot/src/main/resources/import.sql b/catalog-spring-boot/src/main/resources/import.sql new file mode 100755 index 0000000..b357c50 --- /dev/null +++ b/catalog-spring-boot/src/main/resources/import.sql @@ -0,0 +1,8 @@ +INSERT INTO PRODUCT (item_id, name, description, price) VALUES ('329299', 'Red Fedora', 'Official Red Hat Fedora', 34.99); +INSERT INTO PRODUCT (item_id, name, description, price) VALUES ('329199', 'Forge Laptop Sticker', 'JBoss Community Forge Project Sticker', 8.50); +INSERT INTO PRODUCT (item_id, name, description, price) VALUES ('165613', 'Solid Performance Polo', 'Moisture-wicking, antimicrobial 100% polyester design wicks for life of garment. No-curl, rib-knit collar...', 17.80) +INSERT INTO PRODUCT (item_id, name, description, price) VALUES ('165614', 'Ogio Caliber Polo', 'Moisture-wicking 100% polyester. Rib-knit collar and cuffs; Ogio jacquard tape insitem_ide neck; bar-tacked three-button placket with...', 28.75) +INSERT INTO PRODUCT (item_id, name, description, price) VALUES ('165954', '16 oz. Vortex Tumbler', 'Double-wall insulated, BPA-free, acrylic cup. Push-on litem_id with thumb-slitem_ide closure; for hot and cold beverages. Holds 16 oz. Hand wash only. Imprint. Clear.', 6.00) +INSERT INTO PRODUCT (item_id, name, description, price) VALUES ('444434', 'Pebble Smart Watch', 'Smart glasses and smart watches are perhaps two of the most exciting developments in recent years. ', 24.00) +INSERT INTO PRODUCT (item_id, name, description, price) VALUES ('444435', 'Oculus Rift', 'The world of gaming has also undergone some very unique and compelling tech advances in recent years. Virtual reality...', 106.00) +INSERT INTO PRODUCT (item_id, name, description, price) VALUES ('444436', 'Lytro Camera', 'Consumers who want to up their photography game are looking at newfangled cameras like the Lytro Field camera, designed to ...', 44.30) diff --git a/gateway-vertx/pom.xml b/gateway-vertx/pom.xml new file mode 100644 index 0000000..01437b6 --- /dev/null +++ b/gateway-vertx/pom.xml @@ -0,0 +1,143 @@ + + + 4.0.0 + com.redhat.cloudnative + gateway + 1.0-SNAPSHOT + jar + CoolStore Gateway Service + CoolStore Gateway Service with Eclipse Vert.x + + 3.4.2 + 1.0.8 + com.redhat.cloudnative.gateway.GatewayVerticle + 3.5.30 + 1.7.25 + + + + + io.vertx + vertx-dependencies + ${vertx.version} + pom + import + + + + + + io.vertx + vertx-core + + + io.vertx + vertx-web + + + io.vertx + vertx-circuit-breaker + + + io.vertx + vertx-web-client + + + io.vertx + vertx-rx-java + + + io.vertx + vertx-service-discovery + + + io.vertx + vertx-service-discovery-bridge-kubernetes + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-jdk14 + ${slf4j.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.1 + + 1.8 + 1.8 + + + + io.fabric8 + vertx-maven-plugin + ${vertx-maven-plugin.version} + + + vmp + + initialize + package + + + + + true + -Djava.net.preferIPv4Stack=true + + + + io.fabric8 + fabric8-maven-plugin + ${fabric8.maven.plugin.version} + + + + resource + build + + + + + + + + + microservice + gateway + + + + + + + vertx + + + + redhat-openjdk-18/openjdk18-openshift + + + + + + + /health + + + + + + + + \ No newline at end of file diff --git a/gateway-vertx/src/main/java/com/redhat/cloudnative/gateway/GatewayVerticle.java b/gateway-vertx/src/main/java/com/redhat/cloudnative/gateway/GatewayVerticle.java new file mode 100644 index 0000000..147009f --- /dev/null +++ b/gateway-vertx/src/main/java/com/redhat/cloudnative/gateway/GatewayVerticle.java @@ -0,0 +1,195 @@ +package com.redhat.cloudnative.gateway; + + +import io.vertx.circuitbreaker.CircuitBreakerOptions; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.client.WebClientOptions; +import io.vertx.rxjava.circuitbreaker.CircuitBreaker; +import io.vertx.rxjava.core.AbstractVerticle; +import io.vertx.rxjava.ext.web.Router; +import io.vertx.rxjava.ext.web.RoutingContext; +import io.vertx.rxjava.ext.web.client.WebClient; +import io.vertx.rxjava.ext.web.codec.BodyCodec; +import io.vertx.rxjava.ext.web.handler.CorsHandler; +import io.vertx.rxjava.servicediscovery.ServiceDiscovery; +import io.vertx.rxjava.servicediscovery.types.HttpEndpoint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import rx.Observable; +import rx.Single; + +public class GatewayVerticle extends AbstractVerticle { + private static final Logger LOG = LoggerFactory.getLogger(GatewayVerticle.class); + + private WebClient catalog; + private WebClient inventory; + private WebClient cart; + private CircuitBreaker circuit; + + @Override + public void start() { + + circuit = CircuitBreaker.create("inventory-circuit-breaker", vertx, + new CircuitBreakerOptions() + .setFallbackOnFailure(true) + .setMaxFailures(3) + .setResetTimeout(5000) + .setTimeout(1000) + ); + + Router router = Router.router(vertx); + router.route().handler(CorsHandler.create("*") + .allowedMethod(HttpMethod.GET) + .allowedMethod(HttpMethod.POST) + .allowedMethod(HttpMethod.DELETE) + .allowedHeader("Access-Control-Allow-Method") + .allowedHeader("Access-Control-Allow-Origin") + .allowedHeader("Access-Control-Allow-Credentials") + .allowedHeader("Content-Type")); + + router.get("/health").handler(ctx -> ctx.response().end(new JsonObject().put("status", "UP").toString())); + router.get("/api/products").handler(this::products); + router.get("/api/cart/:cartId").handler(this::getCart); + router.post("/api/cart/:cartId/:itemId/:quantity").handler(this::addToCart); + router.delete("/api/cart/:cartId/:itemId/:quantity").handler(this::deleteFromCart); + + ServiceDiscovery.create(vertx, discovery -> { + // Catalog lookup + Single catalogDiscoveryRequest = HttpEndpoint.rxGetWebClient(discovery, + rec -> rec.getName().equals("catalog")) + .onErrorReturn(t -> WebClient.create(vertx, new WebClientOptions() + .setDefaultHost(System.getProperty("catalog.api.host", "localhost")) + .setDefaultPort(Integer.getInteger("catalog.api.port", 9000)))); + + // Inventory lookup + Single inventoryDiscoveryRequest = HttpEndpoint.rxGetWebClient(discovery, + rec -> rec.getName().equals("inventory")) + .onErrorReturn(t -> WebClient.create(vertx, new WebClientOptions() + .setDefaultHost(System.getProperty("inventory.api.host", "localhost")) + .setDefaultPort(Integer.getInteger("inventory.api.port", 9001)))); + + // Cart lookup + Single cartDiscoveryRequest = HttpEndpoint.rxGetWebClient(discovery, + rec -> rec.getName().equals("cart")) + .onErrorReturn(t -> WebClient.create(vertx, new WebClientOptions() + .setDefaultHost(System.getProperty("cart.api.host", "localhost")) + .setDefaultPort(Integer.getInteger("cart.api.port", 9002)))); + + // Zip all 3 requests + Single.zip(catalogDiscoveryRequest, inventoryDiscoveryRequest, cartDiscoveryRequest, (c, i, ct) -> { + // When everything is done + catalog = c; + inventory = i; + cart = ct; + return vertx.createHttpServer() + .requestHandler(router::accept) + .listen(Integer.getInteger("http.port", 8080)); + }).subscribe(); + }); + } + + private void products(RoutingContext rc) { + // Retrieve catalog + catalog.get("/api/products").as(BodyCodec.jsonArray()).rxSend() + .map(resp -> { + if (resp.statusCode() != 200) { + new RuntimeException("Invalid response from the catalog: " + resp.statusCode()); + } + return resp.body(); + }) + .flatMap(products -> + // For each item from the catalog, invoke the inventory service + Observable.from(products) + .cast(JsonObject.class) + .flatMapSingle(product -> + circuit.rxExecuteCommandWithFallback( + future -> + inventory.get("/api/inventory/" + product.getString("itemId")).as(BodyCodec.jsonObject()) + .rxSend() + .map(resp -> { + if (resp.statusCode() != 200) { + LOG.warn("Inventory error for {}: status code {}", + product.getString("itemId"), resp.statusCode()); + return product.copy(); + } + return product.copy().put("availability", + new JsonObject().put("quantity", resp.body().getInteger("quantity"))); + }) + .subscribe( + future::complete, + future::fail), + error -> { + LOG.error("Inventory error for {}: {}", product.getString("itemId"), error.getMessage()); + return product; + } + )) + .toList().toSingle() + ) + .subscribe( + list -> rc.response().end(Json.encodePrettily(list)), + error -> rc.response().end(new JsonObject().put("error", error.getMessage()).toString()) + ); + } + + private void getCart(RoutingContext rc) { + String cartId = rc.request().getParam("cartId"); + + circuit.executeWithFallback( + future -> { + cart.get("/api/cart/" + cartId).as(BodyCodec.jsonObject()) + .send( ar -> { + if (ar.succeeded()) { + rc.response().end(ar.result().body().toString()); + future.complete(); + } else { + rc.response().end(new JsonObject().toString()); + future.fail(ar.cause()); + } + }); + }, v -> new JsonObject()); + } + + private void addToCart(RoutingContext rc) { + String cartId = rc.request().getParam("cartId"); + String itemId = rc.request().getParam("itemId"); + String quantity = rc.request().getParam("quantity"); + + circuit.executeWithFallback( + future -> { + cart.post("/api/cart/" + cartId + "/" + itemId + "/" + quantity) + .as(BodyCodec.jsonObject()) + .send( ar -> { + if (ar.succeeded()) { + rc.response().end(ar.result().body().toString()); + future.complete(); + } else { + rc.response().end(new JsonObject().toString()); + future.fail(ar.cause()); + } + }); + }, v -> new JsonObject()); + } + + private void deleteFromCart(RoutingContext rc) { + String cartId = rc.request().getParam("cartId"); + String itemId = rc.request().getParam("itemId"); + String quantity = rc.request().getParam("quantity"); + + circuit.executeWithFallback( + future -> { + cart.delete("/api/cart/" + cartId + "/" + itemId + "/" + quantity) + .as(BodyCodec.jsonObject()) + .send( ar -> { + if (ar.succeeded()) { + rc.response().end(ar.result().body().toString()); + future.complete(); + } else { + rc.response().end(new JsonObject().toString()); + future.fail(ar.cause()); + } + }); + }, v -> new JsonObject()); + } +} diff --git a/gateway-vertx/src/main/resources/vertx-default-jul-logging.properties b/gateway-vertx/src/main/resources/vertx-default-jul-logging.properties new file mode 100644 index 0000000..2c5c98d --- /dev/null +++ b/gateway-vertx/src/main/resources/vertx-default-jul-logging.properties @@ -0,0 +1,12 @@ + +handlers=java.util.logging.ConsoleHandler +java.util.logging.SimpleFormatter.format=[%p] %t: %m%n\n +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.ConsoleHandler.level=FINEST + +.level=INFO +io.vertx.ext.web.level=INFO +io.vertx.level=INFO +com.hazelcast.level=INFO +io.netty.util.internal.PlatformDependent.level=SEVERE +io.vertx.servicediscovery.level=SEVERE diff --git a/inventory-wildfly-swarm/pom.xml b/inventory-wildfly-swarm/pom.xml new file mode 100644 index 0000000..82d3930 --- /dev/null +++ b/inventory-wildfly-swarm/pom.xml @@ -0,0 +1,146 @@ + + + 4.0.0 + com.redhat.cloudnative + inventory + 1.0-SNAPSHOT + war + CoolStore Inventory Service + CoolStore Inventory Service with WildFly Swarm + + 2017.8.1 + 2.3.4 + 3.5.30 + 1.4.187 + 9.4.1207 + false + + + + + org.wildfly.swarm + bom-all + ${version.wildfly.swarm} + pom + import + + + io.fabric8 + fabric8-project-bom-with-platform-deps + ${fabric8.version} + pom + import + + + + + + org.wildfly.swarm + jaxrs-jsonp + + + org.wildfly.swarm + cdi + + + org.wildfly.swarm + jpa + + + org.wildfly.swarm + monitor + + + com.h2database + h2 + ${h2.version} + + + org.postgresql + postgresql + ${postgresql.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.1 + + 1.8 + 1.8 + + + + maven-war-plugin + 3.1.0 + + false + + + + org.wildfly.swarm + wildfly-swarm-plugin + ${version.wildfly.swarm} + + + + package + + + + + + true + + -Dswarm.http.port=9001 + + + + io.fabric8 + fabric8-maven-plugin + ${fabric8.maven.plugin.version} + + + + resource + build + + + + + + + + + microservice + inventory + + + + + + + wildfly-swarm + + + + redhat-openjdk-18/openjdk18-openshift + + + + + + + /node + + + + + + + + \ No newline at end of file diff --git a/inventory-wildfly-swarm/src/main/java/com/redhat/cloudnative/inventory/Inventory.java b/inventory-wildfly-swarm/src/main/java/com/redhat/cloudnative/inventory/Inventory.java new file mode 100644 index 0000000..97ce1f8 --- /dev/null +++ b/inventory-wildfly-swarm/src/main/java/com/redhat/cloudnative/inventory/Inventory.java @@ -0,0 +1,45 @@ +package com.redhat.cloudnative.inventory; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import java.io.Serializable; + +@Entity +@Table(name = "INVENTORY", uniqueConstraints = @UniqueConstraint(columnNames = "itemId")) +public class Inventory implements Serializable { + private static final long serialVersionUID = -8053933344541613739L; + + @Id + private String itemId; + + private int quantity; + + public Inventory() { + } + + public String getItemId() { + return itemId; + } + + public void setItemId(String itemId) { + this.itemId = itemId; + } + + public int getQuantity() { + return quantity; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } + + @Override + public String toString() { + return "Inventory [" + + "itemId='" + itemId + '\'' + + ", quantity=" + quantity + + ']'; + } +} diff --git a/inventory-wildfly-swarm/src/main/java/com/redhat/cloudnative/inventory/InventoryApplication.java b/inventory-wildfly-swarm/src/main/java/com/redhat/cloudnative/inventory/InventoryApplication.java new file mode 100644 index 0000000..7bb1db2 --- /dev/null +++ b/inventory-wildfly-swarm/src/main/java/com/redhat/cloudnative/inventory/InventoryApplication.java @@ -0,0 +1,8 @@ +package com.redhat.cloudnative.inventory; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +@ApplicationPath("/") +public class InventoryApplication extends Application { +} diff --git a/inventory-wildfly-swarm/src/main/java/com/redhat/cloudnative/inventory/InventoryResource.java b/inventory-wildfly-swarm/src/main/java/com/redhat/cloudnative/inventory/InventoryResource.java new file mode 100644 index 0000000..5151897 --- /dev/null +++ b/inventory-wildfly-swarm/src/main/java/com/redhat/cloudnative/inventory/InventoryResource.java @@ -0,0 +1,35 @@ +package com.redhat.cloudnative.inventory; + +import javax.enterprise.context.ApplicationScoped; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import org.wildfly.swarm.health.Health; +import org.wildfly.swarm.health.HealthStatus; +import java.util.Date; + +@Path("/") +@ApplicationScoped +public class InventoryResource { + @PersistenceContext(unitName = "InventoryPU") + private EntityManager em; + + @GET + @Path("/api/inventory/{itemId}") + @Produces(MediaType.APPLICATION_JSON) + public Inventory getAvailability(@PathParam("itemId") String itemId) { + Inventory inventory = em.find(Inventory.class, itemId); + + if (inventory == null) { + inventory = new Inventory(); + inventory.setItemId(itemId); + inventory.setQuantity(0); + } + + return inventory; + } +} diff --git a/inventory-wildfly-swarm/src/main/resources/META-INF/load.sql b/inventory-wildfly-swarm/src/main/resources/META-INF/load.sql new file mode 100644 index 0000000..404cdd7 --- /dev/null +++ b/inventory-wildfly-swarm/src/main/resources/META-INF/load.sql @@ -0,0 +1,7 @@ +INSERT INTO INVENTORY(itemId, quantity) VALUES (329299, 35) +INSERT INTO INVENTORY(itemId, quantity) VALUES (329199, 12) +INSERT INTO INVENTORY(itemId, quantity) VALUES (165613, 45) +INSERT INTO INVENTORY(itemId, quantity) VALUES (165614, 87) +INSERT INTO INVENTORY(itemId, quantity) VALUES (165954, 43) +INSERT INTO INVENTORY(itemId, quantity) VALUES (444434, 32) +INSERT INTO INVENTORY(itemId, quantity) VALUES (444435, 53) diff --git a/inventory-wildfly-swarm/src/main/resources/META-INF/persistence.xml b/inventory-wildfly-swarm/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000..ce6abef --- /dev/null +++ b/inventory-wildfly-swarm/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,32 @@ + + + + + + java:/jboss/datasources/InventoryDS + + + + + + + + diff --git a/inventory-wildfly-swarm/src/main/resources/project-stages.yml b/inventory-wildfly-swarm/src/main/resources/project-stages.yml new file mode 100644 index 0000000..f9734a7 --- /dev/null +++ b/inventory-wildfly-swarm/src/main/resources/project-stages.yml @@ -0,0 +1,8 @@ +swarm: + datasources: + data-sources: + InventoryDS: + driver-name: h2 + connection-url: jdbc:h2:mem:fruits;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + user-name: sa + password: sa \ No newline at end of file diff --git a/openshift/cart-pipeline.yaml b/openshift/cart-pipeline.yaml new file mode 100644 index 0000000..2447940 --- /dev/null +++ b/openshift/cart-pipeline.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: List +metadata: {} +items: +- apiVersion: v1 + kind: BuildConfig + metadata: + annotations: + pipeline.alpha.openshift.io/uses: '[{"name": "cart", "namespace": "", "kind": "DeploymentConfig"}]' + name: cart-service-pipeline + spec: + source: + git: + ref: pipeline + uri: https://github.com/siamaksade/cart-service.git + type: Git + strategy: + type: JenkinsPipeline + jenkinsPipelineStrategy: + jenkinsfilePath: Jenkinsfile + triggers: + - generic: + secret: FiArdDBH + type: Generic diff --git a/openshift/cart-template.yaml b/openshift/cart-template.yaml new file mode 100644 index 0000000..4469156 --- /dev/null +++ b/openshift/cart-template.yaml @@ -0,0 +1,170 @@ +apiVersion: v1 +kind: Template +metadata: + annotations: + iconClass: icon-java + name: cart +objects: +- apiVersion: v1 + kind: ImageStream + metadata: + name: cart + labels: + application: cart + spec: + tags: + - name: latest +- apiVersion: v1 + kind: BuildConfig + metadata: + name: cart + labels: + application: cart + spec: + output: + to: + kind: ImageStreamTag + name: cart:latest + source: + git: + ref: ${GIT_REF} + uri: ${GIT_URI} + type: Git + strategy: + sourceStrategy: + env: + - name: MAVEN_MIRROR_URL + value: ${MAVEN_MIRROR_URL} + from: + kind: ImageStreamTag + name: redhat-openjdk18-openshift:1.0 + namespace: ${IMAGE_STREAM_NAMESPACE} + type: Source + triggers: + - type: ConfigChange +- apiVersion: v1 + kind: DeploymentConfig + metadata: + name: cart + labels: + application: cart + spec: + replicas: 1 + selector: + deploymentconfig: cart + strategy: + resources: {} + type: Recreate + template: + metadata: + labels: + application: cart + deploymentconfig: cart + name: cart + spec: + containers: + - env: + - name: CATALOG_ENDPOINT + value: "http://catalog:8080" + image: cart + imagePullPolicy: Always + livenessProbe: + failureThreshold: 5 + httpGet: + path: /health + port: 8080 + scheme: HTTP + initialDelaySeconds: 45 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + name: cart + ports: + - containerPort: 8778 + name: jolokia + protocol: TCP + - containerPort: 8080 + name: http + protocol: TCP + - containerPort: 8443 + name: https + protocol: TCP + readinessProbe: + failureThreshold: 10 + httpGet: + path: /health + port: 8080 + scheme: HTTP + initialDelaySeconds: 45 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + memory: 1Gi + cpu: 1 + requests: + memory: 512Mi + cpu: 200m + terminationMessagePath: /dev/termination-log + dnsPolicy: ClusterFirst + restartPolicy: Always + securityContext: {} + terminationGracePeriodSeconds: 75 + triggers: + - imageChangeParams: + automatic: true + containerNames: + - cart + from: + kind: ImageStreamTag + name: cart:latest + type: ImageChange + - type: ConfigChange +- apiVersion: v1 + kind: Service + metadata: + labels: + app: cart + application: cart + name: cart + spec: + ports: + - port: 8080 + protocol: TCP + targetPort: 8080 + selector: + deploymentconfig: cart +- apiVersion: v1 + kind: Route + metadata: + labels: + application: cart + name: cart + spec: + to: + kind: Service + name: cart + weight: 100 +parameters: +- displayName: Application name + name: APPLICATION_NAME + required: true + value: cart +- description: Git source URI for application + displayName: Git source repository + name: GIT_URI + required: true + value: https://github.com/siamaksade/cart-service.git +- description: Git branch/tag reference + displayName: Git branch/tag reference + name: GIT_REF + value: master +- description: Maven mirror url. If nexus is deployed locally, use nexus url (e.g. http://nexus.ci:8081/content/groups/public/) + displayName: Maven mirror url + name: MAVEN_MIRROR_URL +- displayName: ImageStream Namespace + description: Namespace in which the ImageStreams for Red Hat OpenJDK image is installed. These ImageStreams are normally installed in the openshift namespace. You should only need to modify this if you've installed the ImageStreams in a different namespace/project. + name: IMAGE_STREAM_NAMESPACE + required: true + value: openshift \ No newline at end of file diff --git a/openshift/coolstore-deployment-template.yaml b/openshift/coolstore-deployment-template.yaml new file mode 100644 index 0000000..74f97b9 --- /dev/null +++ b/openshift/coolstore-deployment-template.yaml @@ -0,0 +1,863 @@ +apiVersion: v1 +kind: Template +metadata: + annotations: + description: CoolStore Microservices Application Template + iconClass: icon-java + tags: microservice,jboss,spring + name: coolstore +objects: +- apiVersion: v1 + groupNames: null + kind: RoleBinding + metadata: + name: default_edit + roleRef: + name: view + subjects: + - kind: ServiceAccount + name: default +# UI +- apiVersion: v1 + kind: DeploymentConfig + metadata: + name: web-ui + labels: + application: coolstore + component: web-ui + spec: + replicas: 1 + selector: + deploymentconfig: web-ui + strategy: + resources: {} + type: Recreate + template: + metadata: + labels: + application: coolstore + component: web-ui + deploymentconfig: web-ui + spec: + containers: + - env: + - name: KUBERNETES_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: COOLSTORE_GW_ENDPOINT + value: http://gw-${HOSTNAME_SUFFIX} + - name: HOSTNAME_HTTP + value: web-ui:8080 + image: web-ui + imagePullPolicy: Always + name: web-ui + ports: + - containerPort: 8080 + protocol: TCP + livenessProbe: + failureThreshold: 5 + httpGet: + path: / + port: 8080 + scheme: HTTP + initialDelaySeconds: 120 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 5 + readinessProbe: + failureThreshold: 5 + httpGet: + path: / + port: 8080 + scheme: HTTP + initialDelaySeconds: 15 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: 500m + memory: 1Gi + requests: + cpu: 50m + memory: 128Mi + terminationMessagePath: /dev/termination-log + dnsPolicy: ClusterFirst + restartPolicy: Always + securityContext: {} + terminationGracePeriodSeconds: 30 + triggers: + - imageChangeParams: + automatic: true + containerNames: + - web-ui + from: + kind: ImageStreamTag + namespace: ${IMAGESTREAM_NAMESPACE} + name: coolstore-web-ui:${APP_VERSION} + type: ImageChange + - type: ConfigChange +- apiVersion: v1 + kind: Service + metadata: + labels: + app: web-ui + application: coolstore + component: web-ui + name: web-ui + spec: + ports: + - name: 8080-tcp + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + deploymentconfig: web-ui +- apiVersion: v1 + kind: Route + metadata: + name: web-ui + labels: + application: coolstore + component: web-ui + spec: + host: web-ui-${HOSTNAME_SUFFIX} + to: + kind: Service + name: web-ui +# Coolstore Gateway +- apiVersion: v1 + kind: DeploymentConfig + metadata: + name: coolstore-gw + labels: + application: coolstore + component: coolstore-gw + spec: + replicas: 1 + selector: + deploymentconfig: coolstore-gw + strategy: + resources: {} + type: Recreate + template: + metadata: + labels: + application: coolstore + component: coolstore-gw + deploymentconfig: coolstore-gw + name: coolstore-gw + spec: + containers: + - env: + - name: KUBERNETES_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: CART_ENDPOINT + value: cart-${HOSTNAME_SUFFIX} + - name: INVENTORY_ENDPOINT + value: inventory-${HOSTNAME_SUFFIX} + - name: CATALOG_ENDPOINT + value: catalog-${HOSTNAME_SUFFIX} + image: library/coolstore-gw:${APP_VERSION} + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 15 + name: coolstore-gw + ports: + - containerPort: 8778 + name: jolokia + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 10 + resources: + limits: + cpu: 1 + memory: 2Gi + requests: + cpu: 100m + memory: 512Mi + triggers: + - imageChangeParams: + automatic: true + containerNames: + - coolstore-gw + from: + kind: ImageStreamTag + namespace: ${IMAGESTREAM_NAMESPACE} + name: coolstore-gateway:${APP_VERSION} + type: ImageChange + - type: ConfigChange +- apiVersion: v1 + kind: Service + metadata: + labels: + app: coolstore-gw + application: coolstore + component: coolstore-gw + hystrix.enabled: "true" + name: coolstore-gw + spec: + ports: + - port: 8080 + protocol: TCP + targetPort: 8080 + selector: + deploymentconfig: coolstore-gw +- apiVersion: v1 + kind: Route + metadata: + name: coolstore-gw + labels: + application: coolstore + component: coolstore-gw + spec: + host: gw-${HOSTNAME_SUFFIX} + to: + kind: Service + name: coolstore-gw +# Catalog Service +- apiVersion: v1 + kind: DeploymentConfig + metadata: + name: catalog + labels: + application: coolstore + component: catalog + spec: + replicas: 1 + selector: + deploymentconfig: catalog + strategy: + resources: {} + type: Recreate + template: + metadata: + labels: + application: coolstore + component: catalog + deploymentconfig: catalog + name: catalog + spec: + containers: + - env: + - name: JWS_ADMIN_USERNAME + value: Skq3VtCd + - name: JWS_ADMIN_PASSWORD + value: oktt6yhw + - name: DB_USERNAME + value: ${CATALOG_DB_USERNAME} + - name: DB_PASSWORD + value: ${CATALOG_DB_PASSWORD} + - name: DB_NAME + value: catalogdb + - name: DB_SERVER + value: catalog-mongodb + image: catalog + imagePullPolicy: Always + name: catalog + ports: + - containerPort: 8778 + name: jolokia + protocol: TCP + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + failureThreshold: 5 + httpGet: + path: /health + port: 8080 + scheme: HTTP + initialDelaySeconds: 15 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + livenessProbe: + failureThreshold: 5 + httpGet: + path: /health + port: 8080 + scheme: HTTP + initialDelaySeconds: 15 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: 2 + memory: 2Gi + requests: + cpu: 100m + memory: 256Mi + terminationMessagePath: /dev/termination-log + dnsPolicy: ClusterFirst + restartPolicy: Always + securityContext: {} + terminationGracePeriodSeconds: 75 + triggers: + - imageChangeParams: + automatic: true + containerNames: + - catalog + from: + kind: ImageStreamTag + name: coolstore-catalog:${APP_VERSION} + namespace: ${IMAGESTREAM_NAMESPACE} + type: ImageChange + - type: ConfigChange +- apiVersion: v1 + kind: Service + metadata: + annotations: + service.alpha.openshift.io/dependencies: '[{"name":"catalog-mongodb","namespace":"","kind":"Service"}]' + labels: + app: catalog + application: coolstore + component: catalog + name: catalog + spec: + ports: + - port: 8080 + protocol: TCP + targetPort: 8080 + selector: + deploymentconfig: catalog +- apiVersion: v1 + kind: Route + metadata: + labels: + application: coolstore + component: catalog + name: catalog + spec: + host: catalog-${HOSTNAME_SUFFIX} + to: + kind: Service + name: catalog + weight: 100 +- apiVersion: v1 + kind: Service + metadata: + labels: + app: catalog + application: coolstore + component: catalog + name: catalog-mongodb + spec: + ports: + - name: mongo + port: 27017 + protocol: TCP + targetPort: 27017 + selector: + deploymentconfig: catalog-mongodb + sessionAffinity: None + type: ClusterIP +- apiVersion: v1 + kind: DeploymentConfig + metadata: + labels: + application: coolstore + component: catalog + name: catalog-mongodb + spec: + replicas: 1 + selector: + deploymentconfig: catalog-mongodb + strategy: + recreateParams: + timeoutSeconds: 600 + resources: {} + type: Recreate + template: + metadata: + labels: + application: coolstore + component: catalog + deploymentconfig: catalog-mongodb + spec: + containers: + - env: + - name: KUBERNETES_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: MONGODB_USER + value: ${CATALOG_DB_USERNAME} + - name: MONGODB_PASSWORD + value: ${CATALOG_DB_PASSWORD} + - name: MONGODB_DATABASE + value: catalogdb + - name: MONGODB_ADMIN_PASSWORD + value: ${CATALOG_DB_PASSWORD} + image: mongodb + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + initialDelaySeconds: 30 + periodSeconds: 10 + successThreshold: 1 + tcpSocket: + port: 27017 + timeoutSeconds: 1 + name: catalog-mongodb + ports: + - containerPort: 27017 + protocol: TCP + readinessProbe: + exec: + command: + - /bin/sh + - -i + - -c + - mongo 127.0.0.1:27017/$MONGODB_DATABASE -u $MONGODB_USER -p $MONGODB_PASSWORD + --eval="quit()" + failureThreshold: 3 + initialDelaySeconds: 3 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: 500m + memory: 1Gi + requests: + cpu: 100m + memory: 256Mi + securityContext: + capabilities: {} + privileged: false + terminationMessagePath: /dev/termination-log + volumeMounts: + - mountPath: /var/lib/mongodb/data + name: mongodb-data + dnsPolicy: ClusterFirst + restartPolicy: Always + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - name: mongodb-data + persistentVolumeClaim: + claimName: mongodb-data-pv + test: false + triggers: + - imageChangeParams: + automatic: true + containerNames: + - catalog-mongodb + from: + kind: ImageStreamTag + name: mongodb:3.2 + namespace: openshift + type: ImageChange + - type: ConfigChange +- apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + labels: + application: coolstore + component: catalog + name: mongodb-data-pv + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +# Cart Service +- apiVersion: v1 + kind: DeploymentConfig + metadata: + name: cart + labels: + application: coolstore + component: cart + spec: + replicas: 1 + selector: + deploymentconfig: cart + strategy: + resources: {} + type: Recreate + template: + metadata: + labels: + application: coolstore + component: cart + deploymentconfig: cart + name: cart + spec: + containers: + - env: + - name: CATALOG_ENDPOINT + value: "http://catalog:8080" + - name: APPLICATION_MODE + value: prod + image: cart + imagePullPolicy: Always + livenessProbe: + failureThreshold: 5 + httpGet: + path: /health + port: 8080 + scheme: HTTP + initialDelaySeconds: 45 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + name: cart + ports: + - containerPort: 8778 + name: jolokia + protocol: TCP + - containerPort: 8080 + name: http + protocol: TCP + - containerPort: 8443 + name: https + protocol: TCP + readinessProbe: + failureThreshold: 10 + httpGet: + path: /health + port: 8080 + scheme: HTTP + initialDelaySeconds: 45 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + memory: 1Gi + requests: + memory: 200Mi + terminationMessagePath: /dev/termination-log + dnsPolicy: ClusterFirst + restartPolicy: Always + securityContext: {} + terminationGracePeriodSeconds: 75 + triggers: + - imageChangeParams: + automatic: true + containerNames: + - cart + from: + kind: ImageStreamTag + name: cart:${APP_VERSION} + type: ImageChange + - type: ConfigChange +- apiVersion: v1 + kind: Service + metadata: + labels: + app: cart + application: coolstore + component: cart + name: cart + spec: + ports: + - port: 8080 + protocol: TCP + targetPort: 8080 + selector: + deploymentconfig: cart +- apiVersion: v1 + kind: Route + metadata: + labels: + application: coolstore + component: cart + name: cart + spec: + host: cart-${HOSTNAME_SUFFIX} + to: + kind: Service + name: cart + weight: 100 +# Inventory Service +- apiVersion: v1 + kind: DeploymentConfig + metadata: + name: inventory + labels: + application: coolstore + component: inventory + spec: + replicas: 1 + selector: + deploymentconfig: inventory + strategy: + resources: {} + type: Recreate + template: + metadata: + labels: + deploymentconfig: inventory + application: coolstore + component: inventory + name: inventory + spec: + containers: + - env: + - name: OPENSHIFT_KUBE_PING_LABELS + value: application=inventory + - name: OPENSHIFT_KUBE_PING_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: MQ_CLUSTER_PASSWORD + value: 7mzX0pLV03 + - name: JGROUPS_CLUSTER_PASSWORD + value: CqUo3fYDTv + - name: AUTO_DEPLOY_EXPLODED + value: "false" + - name: DB_SERVICE_PREFIX_MAPPING + value: inventory-postgresql=DB + - name: DB_JNDI + value: java:jboss/datasources/InventoryDS + - name: DB_USERNAME + value: ${INVENTORY_DB_USERNAME} + - name: DB_PASSWORD + value: ${INVENTORY_DB_PASSWORD} + - name: DB_DATABASE + value: inventorydb + image: inventory + imagePullPolicy: Always + lifecycle: + preStop: + exec: + command: + - /opt/eap/bin/jboss-cli.sh + - -c + - :shutdown(timeout=60) + livenessProbe: + failureThreshold: 5 + httpGet: + path: /node + port: 8080 + scheme: HTTP + initialDelaySeconds: 120 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 5 + name: inventory + ports: + - containerPort: 8778 + name: jolokia + protocol: TCP + - containerPort: 8080 + name: http + protocol: TCP + - containerPort: 8888 + name: ping + protocol: TCP + readinessProbe: + failureThreshold: 5 + httpGet: + path: /node + port: 8080 + scheme: HTTP + initialDelaySeconds: 15 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: 500m + memory: 1Gi + requests: + cpu: 100m + memory: 512Mi + terminationMessagePath: /dev/termination-log + dnsPolicy: ClusterFirst + restartPolicy: Always + securityContext: {} + terminationGracePeriodSeconds: 75 + triggers: + - imageChangeParams: + automatic: true + containerNames: + - inventory + from: + kind: ImageStreamTag + name: coolstore-inventory:${APP_VERSION} + namespace: ${IMAGESTREAM_NAMESPACE} + type: ImageChange + - type: ConfigChange +- apiVersion: v1 + kind: Service + metadata: + annotations: + service.alpha.openshift.io/dependencies: '[{"name":"inventory-postgresql","namespace":"","kind":"Service"}]' + labels: + app: inventory + application: coolstore + component: inventory + name: inventory + spec: + ports: + - port: 8080 + protocol: TCP + targetPort: 8080 + selector: + deploymentconfig: inventory +- apiVersion: v1 + kind: Route + metadata: + labels: + application: coolstore + component: inventory + name: inventory + spec: + host: inventory-${HOSTNAME_SUFFIX} + to: + kind: Service + name: inventory + weight: 100 +- apiVersion: v1 + kind: DeploymentConfig + metadata: + name: inventory-postgresql + labels: + component: inventory + application: coolstore + spec: + replicas: 1 + selector: + deploymentconfig: inventory-postgresql + strategy: + type: Recreate + template: + metadata: + labels: + application: coolstore + component: inventory + deploymentconfig: inventory-postgresql + name: inventory-postgresql + spec: + containers: + - env: + - name: POSTGRESQL_USER + value: ${INVENTORY_DB_USERNAME} + - name: POSTGRESQL_PASSWORD + value: ${INVENTORY_DB_PASSWORD} + - name: POSTGRESQL_DATABASE + value: inventorydb + image: postgresql + imagePullPolicy: Always + name: inventory-postgresql + ports: + - containerPort: 5432 + protocol: TCP + volumeMounts: + - mountPath: /var/lib/pgsql/data + name: inventory-postgresql-data + livenessProbe: + initialDelaySeconds: 30 + tcpSocket: + port: 5432 + timeoutSeconds: 1 + readinessProbe: + exec: + command: + - /bin/sh + - -i + - -c + - psql -h 127.0.0.1 -U $POSTGRESQL_USER -q -d $POSTGRESQL_DATABASE -c 'SELECT 1' + initialDelaySeconds: 5 + timeoutSeconds: 1 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 50m + memory: 128Mi + terminationGracePeriodSeconds: 60 + volumes: + - name: inventory-postgresql-data + persistentVolumeClaim: + claimName: inventory-postgresql-pv + triggers: + - imageChangeParams: + automatic: true + containerNames: + - inventory-postgresql + from: + kind: ImageStreamTag + name: postgresql:latest + namespace: openshift + type: ImageChange + - type: ConfigChange +- apiVersion: v1 + kind: Service + metadata: + labels: + application: coolstore + component: inventory + name: inventory-postgresql + spec: + ports: + - port: 5432 + targetPort: 5432 + selector: + deploymentconfig: inventory-postgresql +- apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + labels: + application: coolstore + component: inventory + name: inventory-postgresql-pv + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +parameters: +- description: Namespace in which the ImageStreams for CoolStore services are installed + displayName: ImageStream Namespace + name: IMAGESTREAM_NAMESPACE + required: true + value: openshift +- description: CoolStore application image version to be deployed (imagestreams should exist) + displayName: CoolStore Version + name: APP_VERSION + required: true + value: prod +- description: Hostname suffix used for routes e.g. cart- inventory- + displayName: Hostname Suffix + name: HOSTNAME_SUFFIX + required: true +- description: Catalog Service database user name + displayName: Catalog Database Username + from: user[a-zA-Z0-9]{3} + generate: expression + name: CATALOG_DB_USERNAME + required: true +- description: Catalog Service database user password + displayName: Catalog Database Password + from: '[a-zA-Z0-9]{8}' + generate: expression + name: CATALOG_DB_PASSWORD + required: true +- description: Inventory Service database user name + displayName: Inventory Database Username + from: user[a-zA-Z0-9]{3} + generate: expression + name: INVENTORY_DB_USERNAME + required: true +- description: Inventory Service database user password + displayName: Inventory Database Password + from: '[a-zA-Z0-9]{8}' + generate: expression + name: INVENTORY_DB_PASSWORD + required: true diff --git a/openshift/coolstore-template.yaml b/openshift/coolstore-template.yaml new file mode 100644 index 0000000..8636a13 --- /dev/null +++ b/openshift/coolstore-template.yaml @@ -0,0 +1,1039 @@ +apiVersion: v1 +kind: Template +metadata: + annotations: + description: CoolStore Microservices Application Template + iconClass: icon-java + tags: microservice,jboss,spring + name: coolstore +objects: +- apiVersion: v1 + groupNames: null + kind: RoleBinding + metadata: + name: default_edit + roleRef: + name: view + subjects: + - kind: ServiceAccount + name: default +# UI +- apiVersion: v1 + kind: ImageStream + metadata: + name: web-ui + labels: + application: coolstore + component: web-ui + spec: + tags: + - name: latest +- apiVersion: v1 + kind: BuildConfig + metadata: + name: web-ui + labels: + app: web-ui + spec: + output: + to: + kind: ImageStreamTag + name: web-ui:latest + source: + contextDir: web-nodejs + git: + ref: ${GIT_REF} + uri: ${GIT_URI} + type: Git + strategy: + sourceStrategy: + from: + kind: ImageStreamTag + name: nodejs:4 + namespace: openshift + type: Source + triggers: + - imageChange: {} + type: ImageChange + - type: ConfigChange +- apiVersion: v1 + kind: DeploymentConfig + metadata: + name: web-ui + labels: + application: coolstore + component: web-ui + spec: + replicas: 1 + selector: + deploymentconfig: web-ui + strategy: + resources: {} + type: Recreate + template: + metadata: + labels: + application: coolstore + component: web-ui + deploymentconfig: web-ui + spec: + containers: + - env: + - name: KUBERNETES_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: COOLSTORE_GW_ENDPOINT + value: http://gw-${HOSTNAME_SUFFIX} + - name: HOSTNAME_HTTP + value: web-ui:8080 + image: web-ui + imagePullPolicy: Always + name: web-ui + ports: + - containerPort: 8080 + protocol: TCP + livenessProbe: + failureThreshold: 5 + httpGet: + path: / + port: 8080 + scheme: HTTP + initialDelaySeconds: 120 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 5 + readinessProbe: + failureThreshold: 5 + httpGet: + path: / + port: 8080 + scheme: HTTP + initialDelaySeconds: 15 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: 500m + memory: 1Gi + requests: + cpu: 50m + memory: 128Mi + terminationMessagePath: /dev/termination-log + dnsPolicy: ClusterFirst + restartPolicy: Always + securityContext: {} + terminationGracePeriodSeconds: 30 + triggers: + - imageChangeParams: + automatic: true + containerNames: + - web-ui + from: + kind: ImageStreamTag + name: web-ui:latest + type: ImageChange + - type: ConfigChange +- apiVersion: v1 + kind: Service + metadata: + labels: + app: web-ui + application: coolstore + component: web-ui + name: web-ui + spec: + ports: + - name: 8080-tcp + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + deploymentconfig: web-ui +- apiVersion: v1 + kind: Route + metadata: + name: web-ui + labels: + application: coolstore + component: web-ui + spec: + host: web-ui-${HOSTNAME_SUFFIX} + to: + kind: Service + name: web-ui +# Coolstore Gateway +- apiVersion: v1 + kind: ImageStream + metadata: + name: coolstore-gw + labels: + application: coolstore + component: coolstore-gw + spec: + tags: + - name: latest +- apiVersion: v1 + kind: BuildConfig + metadata: + name: coolstore-gw + labels: + application: coolstore + component: coolstore-gw + spec: + output: + to: + kind: ImageStreamTag + name: coolstore-gw:latest + source: + contextDir: gateway-vertx + git: + ref: ${GIT_REF} + uri: ${GIT_URI} + type: Git + strategy: + sourceStrategy: + env: + - name: MAVEN_MIRROR_URL + value: ${MAVEN_MIRROR_URL} + from: + kind: ImageStreamTag + name: redhat-openjdk18-openshift:1.1 + namespace: openshift + type: Source + triggers: + - type: ConfigChange + - imageChange: {} + type: ImageChange +- apiVersion: v1 + kind: DeploymentConfig + metadata: + name: coolstore-gw + labels: + application: coolstore + component: coolstore-gw + spec: + replicas: 1 + selector: + deploymentconfig: coolstore-gw + strategy: + resources: {} + type: Recreate + template: + metadata: + labels: + application: coolstore + component: coolstore-gw + deploymentconfig: coolstore-gw + name: coolstore-gw + spec: + containers: + - env: + - name: KUBERNETES_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: CART_ENDPOINT + value: cart-${HOSTNAME_SUFFIX} + - name: INVENTORY_ENDPOINT + value: inventory-${HOSTNAME_SUFFIX} + - name: CATALOG_ENDPOINT + value: catalog-${HOSTNAME_SUFFIX} + image: library/coolstore-gw:latest + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 15 + name: coolstore-gw + ports: + - containerPort: 8778 + name: jolokia + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 10 + resources: + limits: + cpu: 1 + memory: 2Gi + requests: + cpu: 100m + memory: 512Mi + triggers: + - imageChangeParams: + automatic: true + containerNames: + - coolstore-gw + from: + kind: ImageStreamTag + name: coolstore-gw:latest + type: ImageChange + - type: ConfigChange +- apiVersion: v1 + kind: Service + metadata: + labels: + app: coolstore-gw + application: coolstore + component: coolstore-gw + hystrix.enabled: "true" + name: coolstore-gw + spec: + ports: + - port: 8080 + protocol: TCP + targetPort: 8080 + selector: + deploymentconfig: coolstore-gw +- apiVersion: v1 + kind: Route + metadata: + name: coolstore-gw + labels: + application: coolstore + component: coolstore-gw + spec: + host: gw-${HOSTNAME_SUFFIX} + to: + kind: Service + name: coolstore-gw +# Catalog Service +- apiVersion: v1 + kind: ImageStream + metadata: + name: catalog + labels: + application: coolstore + component: catalog + spec: + tags: + - name: latest +- apiVersion: v1 + kind: BuildConfig + metadata: + name: catalog + labels: + application: coolstore + component: catalog + spec: + output: + to: + kind: ImageStreamTag + name: catalog:latest + source: + contextDir: catalog-spring-boot + git: + ref: ${GIT_REF} + uri: ${GIT_URI} + type: Git + strategy: + sourceStrategy: + env: + - name: MAVEN_MIRROR_URL + value: ${MAVEN_MIRROR_URL} + from: + kind: ImageStreamTag + name: redhat-openjdk18-openshift:1.1 + namespace: openshift + type: Source + triggers: + - type: ConfigChange + - imageChange: {} + type: ImageChange +- apiVersion: v1 + kind: DeploymentConfig + metadata: + name: catalog + labels: + application: coolstore + component: catalog + spec: + replicas: 1 + selector: + deploymentconfig: catalog + strategy: + resources: {} + type: Recreate + template: + metadata: + labels: + application: coolstore + component: catalog + deploymentconfig: catalog + name: catalog + spec: + containers: + - env: + - name: JWS_ADMIN_USERNAME + value: Skq3VtCd + - name: JWS_ADMIN_PASSWORD + value: oktt6yhw + - name: DB_USERNAME + value: ${CATALOG_DB_USERNAME} + - name: DB_PASSWORD + value: ${CATALOG_DB_PASSWORD} + - name: DB_NAME + value: catalogdb + - name: DB_SERVER + value: catalog-mongodb + image: catalog + imagePullPolicy: Always + name: catalog + ports: + - containerPort: 8778 + name: jolokia + protocol: TCP + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + failureThreshold: 5 + httpGet: + path: /health + port: 8080 + scheme: HTTP + initialDelaySeconds: 15 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + livenessProbe: + failureThreshold: 5 + httpGet: + path: /health + port: 8080 + scheme: HTTP + initialDelaySeconds: 15 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: 2 + memory: 2Gi + requests: + cpu: 100m + memory: 256Mi + terminationMessagePath: /dev/termination-log + dnsPolicy: ClusterFirst + restartPolicy: Always + securityContext: {} + terminationGracePeriodSeconds: 75 + triggers: + - imageChangeParams: + automatic: true + containerNames: + - catalog + from: + kind: ImageStreamTag + name: catalog:latest + type: ImageChange + - type: ConfigChange +- apiVersion: v1 + kind: Service + metadata: + annotations: + service.alpha.openshift.io/dependencies: '[{"name":"catalog-mongodb","namespace":"","kind":"Service"}]' + labels: + app: catalog + application: coolstore + component: catalog + name: catalog + spec: + ports: + - port: 8080 + protocol: TCP + targetPort: 8080 + selector: + deploymentconfig: catalog +- apiVersion: v1 + kind: Route + metadata: + labels: + application: coolstore + component: catalog + name: catalog + spec: + host: catalog-${HOSTNAME_SUFFIX} + to: + kind: Service + name: catalog + weight: 100 +- apiVersion: v1 + kind: Service + metadata: + labels: + app: catalog + application: coolstore + component: catalog + name: catalog-mongodb + spec: + ports: + - name: mongo + port: 27017 + protocol: TCP + targetPort: 27017 + selector: + deploymentconfig: catalog-mongodb + sessionAffinity: None + type: ClusterIP +- apiVersion: v1 + kind: DeploymentConfig + metadata: + labels: + application: coolstore + component: catalog + name: catalog-mongodb + spec: + replicas: 1 + selector: + deploymentconfig: catalog-mongodb + strategy: + recreateParams: + timeoutSeconds: 600 + resources: {} + type: Recreate + template: + metadata: + labels: + application: coolstore + component: catalog + deploymentconfig: catalog-mongodb + spec: + containers: + - env: + - name: KUBERNETES_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: MONGODB_USER + value: ${CATALOG_DB_USERNAME} + - name: MONGODB_PASSWORD + value: ${CATALOG_DB_PASSWORD} + - name: MONGODB_DATABASE + value: catalogdb + - name: MONGODB_ADMIN_PASSWORD + value: ${CATALOG_DB_PASSWORD} + image: mongodb + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + initialDelaySeconds: 30 + periodSeconds: 10 + successThreshold: 1 + tcpSocket: + port: 27017 + timeoutSeconds: 1 + name: catalog-mongodb + ports: + - containerPort: 27017 + protocol: TCP + readinessProbe: + exec: + command: + - /bin/sh + - -i + - -c + - mongo 127.0.0.1:27017/$MONGODB_DATABASE -u $MONGODB_USER -p $MONGODB_PASSWORD + --eval="quit()" + failureThreshold: 3 + initialDelaySeconds: 3 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: 500m + memory: 1Gi + requests: + cpu: 100m + memory: 256Mi + securityContext: + capabilities: {} + privileged: false + terminationMessagePath: /dev/termination-log + volumeMounts: + - mountPath: /var/lib/mongodb/data + name: mongodb-data + dnsPolicy: ClusterFirst + restartPolicy: Always + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - name: mongodb-data + emptyDir: {} + test: false + triggers: + - imageChangeParams: + automatic: true + containerNames: + - catalog-mongodb + from: + kind: ImageStreamTag + name: mongodb:3.2 + namespace: openshift + type: ImageChange + - type: ConfigChange +# Cart Service +- apiVersion: v1 + kind: ImageStream + metadata: + name: cart + labels: + application: coolstore + component: cart + spec: + tags: + - name: latest +- apiVersion: v1 + kind: BuildConfig + metadata: + name: cart + labels: + application: coolstore + component: cart + spec: + output: + to: + kind: ImageStreamTag + name: cart:latest + source: + contextDir: cart-spring-boot + git: + ref: ${GIT_REF} + uri: ${GIT_URI} + type: Git + strategy: + sourceStrategy: + env: + - name: MAVEN_MIRROR_URL + value: ${MAVEN_MIRROR_URL} + from: + kind: ImageStreamTag + name: redhat-openjdk18-openshift:1.1 + namespace: openshift + type: Source + triggers: + - type: ConfigChange + - imageChange: {} + type: ImageChange +- apiVersion: v1 + kind: DeploymentConfig + metadata: + name: cart + labels: + application: coolstore + component: cart + spec: + replicas: 1 + selector: + deploymentconfig: cart + strategy: + resources: {} + type: Recreate + template: + metadata: + labels: + application: coolstore + component: cart + deploymentconfig: cart + name: cart + spec: + containers: + - env: + - name: CATALOG_ENDPOINT + value: "http://catalog:8080" + - name: APPLICATION_MODE + value: prod + image: cart + imagePullPolicy: Always + livenessProbe: + failureThreshold: 5 + httpGet: + path: /health + port: 8080 + scheme: HTTP + initialDelaySeconds: 45 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + name: cart + ports: + - containerPort: 8778 + name: jolokia + protocol: TCP + - containerPort: 8080 + name: http + protocol: TCP + - containerPort: 8443 + name: https + protocol: TCP + readinessProbe: + failureThreshold: 10 + httpGet: + path: /health + port: 8080 + scheme: HTTP + initialDelaySeconds: 45 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + memory: 1Gi + requests: + memory: 200Mi + terminationMessagePath: /dev/termination-log + dnsPolicy: ClusterFirst + restartPolicy: Always + securityContext: {} + terminationGracePeriodSeconds: 75 + triggers: + - imageChangeParams: + automatic: true + containerNames: + - cart + from: + kind: ImageStreamTag + name: cart:latest + type: ImageChange + - type: ConfigChange +- apiVersion: v1 + kind: Service + metadata: + labels: + app: cart + application: coolstore + component: cart + name: cart + spec: + ports: + - port: 8080 + protocol: TCP + targetPort: 8080 + selector: + deploymentconfig: cart +- apiVersion: v1 + kind: Route + metadata: + labels: + application: coolstore + component: cart + name: cart + spec: + host: cart-${HOSTNAME_SUFFIX} + to: + kind: Service + name: cart + weight: 100 +# Inventory Service +- apiVersion: v1 + kind: ImageStream + metadata: + name: inventory + labels: + application: coolstore + component: inventory + spec: + tags: + - name: latest +- apiVersion: v1 + kind: BuildConfig + metadata: + name: inventory + labels: + application: coolstore + component: inventory + spec: + output: + to: + kind: ImageStreamTag + name: inventory:latest + source: + contextDir: inventory-wildfly-swarm + git: + ref: ${GIT_REF} + uri: ${GIT_URI} + type: Git + strategy: + sourceStrategy: + env: + - name: MAVEN_MIRROR_URL + value: ${MAVEN_MIRROR_URL} + from: + kind: ImageStreamTag + name: redhat-openjdk18-openshift:1.1 + namespace: openshift + type: Source + triggers: + - type: ConfigChange + - imageChange: {} + type: ImageChange +- apiVersion: v1 + kind: DeploymentConfig + metadata: + name: inventory + labels: + application: coolstore + component: inventory + spec: + replicas: 1 + selector: + deploymentconfig: inventory + strategy: + resources: {} + type: Recreate + template: + metadata: + labels: + deploymentconfig: inventory + application: coolstore + component: inventory + name: inventory + spec: + containers: + - env: + - name: OPENSHIFT_KUBE_PING_LABELS + value: application=inventory + - name: OPENSHIFT_KUBE_PING_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: MQ_CLUSTER_PASSWORD + value: 7mzX0pLV03 + - name: JGROUPS_CLUSTER_PASSWORD + value: CqUo3fYDTv + - name: AUTO_DEPLOY_EXPLODED + value: "false" + - name: DB_SERVICE_PREFIX_MAPPING + value: inventory-postgresql=DB + - name: DB_JNDI + value: java:jboss/datasources/InventoryDS + - name: DB_USERNAME + value: ${INVENTORY_DB_USERNAME} + - name: DB_PASSWORD + value: ${INVENTORY_DB_PASSWORD} + - name: DB_DATABASE + value: inventorydb + image: inventory + imagePullPolicy: Always + lifecycle: + preStop: + exec: + command: + - /opt/eap/bin/jboss-cli.sh + - -c + - :shutdown(timeout=60) + livenessProbe: + failureThreshold: 5 + httpGet: + path: /node + port: 8080 + scheme: HTTP + initialDelaySeconds: 120 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 5 + name: inventory + ports: + - containerPort: 8778 + name: jolokia + protocol: TCP + - containerPort: 8080 + name: http + protocol: TCP + - containerPort: 8888 + name: ping + protocol: TCP + readinessProbe: + failureThreshold: 5 + httpGet: + path: /node + port: 8080 + scheme: HTTP + initialDelaySeconds: 15 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: 500m + memory: 1Gi + requests: + cpu: 100m + memory: 512Mi + terminationMessagePath: /dev/termination-log + dnsPolicy: ClusterFirst + restartPolicy: Always + securityContext: {} + terminationGracePeriodSeconds: 75 + triggers: + - imageChangeParams: + automatic: true + containerNames: + - inventory + from: + kind: ImageStreamTag + name: inventory:latest + type: ImageChange + - type: ConfigChange +- apiVersion: v1 + kind: Service + metadata: + annotations: + service.alpha.openshift.io/dependencies: '[{"name":"inventory-postgresql","namespace":"","kind":"Service"}]' + labels: + app: inventory + application: coolstore + component: inventory + name: inventory + spec: + ports: + - port: 8080 + protocol: TCP + targetPort: 8080 + selector: + deploymentconfig: inventory +- apiVersion: v1 + kind: Route + metadata: + labels: + application: coolstore + component: inventory + name: inventory + spec: + host: inventory-${HOSTNAME_SUFFIX} + to: + kind: Service + name: inventory + weight: 100 +- apiVersion: v1 + kind: DeploymentConfig + metadata: + name: inventory-postgresql + labels: + component: inventory + application: coolstore + spec: + replicas: 1 + selector: + deploymentconfig: inventory-postgresql + strategy: + type: Recreate + template: + metadata: + labels: + application: coolstore + component: inventory + deploymentconfig: inventory-postgresql + name: inventory-postgresql + spec: + containers: + - env: + - name: POSTGRESQL_USER + value: ${INVENTORY_DB_USERNAME} + - name: POSTGRESQL_PASSWORD + value: ${INVENTORY_DB_PASSWORD} + - name: POSTGRESQL_DATABASE + value: inventorydb + image: postgresql + imagePullPolicy: Always + name: inventory-postgresql + ports: + - containerPort: 5432 + protocol: TCP + volumeMounts: + - mountPath: /var/lib/pgsql/data + name: inventory-postgresql-data + livenessProbe: + initialDelaySeconds: 30 + tcpSocket: + port: 5432 + timeoutSeconds: 1 + readinessProbe: + exec: + command: + - /bin/sh + - -i + - -c + - psql -h 127.0.0.1 -U $POSTGRESQL_USER -q -d $POSTGRESQL_DATABASE -c 'SELECT 1' + initialDelaySeconds: 5 + timeoutSeconds: 1 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 50m + memory: 128Mi + terminationGracePeriodSeconds: 60 + volumes: + - name: inventory-postgresql-data + emptyDir: {} + triggers: + - imageChangeParams: + automatic: true + containerNames: + - inventory-postgresql + from: + kind: ImageStreamTag + name: postgresql:latest + namespace: openshift + type: ImageChange + - type: ConfigChange +- apiVersion: v1 + kind: Service + metadata: + labels: + application: coolstore + component: inventory + name: inventory-postgresql + spec: + ports: + - port: 5432 + targetPort: 5432 + selector: + deploymentconfig: inventory-postgresql +parameters: +- description: Git source URI for application + displayName: Git source repository + name: GIT_URI + required: true + value: https://github.com/siamaksade/devops-labs-coolstore.git +- displayName: Git branch/tag reference + name: GIT_REF + required: true + value: master +- description: Maven mirror url. If nexus is deployed locally, use nexus url (e.g. http://nexus.ci:8081/content/groups/public/) + displayName: Maven mirror url + name: MAVEN_MIRROR_URL +- description: Hostname suffix used for routes e.g. cart- inventory- + displayName: Hostname Suffix + name: HOSTNAME_SUFFIX + required: true +- description: Catalog Service database user name + displayName: Catalog Database Username + from: user[a-zA-Z0-9]{3} + generate: expression + name: CATALOG_DB_USERNAME + required: true +- description: Catalog Service database user password + displayName: Catalog Database Password + from: '[a-zA-Z0-9]{8}' + generate: expression + name: CATALOG_DB_PASSWORD + required: true +- description: Inventory Service database user name + displayName: Inventory Database Username + from: user[a-zA-Z0-9]{3} + generate: expression + name: INVENTORY_DB_USERNAME + required: true +- description: Inventory Service database user password + displayName: Inventory Database Password + from: '[a-zA-Z0-9]{8}' + generate: expression + name: INVENTORY_DB_PASSWORD + required: true diff --git a/web-nodejs/app/app.js b/web-nodejs/app/app.js new file mode 100644 index 0000000..04d9a96 --- /dev/null +++ b/web-nodejs/app/app.js @@ -0,0 +1,109 @@ +'use strict'; + +var module = angular.module('app', ['ngRoute', 'patternfly']), auth = { + loggedIn: false, + ssoEnabled: false, + logout: function () { + } +}; + +module.factory('Auth', function () { + return auth; +}); + +angular.element(document).ready(function () { + + // get config + var initInjector = angular.injector(["ng"]); + var $http = initInjector.get("$http"); + + $http.get("coolstore.json").then(function (response) { + module.constant("COOLSTORE_CONFIG", response.data); + + if (!response.data.SSO_ENABLED) { + angular.bootstrap(document, ["app"], { + strictDi: true + }); + } else { + auth.ssoEnabled = true; + var keycloakAuth = new Keycloak('keycloak.json'); + auth.loggedIn = false; + + auth.login = function () { + keycloakAuth.login({ + loginHint: 'appuser' + }); + }; + + keycloakAuth.init({ + onLoad: 'check-sso' + }).success(function () { + if (keycloakAuth.authenticated) { + keycloakAuth.loadUserInfo().success(function (userInfo) { + auth.userInfo = userInfo; + angular.bootstrap(document, ["app"], { + strictDi: true + }); + auth.loggedIn = true; + auth.authz = keycloakAuth; + auth.logout = function () { + auth.loggedIn = false; + auth.authz = null; + auth.userInfo = {}; + keycloakAuth.logout(); + }; + }).error(function () { + angular.bootstrap(document, ["app"], { + strictDi: true + }); + + }); + } else { + angular.bootstrap(document, ["app"], { + strictDi: true + }); + } + }).error(function (msg) { + angular.bootstrap(document, ["app"], { + strictDi: true + }); + }); + } + }); +}); + + +// setup interceptors +module.config(['$httpProvider', function ($httpProvider) { + + $httpProvider.interceptors.push(['$q', 'Auth', function ($q, Auth) { + return { + 'request': function (config) { + var deferred = $q.defer(); + if (Auth.authz && Auth.authz.token) { + Auth.authz.updateToken(5).success(function () { + config.headers = config.headers || {}; + config.headers.Authorization = 'Bearer ' + Auth.authz.token; + config.withCredentials = true; + deferred.resolve(config); + }).error(function () { + deferred.reject('Failed to refresh token'); + }); + } else { + config.withCredentials = false; + deferred.resolve(config); + } + return deferred.promise; + + }, + 'responseError': function (response) { + if (response.status == 401) { + auth.logout(); + } + return $q.reject(response); + + } + } + }]); +}]); + diff --git a/web-nodejs/app/controllers/controllers.js b/web-nodejs/app/controllers/controllers.js new file mode 100644 index 0000000..208b46a --- /dev/null +++ b/web-nodejs/app/controllers/controllers.js @@ -0,0 +1,168 @@ +'use strict'; + +angular.module('app') + + .controller("HomeController", + ['$scope', '$http', '$filter', 'Notifications', 'cart', 'catalog', 'Auth', + function ($scope, $http, $filter, Notifications, cart, catalog, $auth) { + + $scope.products = []; + $scope.addToCart = function (item) { + cart.addToCart(item.product, parseInt(item.quantity)).then(function (data) { + Notifications.success("Added! Your total is " + $filter('currency')(data.cartTotal)); + }, function (err) { + Notifications.error("Error adding to cart: " + err.statusText); + }); + }; + + $scope.isLoggedIn = function () { + return $auth.loggedIn; + }; + $scope.ssoEnabled = function () { + return $auth.ssoEnabled; + }; + + $scope.login = function () { + $auth.login(); + }; + + + // initialize products + catalog.getProducts().then(function (data) { + if (data.error != undefined && data.error != "") { + Notifications.error("Error retrieving products: " + data.error); + return; + } + $scope.products = data.map(function (el) { + return { + quantity: "1", + product: el + } + }) + }, function (err) { + Notifications.error("Error retrieving products: " + err.statusText); + }); + + + }]) + + .controller("CartController", + ['$scope', '$http', 'Notifications', 'cart', 'Auth', + function ($scope, $http, Notifications, cart, $auth) { + + function reset() { + $scope.cart = cart.getCart(); + $scope.items = $scope.cart.shoppingCartItemList; + + $scope.subtotal = 0; + $scope.cart.shoppingCartItemList.forEach(function (item) { + $scope.subtotal += (item.quantity * item.product.price); + }); + } + + $scope.config = { + selectItems: false, + multiSelect: false, + dblClick: false, + showSelectBox: false + }; + + function performAction(action, item) { + cart.removeFromCart(item.product, item.quantity).then(function (newCart) { + reset(); + }, function (err) { + Notifications.error("Error removing from cart: " + err.statusText); + }); + }; + + $scope.actionButtons = [ + { + name: 'Remove', + title: 'Remove', + actionFn: performAction + } + ]; + + + $scope.$watch(function () { + return cart.getCart(); + }, function (newValue) { + reset(); + }); + + $scope.$watch(function () { + return $auth.userInfo; + }, function (newValue) { + cart.reset(); + }); + + $scope.checkout = function () { + cart.checkout().then(function (cartData) { + }, function (err) { + Notifications.error("Error checking out: " + err.statusText); + }); + }; + + $scope.isLoggedIn = function () { + return $auth.loggedIn; + }; + $scope.ssoEnabled = function () { + return $auth.ssoEnabled; + }; + + reset(); + }]) + + .controller("HeaderController", + ['$scope', '$location', '$http', 'Notifications', 'cart', 'Auth', + function ($scope, $location, $http, Notifications, cart, $auth) { + $scope.userInfo = $auth.userInfo; + + $scope.cartTotal = 0.0; + $scope.itemCount = 0; + + $scope.isLoggedIn = function () { + return $auth.loggedIn; + }; + + $scope.login = function () { + $auth.login(); + }; + $scope.logout = function () { + $auth.logout(); + }; + $scope.isLoggedIn = function () { + return $auth.loggedIn; + }; + $scope.ssoEnabled = function () { + return $auth.ssoEnabled; + }; + $scope.profile = function () { + $auth.authz.accountManagement(); + }; + $scope.$watch(function () { + return cart.getCart().cartTotal || 0.0; + }, function (newValue) { + $scope.cartTotal = newValue; + }); + + $scope.$watch(function () { + var totalItems = 0; + cart.getCart().shoppingCartItemList.forEach(function (el) { + totalItems += el.quantity; + }); + return totalItems; + }, function (newValue) { + $scope.itemCount = newValue; + }); + + $scope.$watch(function () { + return $auth.userInfo; + }, function (newValue) { + $scope.userInfo = newValue; + }); + + $scope.isActive = function (loc) { + return loc === $location.path(); + } + }]); diff --git a/web-nodejs/app/coolstore.config.js b/web-nodejs/app/coolstore.config.js new file mode 100644 index 0000000..6f72b4d --- /dev/null +++ b/web-nodejs/app/coolstore.config.js @@ -0,0 +1,19 @@ +var config = { + API_ENDPOINT: 'gateway-' + process.env.OPENSHIFT_BUILD_NAMESPACE, + SECURE_API_ENDPOINT: 'secure-gateway-' + process.env.SECURE_COOLSTORE_GW_SERVICE, + SSO_ENABLED: process.env.SSO_URL ? true : false +}; + +if (process.env.COOLSTORE_GW_ENDPOINT != null) { + config.API_ENDPOINT = process.env.COOLSTORE_GW_ENDPOINT; +} else if (process.env.COOLSTORE_GW_SERVICE != null) { + config.API_ENDPOINT = process.env.COOLSTORE_GW_SERVICE + '-' + process.env.OPENSHIFT_BUILD_NAMESPACE; +} + +if (process.env.SECURE_COOLSTORE_GW_ENDPOINT != null) { + config.SECURE_API_ENDPOINT = process.env.SECURE_COOLSTORE_GW_ENDPOINT; +} else if (process.env.SECURE_COOLSTORE_GW_SERVICE != null) { + config.SECURE_API_ENDPOINT = process.env.SECURE_COOLSTORE_GW_SERVICE + '-' + process.env.OPENSHIFT_BUILD_NAMESPACE; +} + +module.exports = config; \ No newline at end of file diff --git a/web-nodejs/app/css/coolstore.css b/web-nodejs/app/css/coolstore.css new file mode 100644 index 0000000..25ee057 --- /dev/null +++ b/web-nodejs/app/css/coolstore.css @@ -0,0 +1,13 @@ +.media img { + height: 100px; +} + +.p-t-8 { + padding-top: 8px !important; +} + +[ng-click], +[data-ng-click], +[x-ng-click] { + cursor: pointer; +} \ No newline at end of file diff --git a/web-nodejs/app/directives/header.js b/web-nodejs/app/directives/header.js new file mode 100644 index 0000000..83906af --- /dev/null +++ b/web-nodejs/app/directives/header.js @@ -0,0 +1,11 @@ +'use strict'; + + +angular.module('app').directive('header', function() { + return { + restrict: 'A', + replace: true, + templateUrl: 'partials/header.html', + controller: 'HeaderController' + } +}); diff --git a/web-nodejs/app/imgs/16 oz. Vortex Tumbler.jpg b/web-nodejs/app/imgs/16 oz. Vortex Tumbler.jpg new file mode 100644 index 0000000..08218f9 Binary files /dev/null and b/web-nodejs/app/imgs/16 oz. Vortex Tumbler.jpg differ diff --git a/web-nodejs/app/imgs/Forge Laptop Sticker.jpg b/web-nodejs/app/imgs/Forge Laptop Sticker.jpg new file mode 100644 index 0000000..bdca803 Binary files /dev/null and b/web-nodejs/app/imgs/Forge Laptop Sticker.jpg differ diff --git a/web-nodejs/app/imgs/Lytro Camera.jpg b/web-nodejs/app/imgs/Lytro Camera.jpg new file mode 100644 index 0000000..8f92611 Binary files /dev/null and b/web-nodejs/app/imgs/Lytro Camera.jpg differ diff --git a/web-nodejs/app/imgs/Oculus Rift.jpg b/web-nodejs/app/imgs/Oculus Rift.jpg new file mode 100644 index 0000000..eac26f2 Binary files /dev/null and b/web-nodejs/app/imgs/Oculus Rift.jpg differ diff --git a/web-nodejs/app/imgs/Ogio Caliber Polo.jpg b/web-nodejs/app/imgs/Ogio Caliber Polo.jpg new file mode 100644 index 0000000..fda0e3f Binary files /dev/null and b/web-nodejs/app/imgs/Ogio Caliber Polo.jpg differ diff --git a/web-nodejs/app/imgs/Pebble Smart Watch.jpg b/web-nodejs/app/imgs/Pebble Smart Watch.jpg new file mode 100644 index 0000000..98d48f8 Binary files /dev/null and b/web-nodejs/app/imgs/Pebble Smart Watch.jpg differ diff --git a/web-nodejs/app/imgs/Red Fedora.jpg b/web-nodejs/app/imgs/Red Fedora.jpg new file mode 100644 index 0000000..aeb5968 Binary files /dev/null and b/web-nodejs/app/imgs/Red Fedora.jpg differ diff --git a/web-nodejs/app/imgs/Solid Performance Polo.jpg b/web-nodejs/app/imgs/Solid Performance Polo.jpg new file mode 100644 index 0000000..eba0ec0 Binary files /dev/null and b/web-nodejs/app/imgs/Solid Performance Polo.jpg differ diff --git a/web-nodejs/app/imgs/logo.png b/web-nodejs/app/imgs/logo.png new file mode 100644 index 0000000..1b2999a Binary files /dev/null and b/web-nodejs/app/imgs/logo.png differ diff --git a/web-nodejs/app/keycloak.config.js b/web-nodejs/app/keycloak.config.js new file mode 100644 index 0000000..3863cdb --- /dev/null +++ b/web-nodejs/app/keycloak.config.js @@ -0,0 +1,12 @@ +var config = +{ + "auth-server-url" : process.env.SSO_URL, + "realm": process.env.SSO_REALM, + "realm-public-key": process.env.SSO_PUBLIC_KEY , + "resource": process.env.SSO_CLIENT_ID, + "ssl-required": 'external', + "public-client": true, + "enable-cors": true +}; + +module.exports = config; \ No newline at end of file diff --git a/web-nodejs/app/routes/routes.js b/web-nodejs/app/routes/routes.js new file mode 100644 index 0000000..39cdfbe --- /dev/null +++ b/web-nodejs/app/routes/routes.js @@ -0,0 +1,13 @@ +'use strict'; + +angular.module('app').config([ '$routeProvider', function($routeProvider) { + $routeProvider.when('/', { + templateUrl : 'partials/home.html', + controller : 'HomeController' + }).when('/cart', { + templateUrl : 'partials/cart.html', + controller : 'CartController' + }).otherwise({ + redirectTo : '/' + }); +} ]); diff --git a/web-nodejs/app/services/cart.js b/web-nodejs/app/services/cart.js new file mode 100644 index 0000000..2c6f8d9 --- /dev/null +++ b/web-nodejs/app/services/cart.js @@ -0,0 +1,121 @@ +'use strict'; + +angular.module("app") + +.factory('cart', ['$http', '$q', 'COOLSTORE_CONFIG', 'Auth', '$location', function($http, $q, COOLSTORE_CONFIG, $auth, $location) { + var factory = {}, cart, products, cartId, baseUrl; + if ($location.protocol() === 'https') { + baseUrl = (COOLSTORE_CONFIG.SECURE_API_ENDPOINT.startsWith("https://") ? COOLSTORE_CONFIG.SECURE_API_ENDPOINT : "https://" + COOLSTORE_CONFIG.SECURE_API_ENDPOINT + '.' + $location.host().replace(/^.*?\.(.*)/g,"$1")) + '/api/cart'; + } else { + baseUrl = (COOLSTORE_CONFIG.API_ENDPOINT.startsWith("http://") ? COOLSTORE_CONFIG.API_ENDPOINT : "http://" + COOLSTORE_CONFIG.API_ENDPOINT + '.' + $location.host().replace(/^.*?\.(.*)/g,"$1")) + '/api/cart'; + } + + factory.checkout = function() { + var deferred = $q.defer(); + $http({ + method: 'POST', + url: baseUrl + '/checkout/' + cartId + }).then(function(resp) { + cart = resp.data; + deferred.resolve(resp.data); + }, function(err) { + deferred.reject(err); + }); + return deferred.promise; + }; + + factory.reset = function() { + cart = { + shoppingCartItemList: [] + }; + var tmpId = localStorage.getItem('cartId'); + var authId = $auth.userInfo ? $auth.userInfo.sub : null; + + if (tmpId && authId) { + // transfer cart + cartId = authId; + this.setCart(tmpId).then(function(result) { + localStorage.removeItem('cartId'); + }, function(err) { + console.log("could not transfer cart " + tmpId + " to cart " + authId + ": " + err); + }); + return; + } + + if (tmpId && !authId) { + cartId = tmpId; + } + + if (!tmpId && authId) { + cartId = authId; + } + + if (!tmpId && !authId) { + tmpId = 'id-' + Math.random(); + localStorage.setItem('cartId', tmpId); + cartId = tmpId; + } + + cart.shoppingCartItemList = []; + $http({ + method: 'GET', + url: baseUrl + '/' + cartId + }).then(function(resp) { + cart = resp.data; + }, function(err) { + }); + + }; + + factory.getCart = function() { + return cart; + }; + + factory.removeFromCart = function(product, quantity) { + var deferred = $q.defer(); + $http({ + method: 'DELETE', + url: baseUrl + '/' + cartId + '/' + product.itemId + '/' + quantity + }).then(function(resp) { + cart = resp.data; + deferred.resolve(resp.data); + }, function(err) { + deferred.reject(err); + }); + return deferred.promise; + + }; + + factory.setCart = function(id) { + var deferred = $q.defer(); + $http({ + method: 'POST', + url: baseUrl + '/' + cartId + '/' + id + }).then(function(resp) { + cart = resp.data; + deferred.resolve(resp.data); + }, function(err) { + deferred.reject(err); + }); + return deferred.promise; + + }; + + factory.addToCart = function(product, quantity) { + var deferred = $q.defer(); + $http({ + method: 'POST', + url: baseUrl + '/' + cartId + '/' + product.itemId + '/' + quantity + }).then(function(resp) { + cart = resp.data; + deferred.resolve(resp.data); + }, function(err) { + deferred.reject(err); + }); + return deferred.promise; + + }; + + factory.reset(); + return factory; +}]); diff --git a/web-nodejs/app/services/catalog.js b/web-nodejs/app/services/catalog.js new file mode 100644 index 0000000..a0c7eaa --- /dev/null +++ b/web-nodejs/app/services/catalog.js @@ -0,0 +1,33 @@ +'use strict'; + +angular.module("app") + +.factory('catalog', ['$http', '$q', 'COOLSTORE_CONFIG', 'Auth', '$location', function($http, $q, COOLSTORE_CONFIG, $auth, $location) { + var factory = {}, products, baseUrl; + + if ($location.protocol() === 'https') { + baseUrl = (COOLSTORE_CONFIG.SECURE_API_ENDPOINT.startsWith("https://") ? COOLSTORE_CONFIG.SECURE_API_ENDPOINT : "https://" + COOLSTORE_CONFIG.SECURE_API_ENDPOINT + '.' + $location.host().replace(/^.*?\.(.*)/g,"$1")) + '/api/products'; + } else { + baseUrl = (COOLSTORE_CONFIG.API_ENDPOINT.startsWith("http://") ? COOLSTORE_CONFIG.API_ENDPOINT : "http://" + COOLSTORE_CONFIG.API_ENDPOINT + '.' + $location.host().replace(/^.*?\.(.*)/g,"$1")) + '/api/products'; + } + + factory.getProducts = function() { + var deferred = $q.defer(); + if (products) { + deferred.resolve(products); + } else { + $http({ + method: 'GET', + url: baseUrl + }).then(function(resp) { + products = resp.data; + deferred.resolve(resp.data); + }, function(err) { + deferred.reject(err); + }); + } + return deferred.promise; + }; + + return factory; +}]); diff --git a/web-nodejs/bower.json b/web-nodejs/bower.json new file mode 100644 index 0000000..ed78e0a --- /dev/null +++ b/web-nodejs/bower.json @@ -0,0 +1,21 @@ +{ + "name": "coolstore-microservice", + "description": "Web UI for the CoolStore Microservices App", + "authors": [ + "Red Hat" + ], + "license": "Apache-2.0", + "private": true, + "devDependencies": { + "patternfly": "3.17.0", + "angular-patternfly": "3.17.0", + "isotope": "2.2.2", + "imagesloaded": "4.1.1", + "angular-route": "1.5.10", + "keycloak": "1.9.8" + }, + "resolutions": { + "angular": "1.5.10", + "jquery": "3.2.1" + } +} \ No newline at end of file diff --git a/web-nodejs/package.json b/web-nodejs/package.json new file mode 100644 index 0000000..c4d2f0a --- /dev/null +++ b/web-nodejs/package.json @@ -0,0 +1,29 @@ +{ + "author": "Red Hat", + "contributors": [ + "James Falkner (https://developers.redhat.com)" + ], + "name": "web", + "repository": "jbossdemocentral/coolstore-microservice", + "version": "1.0.0", + "license": "Apache-2.0", + "private": true, + "description": "Web UI for the CoolStore Microservices App", + "homepage": "https://developers.redhat.com", + "dependencies": { + "cors": "^2.8.3", + "express": "^4.13.4", + "keycloak-connect": "^3.1.0", + "request": "^2.74.0" + }, + "devDependencies": { + "bower": ">=1.7.9", + "bower-nexus3-resolver": "*" + }, + "engines": { + "node": ">=0.10.10" + }, + "scripts": { + "postinstall": "node_modules/.bin/bower --config.registry.search=${BOWER_MIRROR} install" + } +} \ No newline at end of file diff --git a/web-nodejs/server.js b/web-nodejs/server.js new file mode 100644 index 0000000..74014ea --- /dev/null +++ b/web-nodejs/server.js @@ -0,0 +1,47 @@ +var express = require('express'), + http = require('http'), + request = require('request'), + fs = require('fs'), + app = express(), + path = require("path"), + keycloakConfig = require('./app/keycloak.config.js'), + coolstoreConfig = require('./app/coolstore.config.js'), + Keycloak = require('keycloak-connect'), + cors = require('cors'); + + +var port = process.env.PORT || process.env.OPENSHIFT_NODEJS_PORT || 8080, + ip = process.env.IP || process.env.OPENSHIFT_NODEJS_IP || '0.0.0.0', + secport = process.env.PORT || process.env.OPENSHIFT_NODEJS_PORT || 8443; + +// Enable CORS support +app.use(cors()); + +// error handling +app.use(function(err, req, res, next) { + console.error(err.stack); + res.status(500).send('Something bad happened!'); +}); + +// keycloak config server +app.get('/keycloak.json', function(req, res, next) { + res.json(keycloakConfig); +}); +// coolstore config server +app.get('/coolstore.json', function(req, res, next) { + res.json(coolstoreConfig); +}); + +app.use(express.static(path.join(__dirname, '/views'))); +app.use('/app', express.static(path.join(__dirname, '/app'))); +app.use('/bower_components', express.static(path.join(__dirname, '/bower_components'))); + +console.log("coolstore config: " + JSON.stringify(coolstoreConfig)); +console.log("keycloak config: " + JSON.stringify(keycloakConfig)); + + +http.createServer(app).listen(port); + +console.log('HTTP Server running on http://%s:%s', ip, port); + +module.exports = app; \ No newline at end of file diff --git a/web-nodejs/views/index.html b/web-nodejs/views/index.html new file mode 100644 index 0000000..d5fdd42 --- /dev/null +++ b/web-nodejs/views/index.html @@ -0,0 +1,53 @@ + + + + + + Red Hat CoolStore Microservices App + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + \ No newline at end of file diff --git a/web-nodejs/views/partials/cart.html b/web-nodejs/views/partials/cart.html new file mode 100644 index 0000000..88efdd4 --- /dev/null +++ b/web-nodejs/views/partials/cart.html @@ -0,0 +1,87 @@ +
+ +
+ +
+
+
+ +
+ +
+
+ {{item.product.name}} +
+
+ +

{{item.product.name}}

+
+ Quantity: {{item.quantity}} +
+
+ {{item.product.description}} +
+
+
+
+
+
+
+

You have not added any items to your Shopping Cart!

+ Return to Store +
+
+
+ +
+
+

Shopping Summary

+
+
+ +

Cart Total: {{subtotal | currency}}

+

Promotional Item Savings: {{cart.cartItemPromoSavings | currency}} +

+

Subtotal: {{cart.cartItemTotal | currency}}

+

Shipping: {{cart.shippingTotal | currency}}

+

Promotional Shipping Savings: {{cart.shippingPromoSavings | currency}} +

+

+ Total Order Amount: {{cart.cartTotal | currency}} +

+
+ +
+
+ + + \ No newline at end of file diff --git a/web-nodejs/views/partials/header.html b/web-nodejs/views/partials/header.html new file mode 100644 index 0000000..fb2c6e6 --- /dev/null +++ b/web-nodejs/views/partials/header.html @@ -0,0 +1,50 @@ + diff --git a/web-nodejs/views/partials/home.html b/web-nodejs/views/partials/home.html new file mode 100644 index 0000000..7fc6e84 --- /dev/null +++ b/web-nodejs/views/partials/home.html @@ -0,0 +1,53 @@ +
+
+
+
+
+ +
+
+ +
+
+ +
+

{{item.product.price | currency}}

+
+   +
+ +
 Not in Stock
+
+ +
+
+ + +
+ +
+
+
+
+
+ +
+ + \ No newline at end of file