├── src └── main │ ├── resources │ ├── application.properties │ └── static │ │ ├── assets │ │ ├── pod.png │ │ ├── frame.png │ │ ├── gunshot.wav │ │ ├── service.png │ │ ├── sight.png │ │ ├── storage.png │ │ ├── yeehaw.wav │ │ ├── explosion.wav │ │ ├── gunsight.png │ │ ├── sceenshot.png │ │ ├── kubernetes.png │ │ ├── gameplayfield.png │ │ └── game.js │ │ ├── index.html │ │ └── css │ │ ├── styles.css │ │ ├── popper.min.js │ │ └── jquery-3.7.0.min.js │ └── java │ └── io │ └── techdope │ └── wildwest │ ├── WildWestApplication.java │ ├── models │ ├── Score.java │ ├── PlatformObject.java │ └── Game.java │ ├── controllers │ ├── APIController.java │ └── GameController.java │ └── helpers │ └── PlatformObjectHelper.java ├── kubernetes ├── service.yaml ├── rbac.yaml ├── destructive.yaml ├── ingress.yaml └── k8s.yaml ├── Dockerfile ├── README.md └── pom.xml /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/static/assets/pod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gshipley/wild-west-kubernetes/HEAD/src/main/resources/static/assets/pod.png -------------------------------------------------------------------------------- /src/main/resources/static/assets/frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gshipley/wild-west-kubernetes/HEAD/src/main/resources/static/assets/frame.png -------------------------------------------------------------------------------- /src/main/resources/static/assets/gunshot.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gshipley/wild-west-kubernetes/HEAD/src/main/resources/static/assets/gunshot.wav -------------------------------------------------------------------------------- /src/main/resources/static/assets/service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gshipley/wild-west-kubernetes/HEAD/src/main/resources/static/assets/service.png -------------------------------------------------------------------------------- /src/main/resources/static/assets/sight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gshipley/wild-west-kubernetes/HEAD/src/main/resources/static/assets/sight.png -------------------------------------------------------------------------------- /src/main/resources/static/assets/storage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gshipley/wild-west-kubernetes/HEAD/src/main/resources/static/assets/storage.png -------------------------------------------------------------------------------- /src/main/resources/static/assets/yeehaw.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gshipley/wild-west-kubernetes/HEAD/src/main/resources/static/assets/yeehaw.wav -------------------------------------------------------------------------------- /src/main/resources/static/assets/explosion.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gshipley/wild-west-kubernetes/HEAD/src/main/resources/static/assets/explosion.wav -------------------------------------------------------------------------------- /src/main/resources/static/assets/gunsight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gshipley/wild-west-kubernetes/HEAD/src/main/resources/static/assets/gunsight.png -------------------------------------------------------------------------------- /src/main/resources/static/assets/sceenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gshipley/wild-west-kubernetes/HEAD/src/main/resources/static/assets/sceenshot.png -------------------------------------------------------------------------------- /src/main/resources/static/assets/kubernetes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gshipley/wild-west-kubernetes/HEAD/src/main/resources/static/assets/kubernetes.png -------------------------------------------------------------------------------- /src/main/resources/static/assets/gameplayfield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gshipley/wild-west-kubernetes/HEAD/src/main/resources/static/assets/gameplayfield.png -------------------------------------------------------------------------------- /kubernetes/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: wildwest 5 | namespace: wildwest 6 | labels: 7 | app: wildwest 8 | spec: 9 | type: ClusterIP 10 | ports: 11 | - name: wildwest 12 | port: 8080 13 | selector: 14 | app: wildwest -------------------------------------------------------------------------------- /kubernetes/rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | creationTimestamp: null 5 | name: view 6 | namespace: wildwest 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: ClusterRole 10 | name: view 11 | subjects: 12 | - kind: ServiceAccount 13 | name: default 14 | namespace: wildwest 15 | -------------------------------------------------------------------------------- /kubernetes/destructive.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | creationTimestamp: null 5 | name: edit 6 | namespace: wildwest 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: ClusterRole 10 | name: edit 11 | subjects: 12 | - kind: ServiceAccount 13 | name: default 14 | namespace: wildwest 15 | -------------------------------------------------------------------------------- /src/main/java/io/techdope/wildwest/WildWestApplication.java: -------------------------------------------------------------------------------- 1 | package io.techdope.wildwest; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class WildWestApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(WildWestApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.6.2-jdk-11 as builder 2 | COPY src /usr/src/app/src 3 | COPY pom.xml /usr/src/app 4 | RUN mvn -f /usr/src/app/pom.xml clean package 5 | FROM adoptopenjdk/openjdk11:latest as runtime 6 | LABEL maintainer="gshipley@gmail.com" 7 | EXPOSE 8080 8 | COPY --from=builder /usr/src/app/target/wildwest-1.0.jar /usr/app/wildwest.jar 9 | ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/usr/app/wildwest.jar"] 10 | -------------------------------------------------------------------------------- /kubernetes/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: ingress-wildwest 5 | namespace: wildwest 6 | annotations: 7 | # use the shared ingress-nginx 8 | kubernetes.io/ingress.class: "nginx" 9 | spec: 10 | rules: 11 | - host: wildwest.apps.techdope.io 12 | http: 13 | paths: 14 | - path: / 15 | backend: 16 | serviceName: wildwest 17 | servicePort: 8080 18 | -------------------------------------------------------------------------------- /src/main/java/io/techdope/wildwest/models/Score.java: -------------------------------------------------------------------------------- 1 | package io.techdope.wildwest.models; 2 | 3 | public class Score { 4 | 5 | private int score; 6 | private String gameID; 7 | 8 | public String getGameID() { 9 | return gameID; 10 | } 11 | public void setGameID(String gameID) { 12 | this.gameID = gameID; 13 | } 14 | public Score() { 15 | this.score = 0; 16 | } 17 | public void setScore(int score) { 18 | this.score = score; 19 | } 20 | 21 | public int getScore() { 22 | return this.score; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/io/techdope/wildwest/models/PlatformObject.java: -------------------------------------------------------------------------------- 1 | package io.techdope.wildwest.models; 2 | 3 | public class PlatformObject { 4 | 5 | private String objectID; 6 | private String objectName; 7 | private String objectType; 8 | 9 | public PlatformObject(String objectID, String objectName, String objectType) { 10 | this.objectID = objectID; 11 | this.objectName = objectName; 12 | this.objectType = objectType; 13 | } 14 | public String getObjectID() { 15 | return objectID; 16 | } 17 | public void setObjectID(String objectID) { 18 | this.objectID = objectID; 19 | } 20 | public String getObjectType() { 21 | return objectType; 22 | } 23 | public void setObjectType(String objectType) { 24 | this.objectType = objectType; 25 | } 26 | public String getObjectName() { 27 | return objectName; 28 | } 29 | public void setObjectName(String objectName) { 30 | this.objectName = objectName; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /kubernetes/k8s.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: wildwest 5 | 6 | --- 7 | 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: wildwest 12 | namespace: wildwest 13 | spec: 14 | replicas: 5 15 | selector: 16 | matchLabels: 17 | app: wildwest 18 | template: 19 | metadata: 20 | labels: 21 | app: wildwest 22 | spec: 23 | terminationGracePeriodSeconds: 0 24 | containers: 25 | - name: wildwest 26 | image: gshipley/wildwest:latest 27 | imagePullPolicy: Always 28 | ports: 29 | - containerPort: 8080 30 | 31 | --- 32 | 33 | apiVersion: v1 34 | kind: Service 35 | metadata: 36 | name: wildwest 37 | namespace: wildwest 38 | labels: 39 | app: wildwest 40 | spec: 41 | type: NodePort 42 | ports: 43 | - name: wildwest 44 | port: 8080 45 | selector: 46 | app: wildwest 47 | 48 | --- 49 | apiVersion: rbac.authorization.k8s.io/v1 50 | kind: RoleBinding 51 | metadata: 52 | creationTimestamp: null 53 | name: view 54 | namespace: wildwest 55 | roleRef: 56 | apiGroup: rbac.authorization.k8s.io 57 | kind: ClusterRole 58 | name: view 59 | subjects: 60 | - kind: ServiceAccount 61 | name: default 62 | namespace: wildwest 63 | -------------------------------------------------------------------------------- /src/main/java/io/techdope/wildwest/models/Game.java: -------------------------------------------------------------------------------- 1 | package io.techdope.wildwest.models; 2 | 3 | import java.util.Hashtable; 4 | 5 | public class Game { 6 | 7 | private Score score; 8 | private Hashtable gameObjects; 9 | 10 | // The GameMode enum can be used to provide access to kubernetes distributions that extend K8s 11 | // with their own platform objects. If adding a game mode, you will need to ensure that the 12 | // K8s distribution you are targeting uses the same API client. 13 | public enum GameMode { 14 | KUBERNETES 15 | } 16 | 17 | public GameMode mode = GameMode.KUBERNETES; 18 | 19 | public Game() { 20 | score = new Score(); 21 | gameObjects = new Hashtable<>(); 22 | } 23 | 24 | public Score getScore() { 25 | return score; 26 | } 27 | 28 | public void setScore(Score score) { 29 | this.score = score; 30 | } 31 | 32 | public void addGameObject(PlatformObject newObject) { 33 | if (!gameObjects.containsKey(newObject.getObjectID())) { 34 | gameObjects.put(newObject.getObjectID(), newObject); 35 | } 36 | } 37 | 38 | public void removeGameObject(PlatformObject theObject) { 39 | gameObjects.remove(theObject.getObjectID()); 40 | } 41 | 42 | public GameMode getGameMode() { 43 | return mode; 44 | } 45 | 46 | public void setGameMode(GameMode mode) { 47 | this.mode = mode; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Wild West Kubernetes** 2 | 3 | ![](https://github.com/gshipley/wild-west-kubernetes/raw/master/src/main/resources/static/assets/sceenshot.png) 4 | 5 | Wild West Kubernetes is a sample application written in Spring Boot and the Phaser game engine to make killing pods fun. In order to play the game, you need to have a kubernetes cluster running and issue the following command: 6 | 7 | ``` 8 | kubectl apply -f https://git.io/k8s-wild-west 9 | ``` 10 | 11 | If you are using OpenShift, you will need to add a role to a service account 12 | ``` 13 | oc policy add-role-to-user view system:serviceaccount:wildwest:default 14 | ``` 15 | Where wildwest is the name of the project in openshift. 16 | 17 | 18 | This will create a namespace called *wildwest* and deploy the gshipley/wildwest:latest docker image with five replicas. This will also create a service and apply the correct RBAC view role to pull information from the kubernetes API. It should be noted that the default kubernetes/k8s.yaml file creates a service using a NodePort. This should work in most minikube environments but you should change it if you are running on a cluster with a LoadBalancer. For an example of an ingress, check out the kubernetes/ingress.yaml file. 19 | 20 | By default, the game doesn't actually destroy the pods when you shoot them. If you want to enable destructive mode, issue the following command: 21 | 22 | ``` 23 | kubectl apply -f https://git.io/k8s-wild-west-destructive 24 | ``` 25 | 26 | For OpenShift, apply this role for destructive mode 27 | ``` 28 | oc policy add-role-to-user edit system:serviceaccount:wildwest:default 29 | ``` 30 | 31 | 32 | Once you have the game deployed, you will need to expose the service so that you can access the web application. If you are using minikube, you could use port-fowarding: 33 | 34 | ``` 35 | kubectl port-forward -n wildwest svc/wildwest 8080:8080 36 | ``` 37 | 38 | Happy Pod hunting. 39 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | io.techdope 7 | wildwest 8 | 1.0 9 | jar 10 | 11 | wildwest 12 | wildwest 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.6.3 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 11 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-web 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-test 39 | test 40 | 41 | 42 | org.springframework 43 | spring-web 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-devtools 48 | 49 | 50 | io.kubernetes 51 | client-java 52 | 11.0.1 53 | compile 54 | 55 | 56 | 57 | 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-maven-plugin 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/main/java/io/techdope/wildwest/controllers/APIController.java: -------------------------------------------------------------------------------- 1 | package io.techdope.wildwest.controllers; 2 | 3 | import java.util.List; 4 | 5 | import io.techdope.wildwest.helpers.PlatformObjectHelper; 6 | import io.techdope.wildwest.models.Game; 7 | import io.techdope.wildwest.models.PlatformObject; 8 | import io.techdope.wildwest.models.Score; 9 | 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestParam; 13 | import org.springframework.web.bind.annotation.RestController; 14 | import org.springframework.web.bind.annotation.CrossOrigin; 15 | 16 | 17 | 18 | @CrossOrigin(origins = "*") 19 | @RestController 20 | public class APIController { 21 | @Autowired 22 | private GameController gameController; 23 | 24 | @RequestMapping("/score") 25 | public Score getScore(@RequestParam(value = "gameID") String gameID) { 26 | return this.gameController.getGame(gameID).getScore(); 27 | } 28 | 29 | @RequestMapping("/createGame") 30 | public Game getScore() { 31 | return gameController.createGame(); 32 | } 33 | 34 | @RequestMapping("/egg") 35 | public String easterEgg() { 36 | return "Every game needs an easter egg!!"; 37 | } 38 | 39 | @RequestMapping("/objects") 40 | public List getPlatformObjects() { 41 | PlatformObjectHelper helper = new PlatformObjectHelper(); 42 | return helper.getPlatformObjects(); 43 | } 44 | 45 | @RequestMapping("/getRandomObject") 46 | public PlatformObject getRandomPlatformObject() { 47 | PlatformObjectHelper helper = new PlatformObjectHelper(); 48 | return helper.getRandomPlatformObject(); 49 | } 50 | 51 | @CrossOrigin 52 | @RequestMapping("/deleteObject") 53 | public void deletePlatformObject(@RequestParam(value = "gameID") String gameID, 54 | @RequestParam(value = "objectID") String objectID, 55 | @RequestParam(value = "objectType") String objectType, 56 | @RequestParam(value = "objectName") String objectName) { 57 | 58 | PlatformObjectHelper helper = new PlatformObjectHelper(); 59 | helper.deletePlatformObject(gameID, objectID, objectType, objectName); 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /src/main/java/io/techdope/wildwest/controllers/GameController.java: -------------------------------------------------------------------------------- 1 | package io.techdope.wildwest.controllers; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.context.annotation.Scope; 5 | 6 | import io.techdope.wildwest.models.Game; 7 | import io.techdope.wildwest.models.Score; 8 | 9 | import java.util.Hashtable; 10 | import java.util.Random; 11 | 12 | 13 | 14 | @Configuration 15 | @Scope(value = "singleton") 16 | public class GameController { 17 | private Hashtable games = new Hashtable<>(); 18 | 19 | public Game createGame() { 20 | Game newGame = new Game(); 21 | Score gameScore = new Score(); 22 | gameScore.setGameID(this.generateGameID()); 23 | gameScore.setScore(0); 24 | newGame.setScore(gameScore); 25 | newGame.setGameMode(this.determineGameMode()); 26 | 27 | games.put(newGame.getScore().getGameID(), newGame); 28 | 29 | return newGame; 30 | } 31 | 32 | public Game getGame(String gameID) { 33 | return this.games.get(gameID); 34 | } 35 | 36 | public void deleteGame(String gameID) { 37 | this.games.remove(gameID); 38 | } 39 | 40 | private String generateGameID() { 41 | String randomChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; 42 | StringBuilder gameID = new StringBuilder(); 43 | Random rnd = new Random(); 44 | while (gameID.length() < 18) { 45 | int index = (int) (rnd.nextFloat() * randomChars.length()); 46 | gameID.append(randomChars.charAt(index)); 47 | } 48 | 49 | return gameID.toString(); 50 | } 51 | 52 | private Game.GameMode determineGameMode() { 53 | Game.GameMode currentMode = Game.GameMode.KUBERNETES; 54 | 55 | // For now, the game mode is determined by an environment variable in the backend pod named GAME_MODE 56 | // If this environment variable is not set, it will default to KUBERNETES. 57 | // Given that most people will be using Kubernetes as their distribution for this game, a sane default 58 | // of kubernetes was chosen and will be used if no environment variable is defined. 59 | // This was added in case someone wants to add support for specific distributions of K8s 60 | 61 | if (System.getenv().containsKey("GAME_MODE")) { 62 | // possible game mode options are kube, k, kubernetes, openshift 63 | String gameEnvironmentVariable = System.getenv("GAME_MODE"); 64 | switch (gameEnvironmentVariable) { 65 | case "kube": 66 | case "kubernetes": 67 | case "k": 68 | currentMode = Game.GameMode.KUBERNETES; 69 | break; 70 | default: 71 | currentMode = Game.GameMode.KUBERNETES; 72 | } 73 | } 74 | return currentMode; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | kubernetes 17 |
18 |
19 |
20 |
21 |
22 | 52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
© gshipley
64 |
65 |
66 | 67 | 86 |
87 | 88 | 89 | -------------------------------------------------------------------------------- /src/main/java/io/techdope/wildwest/helpers/PlatformObjectHelper.java: -------------------------------------------------------------------------------- 1 | package io.techdope.wildwest.helpers; 2 | 3 | import io.kubernetes.client.openapi.ApiClient; 4 | import io.kubernetes.client.openapi.Configuration; 5 | import io.kubernetes.client.openapi.apis.CoreV1Api; 6 | import io.kubernetes.client.openapi.models.*; 7 | import io.kubernetes.client.util.ClientBuilder; 8 | import io.techdope.wildwest.models.PlatformObject; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.Random; 13 | 14 | public class PlatformObjectHelper { 15 | 16 | private ApiClient client; 17 | private CoreV1Api api; 18 | public PlatformObjectHelper() { 19 | try { 20 | // Let's establish a connection to the API server 21 | client = ClientBuilder.cluster().build(); 22 | // set the global default api-client to the in-cluster one from above 23 | Configuration.setDefaultApiClient(client); 24 | // the CoreV1Api loads default api-client from global configuration. 25 | api = new CoreV1Api(); 26 | } catch (Exception e) { 27 | e.printStackTrace(); 28 | } 29 | } 30 | 31 | public List getPlatformObjects() { 32 | 33 | ArrayList platformObjects = new ArrayList<>(); 34 | platformObjects.addAll(this.getPods()); 35 | platformObjects.addAll(this.getPVs()); 36 | platformObjects.addAll(this.getServices()); 37 | 38 | return platformObjects; 39 | 40 | } 41 | 42 | public PlatformObject getRandomPlatformObject() { 43 | List theObjects = this.getPlatformObjects(); 44 | 45 | 46 | return theObjects.get(new Random().nextInt(theObjects.size())); 47 | } 48 | 49 | private List getPods() { 50 | ArrayList thePods = new ArrayList<>(); 51 | try { 52 | V1PodList pods = api.listNamespacedPod("wildwest", null, null, null, null, null, null, null, null, null, null); 53 | for (V1Pod item : pods.getItems()) { 54 | thePods.add(new PlatformObject(item.getMetadata().getUid(), item.getMetadata().getName(), "POD")); 55 | } 56 | } catch (Exception e) { 57 | e.printStackTrace(); 58 | } 59 | return thePods; 60 | } 61 | 62 | 63 | private List getPVs() { 64 | ArrayList thePVs = new ArrayList<>(); 65 | try { 66 | V1PersistentVolumeClaimList pvs = api.listNamespacedPersistentVolumeClaim("wildwest",null, true, null,null,null,null,null 67 | ,null,null,false); 68 | 69 | for (V1PersistentVolumeClaim item : pvs.getItems()) { 70 | thePVs.add(new PlatformObject(item.getMetadata().getUid(), item.getMetadata().getName(), "PVC")); 71 | } 72 | } catch (Exception e) { 73 | e.printStackTrace(); 74 | } 75 | return thePVs; 76 | } 77 | 78 | private List getServices() { 79 | ArrayList theServices = new ArrayList<>(); 80 | try { 81 | V1ServiceList services = api.listNamespacedService("wildwest", null, true, null, null, null, null, null, null, null, null); 82 | 83 | for (V1Service item : services.getItems()) { 84 | theServices.add(new PlatformObject(item.getMetadata().getUid(), item.getMetadata().getName(), "SERVICE")); 85 | } 86 | } catch (Exception e) { 87 | e.printStackTrace(); 88 | } 89 | return theServices; 90 | } 91 | 92 | public void deletePlatformObject(String gameID, String objectID, String objectType, String objectName) { 93 | try { 94 | 95 | switch (objectType) { 96 | case "POD": 97 | //client.pods().withName(objectName).delete(); 98 | api.deleteNamespacedPod(objectName, "wildwest", null, null, null, null, null, null); 99 | break; 100 | case "SERVICE": 101 | //client.builds().withName(objectName).delete(); 102 | break; 103 | case "PVC": 104 | //client.builds().withName(objectName).delete(); 105 | break; 106 | } 107 | } catch (Exception e) { 108 | e.printStackTrace(); 109 | } 110 | 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/resources/static/css/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | body { 5 | margin: 0 auto; 6 | font-family: "Montserrat", "Europa", "Helvetica", sans-serif; 7 | 8 | } 9 | #main { 10 | display: flex; 11 | min-height: calc(100vh - 40vh); 12 | background-color:#2c2c2b; 13 | } 14 | #main > article { 15 | background:#191919; 16 | } 17 | #main > nav, 18 | #main > aside { 19 | flex: auto; 20 | } 21 | 22 | #main > nav { 23 | order: -1; 24 | max-width: 25%; 25 | margin: 0; 26 | max-height: 100%; 27 | height: 100%; 28 | 29 | } 30 | 31 | footer { 32 | background: #2c2c2b; 33 | height: 14vh; 34 | color: #fff; 35 | text-transform: uppercase; 36 | } 37 | header { 38 | background: white; 39 | height: 10vh; 40 | margin: 1em; 41 | } 42 | 43 | aside { 44 | width: 25%; 45 | } 46 | 47 | h1 { 48 | font-size: 2em; 49 | text-transform: uppercase; 50 | letter-spacing: 105%; 51 | font-weight: 600; 52 | padding:0; 53 | color: #fff; 54 | } 55 | 56 | h2 { 57 | text-transform: uppercase; 58 | letter-spacing: 105%; 59 | font-weight: 400; 60 | padding: 0; 61 | color: #fff; 62 | text-align: center; 63 | } 64 | 65 | h3 { 66 | color: #ef4048; 67 | text-transform: uppercase; 68 | font-weight: 300; 69 | } 70 | 71 | h4 { 72 | text-align: center; 73 | text-transform: uppercase; 74 | font-weight: 200; 75 | } 76 | 77 | h5 { 78 | text-align: center; 79 | text-transform: uppercase; 80 | font-weight: 200; 81 | color: #fff; 82 | 83 | } 84 | 85 | p { 86 | font-weight: lighter; 87 | font-size: .8em; 88 | } 89 | 90 | .heading { 91 | background: #1E90FF; 92 | color: #fff; 93 | text-align:center; 94 | padding: 1em; 95 | margin: 0; 96 | } 97 | 98 | .info { 99 | background: #2c2c2b; 100 | color: #fff; 101 | padding: 1em; 102 | } 103 | 104 | .heading hr { 105 | display: block; 106 | height: 1px; 107 | border: 0; 108 | border-top: 1px solid #f15c22; 109 | margin-top: 0; 110 | padding: 0; 111 | } 112 | .legend { 113 | background: #191919; 114 | color: #fff; 115 | 116 | } 117 | 118 | .grid { 119 | display: flex; 120 | justify-content: center; 121 | } 122 | 123 | .suggestion{ 124 | width: 100px; 125 | height: 75px; 126 | background: #383838; 127 | border: 1px solid #515151; 128 | flex-direction: column; 129 | flex-wrap: center; 130 | margin: 1em; 131 | } 132 | 133 | 134 | .grid2 { 135 | display: flex; 136 | justify-content: space-between; 137 | 138 | } 139 | .grid2 div { 140 | flex-direction: row; 141 | flex-wrap: wrap; 142 | margin: 20px; 143 | } 144 | 145 | .accent { 146 | height: 120px; 147 | max-height: 100%; 148 | background: #2c2c2b; 149 | padding: 1em; 150 | } 151 | .stripes { 152 | height: 1px; 153 | border: 0; 154 | margin: .8em 0; 155 | padding: 0; 156 | } 157 | 158 | .stripes1{ 159 | border-top: 1px solid #1aa249; 160 | } 161 | 162 | .stripes2{ 163 | border-top: 1px solid #cd2028; 164 | } 165 | 166 | .stripes3{ 167 | border-top: 1px solid #cdbc2c; 168 | } 169 | 170 | .stripes4{ 171 | border-top: 1px solid #43423a; 172 | } 173 | .col-sm-3 { 174 | display:inline-block; 175 | 176 | } 177 | 178 | 179 | /* Accordion Menu */ 180 | .accordion { 181 | background: #2c2c2b; 182 | color: #fff; 183 | cursor: pointer; 184 | padding: 1em; 185 | width: 100%; 186 | border: 1px solid #383838; 187 | text-align: left; 188 | font-size: 15px; 189 | transition: 0.4s; 190 | text-decoration: none; 191 | outline: none; 192 | font-weight: 100; 193 | transition: 0.4s; 194 | 195 | 196 | } 197 | 198 | .active, .accordion:hover { 199 | background-color: #383838; 200 | } 201 | 202 | .accordion:after { 203 | content: '\002B'; 204 | color: #777; 205 | font-weight: bold; 206 | float: right; 207 | margin-left: 5px; 208 | } 209 | 210 | .panel { 211 | padding: .7em .5em .7em .5em; 212 | display: none; 213 | background: #2c2c2b; 214 | color: #fff; 215 | border-bottom: 1px solid #383838; 216 | 217 | } 218 | 219 | .panel.panel-open { 220 | display: block; 221 | } 222 | 223 | .active:after { 224 | content: "\2212"; 225 | } 226 | 227 | .banner img{ 228 | padding: 1em; 229 | display: block; 230 | width:200px; 231 | max-width:200px; 232 | } 233 | 234 | @media (max-width:640px){ 235 | .banner img{ 236 | display:none; 237 | } 238 | } 239 | 240 | .gameholder { 241 | height: auto; 242 | width: 100%; 243 | } 244 | 245 | .gamePlaceholder { 246 | width: 100%; 247 | background: no-repeat; 248 | height: auto; 249 | max-width:100%; 250 | } 251 | 252 | .jumbotron { 253 | background-color:transparent; 254 | } 255 | 256 | .menufield { 257 | padding-right: 0; 258 | } 259 | 260 | .menufield nav { 261 | background:#191919; 262 | } 263 | 264 | .gamefield { 265 | padding-left: 0; 266 | } -------------------------------------------------------------------------------- /src/main/resources/static/assets/game.js: -------------------------------------------------------------------------------- 1 | //******************************************************************* 2 | //** Wild West K8s 3 | //** Author: Grant Shipley @gshipley 4 | //** Thanks to Marek Jelen, Jorge Morales, and RyanJ 5 | //** Shootem-up game to kill K8s pods 6 | //******************************************************************* 7 | var game; 8 | var gunSight; 9 | var gunshot; 10 | var currObject; // This is the sprite in the game representing the current K8s Object 11 | var currentK8sObject; 12 | var emitter; 13 | var gameLoop; 14 | var index=0; 15 | var frameObject; 16 | var line=''; 17 | var yeehaw; 18 | var explosion; 19 | var gameID; 20 | var objectText; 21 | var killFrameText; 22 | var scoreText; 23 | var introText; 24 | var gameScore = 0; 25 | var noviceMode = false; 26 | var content = [ 27 | " ", 28 | "Wild West K8s", 29 | " " 30 | ]; 31 | var locations = [ 32 | [341, 409], // door 1 33 | [585, 420], // door 2 34 | [7825, 425], // door 3 35 | [643, 122], // top of building 3 36 | [955, 287], // building 4 balcony 37 | [149, 149], // building 1 left roof 38 | [149, 140], // building 1 right roof 39 | [860, 634], // Barrel 40 | [30, 530] // cactus 41 | ]; 42 | 43 | // We need to create the game on the server 44 | $.ajax({ 45 | url: '/createGame', 46 | async: false, 47 | success: function(results) { 48 | gameID = results.score.gameID; 49 | 50 | // Now that we have a gameID from the server, we can start the game 51 | game = new Phaser.Game(1151, 768, Phaser.AUTO, 'k8sgame', { preload: preload, create: create, update: update, render: render }); 52 | } 53 | }); 54 | 55 | function preload() { 56 | 57 | game.load.image('playfield', 'assets/gameplayfield.png'); 58 | game.load.image('gunsight', 'assets/gunsight.png'); 59 | game.load.audio('gunshot', 'assets/gunshot.wav'); 60 | game.load.image('SERVICE', 'assets/service.png'); 61 | game.load.image('POD', 'assets/pod.png'); 62 | game.load.image('PVC', 'assets/storage.png'); 63 | game.load.audio('yeehaw', 'assets/yeehaw.wav'); 64 | game.load.audio('explosion', 'assets/explosion.wav'); 65 | game.load.image('killframe', 'assets/frame.png'); 66 | } 67 | 68 | function create() { 69 | // load the playfield background image 70 | var playfield = game.add.image(game.world.centerX, game.world.centerY, 'playfield'); 71 | playfield.anchor.setTo(0.5, 0.5); 72 | 73 | // Start the physics system for the gunsight and explosion 74 | game.physics.startSystem(Phaser.Physics.ARCADE); 75 | 76 | introText = game.add.text(32, 660, '', { font: "26pt Courier", fill: "#000000", stroke: "#000000", strokeThickness: 2 }); 77 | scoreText = game.add.text(765, 10, 'Score: 000', { font: "16pt Courier", fill: "#000000", stroke: "#000000", strokeThickness: 2 }); 78 | 79 | objectText = game.add.text(32, 670, '', { font: "16pt Courier", fill: "#000000", stroke: "#000000", strokeThickness: 2 }); 80 | 81 | //display the intro text 82 | displayIntroText(); 83 | 84 | // Load the gunshot audio 85 | gunshot = game.add.audio('gunshot'); 86 | // Load the yeehaw 87 | yeehaw = game.add.audio('yeehaw'); 88 | // Play the intro sound 89 | yeehaw.play(); 90 | 91 | // Set the explosion sound 92 | explosion = game.add.audio('explosion'); 93 | 94 | 95 | // load the gun sights 96 | gunSight = game.add.sprite(game.world.centerX, game.world.centerY, 'gunsight'); 97 | gunSight.anchor.set(0.5); 98 | game.physics.arcade.enable(gunSight); 99 | gunSight.inputEnabled = true; 100 | 101 | // If the player fired their weapon 102 | gunSight.events.onInputDown.add(function () { 103 | gunshot.play(); 104 | // Check if the gunsight is over the currentObject 105 | 106 | if(checkOverlap(gunSight, currObject)) { 107 | // delete the object on the game server 108 | stopGameDisplayLoop(); 109 | deletePlatformObject(currentK8sObject); 110 | 111 | // Add the emitter for the explosion and play the yeehaw for target hit 112 | explosion.play(); 113 | emitter = game.add.emitter(0, 0, 100); 114 | emitter.makeParticles(currentK8sObject.objectType); 115 | emitter.gravity = 200; 116 | 117 | // Position the emitter where the mouse/touch event was 118 | emitter.x = locations[currLocation][0]; 119 | emitter.y = locations[currLocation][1]; 120 | 121 | // The first parameter sets the effect to "explode" which means all particles are emitted at once 122 | // The second gives each particle a 2000ms lifespan 123 | // The third is ignored when using burst/explode mode 124 | // The final parameter (10) is how many particles will be emitted in this single burst 125 | emitter.start(true, 2000, null, 10); 126 | 127 | currObject.destroy(); 128 | 129 | objectText.text=""; 130 | scoreText.text = "Score: ".concat(gameScore += 100); 131 | } else { 132 | // The player missed the target and should be penalized with a deduction in score 133 | scoreText.text = "Score: ".concat(gameScore -= 50); 134 | } 135 | }, this); 136 | 137 | } 138 | 139 | function displayKillFrame() { 140 | frameObject = game.add.sprite(220, 153, 'killframe'); 141 | frameObject.inputEnabled = true; 142 | 143 | killFrameText = game.add.text(330, 270, '', { font: "26pt Courier", fill: "#000000", stroke: "#000000", strokeThickness: 2 }); 144 | killFrameText.setText(killFrameHelp[currentK8sObject.objectType]); 145 | 146 | frameObject.events.onInputDown.add(function() { 147 | frameObject.destroy(); 148 | killFrameText.destroy(); 149 | startGameDisplayLoop(); 150 | }, this); 151 | } 152 | 153 | function displayObject() { 154 | 155 | // Get a random location from the location array as defined in the locations array 156 | currLocation = getRandomLocation(0, locations.length-1); 157 | 158 | // Get a random object from the kubernetes API 159 | getRandomKubernetesObject(); 160 | 161 | // Add the object to the playfiend using the random location 162 | currObject = game.add.sprite(locations[currLocation][0], locations[currLocation][1], currentK8sObject.objectType); 163 | 164 | //delete the kubernetes object after it has been visible for 3 seconds. 165 | game.time.events.add(Phaser.Timer.SECOND * 2, function() { 166 | currObject.destroy(); 167 | objectText.text = ""; 168 | }); 169 | gunSight.bringToTop(); 170 | } 171 | 172 | 173 | function getRandomKubernetesObject() { 174 | $.ajax({ 175 | url: '/getRandomObject', 176 | async: false, 177 | success: function(results) { 178 | currentK8sObject = results; 179 | objectText.text = "Type: " + results.objectType + "\nName: " + results.objectName + "\nID: " + results.objectID; 180 | 181 | } 182 | }); 183 | } 184 | 185 | function deletePlatformObject() { 186 | $.ajax({ 187 | url: '/deleteObject', 188 | async: false, 189 | type: 'GET', 190 | data: { gameID: gameID, objectType : currentK8sObject.objectType, objectName : currentK8sObject.objectName, objectID : currentK8sObject.objectID }, 191 | success: function() { 192 | if(noviceMode == false) { 193 | startGameDisplayLoop(); 194 | } else { 195 | displayKillFrame(currentK8sObject); 196 | 197 | } 198 | }, 199 | error: function() { 200 | startGameDisplayLoop(); 201 | } 202 | }) 203 | } 204 | function checkOverlap(spriteA, spriteB) { 205 | if(typeof spriteA != 'undefined' && typeof spriteB != 'undefined') { 206 | var boundsA = spriteA.getBounds(); 207 | var boundsB = spriteB.getBounds(); 208 | 209 | return Phaser.Rectangle.intersects(boundsA, boundsB); 210 | } 211 | 212 | } 213 | 214 | function getRandomLocation(min,max){ 215 | return Math.floor(Math.random()*(max-min+1)+min); 216 | } 217 | 218 | function update() { 219 | 220 | // If the gunsight is > 8px away from the pointer then let's move to it 221 | if (game.physics.arcade.distanceToPointer(gunSight, game.input.activePointer) > 8) { 222 | // Make the object seek to the active pointer (mouse or touch). 223 | game.physics.arcade.moveToPointer(gunSight, 300, game.input.activePointer, 100); 224 | } 225 | else { 226 | // Otherwise turn off velocity because we're close enough to the pointer 227 | gunSight.body.velocity.set(0); 228 | } 229 | } 230 | 231 | function updateLine() { 232 | 233 | if (line.length < content[index].length) 234 | { 235 | line = content[index].substr(0, line.length + 1); 236 | // text.text = line; 237 | introText.setText(line); 238 | } 239 | else 240 | { 241 | // Wait 2 seconds then start a new line 242 | game.time.events.add(Phaser.Timer.SECOND * 1, displayIntroText, this); 243 | } 244 | 245 | } 246 | 247 | function displayIntroText() { 248 | 249 | index++; 250 | 251 | if (index < content.length) 252 | { 253 | line = ''; 254 | game.time.events.repeat(80, content[index].length + 1, updateLine, this); 255 | } else { 256 | introText.destroy(); 257 | startGameDisplayLoop(); 258 | } 259 | 260 | } 261 | 262 | function startGameDisplayLoop() { 263 | gameLoop = game.time.events.loop(Phaser.Timer.SECOND * 3, displayObject, this); 264 | } 265 | 266 | function stopGameDisplayLoop() { 267 | game.time.events.remove(gameLoop); 268 | } 269 | 270 | function render() { 271 | // If you are working / modifying this code base, 272 | // uncomment the following line to display helpful information 273 | // in the top left corner 274 | 275 | // game.debug.inputInfo(32, 32); 276 | } -------------------------------------------------------------------------------- /src/main/resources/static/css/popper.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) Federico Zivolo 2018 3 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). 4 | */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=getComputedStyle(e,null);return t?o[t]:o}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll|overlay)/.test(r+s+p)?e:n(o(e))}function r(e){return 11===e?re:10===e?pe:re||pe}function p(e){if(!e)return document.documentElement;for(var o=r(10)?document.body:null,n=e.offsetParent;n===o&&e.nextElementSibling;)n=(e=e.nextElementSibling).offsetParent;var i=n&&n.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TD','TABLE'].indexOf(n.nodeName)&&'static'===t(n,'position')?p(n):n:e?e.ownerDocument.documentElement:document.documentElement}function s(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||p(e.firstElementChild)===e)}function d(e){return null===e.parentNode?e:d(e.parentNode)}function a(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,n=o?e:t,i=o?t:e,r=document.createRange();r.setStart(n,0),r.setEnd(i,0);var l=r.commonAncestorContainer;if(e!==l&&t!==l||n.contains(i))return s(l)?l:p(l);var f=d(e);return f.host?a(f.host,t):a(e,d(t).host)}function l(e){var t=1=o.clientWidth&&n>=o.clientHeight}),l=0a[e]&&!t.escapeWithReference&&(n=J(f[o],a[e]-('right'===e?f.width:f.height))),ae({},o,n)}};return l.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';f=le({},f,m[t](e))}),e.offsets.popper=f,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,n=t.reference,i=e.placement.split('-')[0],r=Z,p=-1!==['top','bottom'].indexOf(i),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(n[s])&&(e.offsets.popper[d]=r(n[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var n;if(!q(e.instance.modifiers,'arrow','keepTogether'))return e;var i=o.element;if('string'==typeof i){if(i=e.instance.popper.querySelector(i),!i)return e;}else if(!e.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',c=a?'bottom':'right',u=S(i)[l];d[c]-us[c]&&(e.offsets.popper[m]+=d[m]+u-s[c]),e.offsets.popper=g(e.offsets.popper);var b=d[m]+d[l]/2-u/2,y=t(e.instance.popper),w=parseFloat(y['margin'+f],10),E=parseFloat(y['border'+f+'Width'],10),v=b-e.offsets.popper[m]-w-E;return v=$(J(s[l]-u,v),0),e.arrowElement=i,e.offsets.arrow=(n={},ae(n,m,Q(v)),ae(n,h,''),n),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=v(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),n=e.placement.split('-')[0],i=T(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case he.FLIP:p=[n,i];break;case he.CLOCKWISE:p=z(n);break;case he.COUNTERCLOCKWISE:p=z(n,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(n!==s||p.length===d+1)return e;n=e.placement.split('-')[0],i=T(n);var a=e.offsets.popper,l=e.offsets.reference,f=Z,m='left'===n&&f(a.right)>f(l.left)||'right'===n&&f(a.left)f(l.top)||'bottom'===n&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&g||'bottom'===n&&u,y=-1!==['top','bottom'].indexOf(n),w=!!t.flipVariations&&(y&&'start'===r&&h||y&&'end'===r&&c||!y&&'start'===r&&g||!y&&'end'===r&&u);(m||b||w)&&(e.flipped=!0,(m||b)&&(n=p[d+1]),w&&(r=G(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=le({},e.offsets.popper,C(e.instance.popper,e.offsets.reference,e.placement)),e=P(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport'},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],n=e.offsets,i=n.popper,r=n.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return i[p?'left':'top']=r[o]-(s?i[p?'width':'height']:0),e.placement=T(t),e.offsets.popper=g(i),e}},hide:{order:800,enabled:!0,fn:function(e){if(!q(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=D(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.right+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},R=function(){V()},M=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&z(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function X(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&M(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function U(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function z(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",R),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",le.option=!!xe.lastChild;var ke={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n",""]);var je=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function Me(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="
",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return R(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return R(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0