├── .gitignore ├── CONTRIBUTING.MD ├── LICENSE ├── Makefile ├── README.md ├── Sample.properties ├── apps ├── admin │ ├── containers │ │ ├── Dockerfile │ │ ├── kubernetes.go │ │ ├── kubernetes_test.go │ │ ├── main.go │ │ ├── main_test.go │ │ └── testdata │ │ │ ├── deployment_create.json │ │ │ ├── deployment_delete.json │ │ │ ├── empty.json │ │ │ ├── node_schedulable.json │ │ │ ├── node_unschedulable.json │ │ │ ├── nodes.json │ │ │ ├── pod.json │ │ │ ├── pods.json │ │ │ └── pods_onenode_only.json │ ├── docker │ │ └── Makefile │ └── kubernetes │ │ └── Makefile ├── api │ ├── containers │ │ ├── Dockerfile │ │ ├── main.go │ │ └── main_test.go │ ├── docker │ │ └── Makefile │ └── kubernetes │ │ └── Makefile ├── game │ ├── containers │ │ ├── Dockerfile │ │ ├── default.conf │ │ └── default │ │ │ ├── advanced.html │ │ │ ├── assets │ │ │ ├── audio │ │ │ │ ├── countdown.mp3 │ │ │ │ ├── explosion.wav │ │ │ │ ├── license.txt │ │ │ │ ├── pop.wav │ │ │ │ └── startup.mp3 │ │ │ ├── css │ │ │ │ ├── alert.png │ │ │ │ ├── background.png │ │ │ │ ├── background2.png │ │ │ │ ├── bomb_waiting2.png │ │ │ │ ├── cursor.png │ │ │ │ ├── cursor_down.png │ │ │ │ ├── cursor_inactive.png │ │ │ │ ├── cursor_next.png │ │ │ │ ├── cursor_next_down.png │ │ │ │ ├── cursor_next_inactive.png │ │ │ │ ├── cursordown.png │ │ │ │ ├── holder_background.png │ │ │ │ ├── license.txt │ │ │ │ ├── main.css │ │ │ │ ├── next.css │ │ │ │ ├── pending.png │ │ │ │ ├── pending_next.png │ │ │ │ ├── running.png │ │ │ │ ├── running_next.png │ │ │ │ ├── service_down.png │ │ │ │ ├── service_inactive.png │ │ │ │ ├── service_up.png │ │ │ │ ├── sign.png │ │ │ │ ├── terminating.png │ │ │ │ └── terminating_next.png │ │ │ ├── img │ │ │ │ ├── bomb_explode.png │ │ │ │ ├── bomb_explode_next.png │ │ │ │ ├── bomb_waiting.png │ │ │ │ ├── bomb_waiting_next.png │ │ │ │ └── license.txt │ │ │ └── js │ │ │ │ ├── advanced.js │ │ │ │ ├── lib.js │ │ │ │ ├── main.js │ │ │ │ └── next.js │ │ │ ├── healthz │ │ │ └── index.html │ │ │ ├── index.html │ │ │ └── next.html │ ├── docker │ │ └── Makefile │ └── kubernetes │ │ └── Makefile └── ingress │ ├── Makefile │ ├── ingress.generic.yaml │ ├── ingress.minikube.yaml │ └── ingress.sample.yaml ├── infrastructure └── Makefile └── screenshots ├── advanced.png ├── game.png └── next.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Makefile.properties 3 | Makefile.jp.properties 4 | Makefile.us.properties 5 | apps/game/kubernetes/game-deployment.yaml 6 | apps/game/containers/default/assets/js/config.js 7 | apps/admin/kubernetes/admin-deployment.yaml 8 | apps/api/kubernetes/api-deployment.yaml 9 | 10 | /Makefile.au.properties 11 | /Makefile.us.old.properties 12 | /Makefile.eu.properties 13 | /Makefile.sg.properties 14 | /apps/ingress/ingress.yaml 15 | /apps/admin/containers/main 16 | /apps/api/containers/main 17 | /Makefile.de.properties 18 | Makefile.uk.properties 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.MD: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Google Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | BASEDIR = $(shell pwd) 15 | 16 | include Makefile.properties 17 | 18 | deploy: env creds 19 | cd "$(BASEDIR)/apps/api/kubernetes/" && $(MAKE) deploy 20 | cd "$(BASEDIR)/apps/game/kubernetes/" && $(MAKE) deploy 21 | cd "$(BASEDIR)/apps/admin/kubernetes/" && $(MAKE) deploy 22 | cd "$(BASEDIR)/apps/ingress/" && $(MAKE) deploy 23 | 24 | reset.safe: env creds 25 | cd "$(BASEDIR)/apps/api/kubernetes/" && $(MAKE) reset.safe 26 | cd "$(BASEDIR)/apps/game/kubernetes/" && $(MAKE) reset.safe 27 | cd "$(BASEDIR)/apps/admin/kubernetes/" && $(MAKE) reset.safe 28 | 29 | deploy.minikube: creds.minikube 30 | cd "$(BASEDIR)/apps/api/kubernetes/" && $(MAKE) deploy.minikube 31 | cd "$(BASEDIR)/apps/game/kubernetes/" && $(MAKE) deploy.minikube 32 | cd "$(BASEDIR)/apps/admin/kubernetes/" && $(MAKE) deploy.minikube 33 | cd "$(BASEDIR)/apps/ingress/" && $(MAKE) deploy.minikube 34 | 35 | deploy.minikube.dockerhub: creds.minikube 36 | minikube addons enable ingress 37 | cd "$(BASEDIR)/apps/api/kubernetes/" && $(MAKE) deploy.minikube.dockerhub 38 | cd "$(BASEDIR)/apps/game/kubernetes/" && $(MAKE) deploy.minikube.dockerhub 39 | cd "$(BASEDIR)/apps/admin/kubernetes/" && $(MAKE) deploy.minikube.dockerhub 40 | cd "$(BASEDIR)/apps/ingress/" && $(MAKE) deploy.minikube 41 | @printf -- "*** DONE ***\n" 42 | @printf -- "add the following line to your /etc/hosts file:\n\n" 43 | @printf -- "$(shell minikube ip) minikube.wap\n\n" 44 | 45 | deploy.generic: 46 | cd "$(BASEDIR)/apps/api/kubernetes/" && $(MAKE) deploy.generic 47 | cd "$(BASEDIR)/apps/game/kubernetes/" && $(MAKE) deploy.generic 48 | cd "$(BASEDIR)/apps/admin/kubernetes/" && $(MAKE) deploy.generic 49 | cd "$(BASEDIR)/apps/ingress/" && $(MAKE) deploy.generic 50 | 51 | clean: env creds 52 | cd "$(BASEDIR)/apps/api/kubernetes/" && $(MAKE) clean 53 | cd "$(BASEDIR)/apps/game/kubernetes/" && $(MAKE) clean 54 | cd "$(BASEDIR)/apps/admin/kubernetes/" && $(MAKE) clean 55 | cd "$(BASEDIR)/apps/ingress/" && $(MAKE) clean 56 | 57 | clean.generic: 58 | cd "$(BASEDIR)/apps/api/kubernetes/" && $(MAKE) clean.generic 59 | cd "$(BASEDIR)/apps/game/kubernetes/" && $(MAKE) clean.generic 60 | cd "$(BASEDIR)/apps/admin/kubernetes/" && $(MAKE) clean.generic 61 | cd "$(BASEDIR)/apps/ingress/" && $(MAKE) clean.generic 62 | 63 | clean.minikube: 64 | cd "$(BASEDIR)/apps/api/kubernetes/" && $(MAKE) clean.minikube 65 | cd "$(BASEDIR)/apps/game/kubernetes/" && $(MAKE) clean.minikube 66 | cd "$(BASEDIR)/apps/admin/kubernetes/" && $(MAKE) clean.minikube 67 | cd "$(BASEDIR)/apps/ingress/" && $(MAKE) clean.minikube 68 | 69 | clean.minikube.dockerhub: 70 | cd "$(BASEDIR)/apps/api/kubernetes/" && $(MAKE) clean.minikube 71 | cd "$(BASEDIR)/apps/game/kubernetes/" && $(MAKE) clean.minikube 72 | cd "$(BASEDIR)/apps/admin/kubernetes/" && $(MAKE) clean.minikube.dockerhub 73 | cd "$(BASEDIR)/apps/ingress/" && $(MAKE) clean.minikube 74 | 75 | build: env creds 76 | cd "$(BASEDIR)/apps/api/kubernetes/" && $(MAKE) build 77 | cd "$(BASEDIR)/apps/game/kubernetes/" && $(MAKE) build 78 | cd "$(BASEDIR)/apps/admin/kubernetes/" && $(MAKE) build 79 | 80 | build.dockerhub: 81 | cd "$(BASEDIR)/apps/api/kubernetes/" && $(MAKE) build.dockerhub 82 | cd "$(BASEDIR)/apps/game/kubernetes/" && $(MAKE) build.dockerhub 83 | cd "$(BASEDIR)/apps/admin/kubernetes/" && $(MAKE) build.dockerhub 84 | 85 | build.generic: 86 | cd "$(BASEDIR)/apps/api/kubernetes/" && $(MAKE) build.generic 87 | cd "$(BASEDIR)/apps/game/kubernetes/" && $(MAKE) build.generic 88 | cd "$(BASEDIR)/apps/admin/kubernetes/" && $(MAKE) build.generic 89 | 90 | config: env creds 91 | @cd "$(BASEDIR)/apps/ingress/" && $(MAKE) config 92 | 93 | test: 94 | cd "$(BASEDIR)/apps/api/kubernetes/" && $(MAKE) test 95 | cd "$(BASEDIR)/apps/admin/kubernetes/" && $(MAKE) test 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Whack-a-pod 2 | This is a demo that can be used to show how resilient services running on 3 | Kubernetes can be. Main app shows a giant sign that flashes in various random 4 | colors. Those colors come a Kubernetes powered microservice. If the service 5 | goes down, the sign turns red. Your goal is to try and knock the service down 6 | by killing the Kubernetes pods that run the service. You can do that by 7 | whacking the pods wich are respresented as moles. 8 | 9 | ![Whack-a-pod Screenshot](screenshots/game.png "Screenshot") 10 | 11 | There is also a less busy verison of the game available at /next.html. This 12 | version has an advanced mode that allows someone to do a more visual 13 | explanation of the mechanics. 14 | 15 | ![Next Screenshot](screenshots/next.png "Next Version") 16 | 17 | The advanced version allows you to track the pod that is serving the color 18 | service and to simulate creating and destroying nodes. 19 | 20 | ![Advanced Screenshot](screenshots/advanced.png "Advanced Version") 21 | 22 | ## Getting Started 23 | 24 | The current directions assume you are using Google Cloud Platform to take 25 | advantage of Container Engine to build a manage your Kubernetes cluster. There 26 | is nothing preventing this app from *running* on a Kubernetes cluster hosted 27 | elsewhere, but the directions for setup assume Container Engine. If there is 28 | significant interest in these directions, I'll be happy to publish them (or 29 | better yet, accept a pull request.) 30 | 31 | ### Create and configure GCP project 32 | 1. Create Project in Cloud Console 33 | 1. Navigate to Compute Engine (to activate Compute Engine service) 34 | 1. Navigate to the API Library and activate Container Builder API 35 | 36 | 37 | ### Create Configs 38 | 1. Make a copy of `/Samples.properties`, renamed to `/Makefile.properties` 39 | 1. Alter value for `PROJECT` to your project id 40 | 1. Alter `ZONE` and `REGION` if you want to run this demo in a particular area. 41 | 1. Alter `CLUSTER` if you want to call your cluster something other than 42 | `whack-a-pod`. 43 | 1. Set `INGRESSNAME` if you need to use something other than the default. 44 | 1. Set `DOCKERREPO` if you need to use something other Google Container Registry. 45 | 1. Open a terminal in `/`. 46 | 1. Run `make config` to create your ingress file. 47 | 1. This should create the following file: 48 | 1. /apps/ingress/ingress.yaml 49 | 50 | 51 | >I use this application to show off Google Cloud Platform, so I tend set it up 52 | multiple times, once per region or datacenter. Therefore, I rename the `INGRESSNAME` and 53 | `CLUSTER` a bunch. If you only have one cluster, you don't have to fiddle with 54 | these. 55 | 56 | ### Build Infrastructure 57 | 1. Open a terminal in `/infrastructure/`. 58 | 1. Run `make build`. 59 | `make build` will do the following: 60 | 1. Create Kubernetes Cluster 61 | 1. Create 1 static ip addresse for use in the app 62 | 63 | >If you get the error `ResponseError: code=503, 64 | message=Project projectname is not fully initialized with the default service 65 | accounts. Please try again later.` You need to navigfate to Compute Engine in 66 | Google Cloud console to activate Compute Engine service. 67 | 68 | 69 | 70 | ### Build Application 71 | 1. Open a terminal in root of whack_a_pod location. 72 | 1. Run `make build` 73 | 1. Run `make deploy` 74 | 1. When process finishes Browse to the the IP address value for the ingress. 75 | 1. To get that address: `gcloud compute addresses describe #INGRESSNAME# --global` 76 | 77 | ## Run demo 78 | There are two skins to the game. 79 | 1. Carnival version: 80 | * http://[gamehost]/ 81 | 1. Google Cloud Next branded version: 82 | * http://[gamehost]/next.html 83 | * http://[gamehost]/advanced.html 84 | 85 | The advanced version of the game is a great demo for teaching some of the 86 | fundamentals of Kubernetes. It allows you to cordon and uncordon nodes of the 87 | Kubernetes cluster to simulate Node failure. In addition it shows which Pod of 88 | the Replica Set is actually answering calls for the service. 89 | 90 | ### Clean Up 91 | 1. Open a terminal in `/`. 92 | 1. Run `make clean` 93 | 1. Open a terminal in `/infrastructure/`. 94 | 1. Run `make clean` 95 | 96 | ## Minikube 97 | Whack a Pod can run on Minikube. Its performance isn't stellar, but the game 98 | versions of it run just as well a it does on a flaky conference wifi. 99 | 100 | ### Prerequisite 101 | * Install minikube 102 | [Directions](https://github.com/kubernetes/minikube/releases) 103 | * Enable ingress 104 | `minikube addons enable ingress` 105 | * Install xhyve driver (Mac OS]) 106 | [Directions](https://github.com/kubernetes/minikube/blob/master/docs/drivers.md) 107 | 108 | ### Docker Repository 109 | You can use the Container Registry based commands in the Makefiles to build and 110 | host your Docker images. 111 | 112 | 1. Open a terminal in root of whack_a_pod location. 113 | 1. Run `make build` 114 | 1. Make images publicly available by following [these directions](https://cloud.google.com/container-registry/docs/access-control) 115 | 116 | This still requires a Google Cloud Platform Project. If you would like to build 117 | them some other way, you can, nothing restricts you from doing so. Just make 118 | sure you set `$(DOCKERREPO)` to the right value in Makefile.properties. 119 | 120 | #### Docker Repository with DockerHub 121 | Alternatively, to host your images on DockerHub, 122 | 123 | 1. Create an account on DockerHub 124 | 2. Create an image repository on DockerHub 125 | 3. In the `Makefile.properties`, change the value of `PROJECT` so that it equals your image repository slug 126 | 4. In the `Makefile.properties`, change the value of `DOCKERREPO` so that it looks like `yourusername/$(PROJECT)` 127 | 5. Run `make build.dockerhub` 128 | 129 | Your repository should have three new images with the tags: 130 | - admin 131 | - api 132 | - game 133 | 134 | For example, if your username is `username` and your repository name is `wap`, your images will be pushed to: 135 | - username/wap:admin 136 | - username/wap:api 137 | - username/wap:game 138 | 139 | ### Running on Minikube 140 | 141 | 1. Open a terminal in root of whack_a_pod location. 142 | 1. Run `minikube start --vm-driver=xhyve` 143 | 1. Run `make deploy.minikube` 144 | 1. Run `minikube ip` to get the IP address of the Minikube deployment. 145 | 1. Create an entry in /etc/hosts pointing IP address to `minikube.wap`. 146 | 147 | ### Running on Minikube with DockerHub 148 | 1. Open a terminal in root of whack_a_pod location. 149 | 1. Run `minikube start` 150 | 1. Run `make deploy.minikube.dockerhub` 151 | 1. The last line of the previous command should contain an IP address beside a `"minikube.wap"` string 152 | 1. Run `sudo vim /etc/hosts`, append the last line to the `/etc/hosts` and save the file. 153 | 1. Wait for the ingress to obtain an IP address (run `kubectl get ing --watch` and wait till the `ADDRESS` column has a value) 154 | 1. Visit [http://minikube.wap](http://minikube.wap) in the browser 155 | 156 | ### Clean Minikube 157 | 1. Run `make clean.minikube` 158 | 1. Run `minikube stop` 159 | 160 | ### Running on any Kubernetes (generic) 161 | 162 | This method is for generic usage and can be run on any Kubernetes installation. There are few differences: 163 | * It will push built images to repo `DOCKERREPO` defined in Makefile.properties 164 | * It's agnostic of any loadbalancer in front of ingress so you can use `NodePort` type for ingress service 165 | * It works with RBAC model - proper serviceaccount and role bindings are created 166 | * It will deploy all objects in current namespace 167 | 168 | 1. Open a terminal in root of whack_a_pod location. 169 | 1. Build application with `make build.generic` **OR** skip building by setting `DOCKERREPO` to **cloudowski** and use prebuilt images availabe on dockerhub 170 | 1. Run `make deploy.generic` 171 | 1. Define name `whackapod.example.com` in your `/etc/hosts` pointing to IP address of your load balancer in front of ingress controller or one of nodes IP (when using `NodePort`) 172 | 1. Open your browser at [http://whackapod.example.com/](http://whackapod.example.com) 173 | 174 | ### Clean generic deployment 175 | 1. Run `make clean.generic` 176 | 177 | ## Architecture 178 | There are three Kubernetes services that make up the whole application: 179 | 1. Game 180 | Game contains all of the front end clients for the game, both the carnival 181 | version and the Google Cloud Next version. 182 | 1. Admin 183 | Admin contains all of the logic for managing the whole application. This is 184 | the application the front end calls to get a list of the pods running the 185 | color api, it also has calls to create and delete deployments, delete pods, and 186 | drain and uncordon nodes. 187 | 1. Api 188 | Api contains two service calls: color and color-complete. Color is a random 189 | hexidecimal RGB color value. Color-complete is the same as color, but also 190 | sends the pod name of the pod that answered the service call. 191 | 192 | 193 | "This is not an official Google Project." 194 | -------------------------------------------------------------------------------- /Sample.properties: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | PROJECT=[your project] 15 | ZONE=us-central1-c 16 | REGION=us-central1 17 | CLUSTER=whack-a-pod 18 | INGRESSNAME=whack-a-pod-ingress 19 | DOCKERREPO=gcr.io/$(PROJECT) 20 | 21 | creds: 22 | gcloud container clusters get-credentials $(CLUSTER) 23 | 24 | env: 25 | gcloud config set project $(PROJECT) 26 | gcloud config set compute/zone $(ZONE) 27 | 28 | creds.minikube: 29 | kubectl config use-context minikube -------------------------------------------------------------------------------- /apps/admin/containers/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All Rights Reserved. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | FROM scratch 15 | ADD main / 16 | CMD ["/main"] -------------------------------------------------------------------------------- /apps/admin/containers/kubernetes.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | package main 12 | 13 | import ( 14 | "bytes" 15 | "encoding/json" 16 | "fmt" 17 | "io/ioutil" 18 | "net/http" 19 | "os" 20 | "strconv" 21 | ) 22 | 23 | type httpClient interface { 24 | Do(req *http.Request) (*http.Response, error) 25 | } 26 | 27 | func queryK8sAPI(url, method string, data []byte) ([]byte, int, error) { 28 | req, err := http.NewRequest(method, url, bytes.NewBuffer(data)) 29 | if err != nil { 30 | return nil, http.StatusInternalServerError, fmt.Errorf("could not create request for HTTP %s %s: %v", method, url, err) 31 | } 32 | // This is required for k8s api calls. 33 | req.Header.Add("Authorization", "Bearer "+token) 34 | 35 | if method == http.MethodPost { 36 | req.Header.Set("Content-Type", "application/json") 37 | req.Header.Set("Content-Length", strconv.Itoa(len(string(data)))) 38 | } 39 | 40 | if method == http.MethodPatch { 41 | req.Header.Set("Content-Type", "application/merge-patch+json") 42 | req.Header.Set("Content-Length", strconv.Itoa(len(string(data)))) 43 | } 44 | 45 | resp, err := client.Do(req) 46 | if err != nil { 47 | return nil, http.StatusInternalServerError, fmt.Errorf("could not execute HTTP request for HTTP %s %s: %v", method, url, err) 48 | } 49 | defer resp.Body.Close() 50 | 51 | b, err := ioutil.ReadAll(resp.Body) 52 | if err != nil { 53 | return nil, http.StatusInternalServerError, fmt.Errorf("could not read HTTP request for HTTP %s %s: %v", method, url, err) 54 | } 55 | return b, resp.StatusCode, nil 56 | } 57 | 58 | func listPods() ([]byte, error) { 59 | url := root + "/api/v1/pods?labelSelector=" + selector 60 | 61 | b, _, err := queryK8sAPI(url, "GET", nil) 62 | if err != nil { 63 | return nil, fmt.Errorf("can't list pods: %v", err) 64 | } 65 | 66 | return b, nil 67 | } 68 | 69 | func deletePod(podname string) ([]byte, error) { 70 | url := root + podname 71 | 72 | b, status, err := queryK8sAPI(url, "DELETE", nil) 73 | if err != nil { 74 | return nil, fmt.Errorf("can't delete pod: %v", err) 75 | } 76 | 77 | if status == http.StatusNotFound { 78 | return nil, errItemNotExist 79 | } 80 | 81 | return b, nil 82 | 83 | } 84 | 85 | func deletePods(node string) ([]byte, error) { 86 | url := root + "/api/v1/namespaces/default/pods" + "?labelSelector=" + selector 87 | if len(node) > 0 { 88 | fs := "&fieldSelector=spec.nodeName=" + node 89 | url += fs 90 | } 91 | 92 | b, status, err := queryK8sAPI(url, "DELETE", nil) 93 | if err != nil { 94 | return nil, fmt.Errorf("can't delete pods: %v", err) 95 | } 96 | 97 | if status == http.StatusNotFound { 98 | return nil, errItemNotExist 99 | } 100 | 101 | return b, nil 102 | 103 | } 104 | 105 | func describePod(podname string) ([]byte, error) { 106 | url := root + podname 107 | 108 | b, _, err := queryK8sAPI(url, "GET", nil) 109 | if err != nil { 110 | return nil, fmt.Errorf("can't describe pod: %v", err) 111 | } 112 | 113 | return b, nil 114 | 115 | } 116 | 117 | func listNodes() ([]byte, error) { 118 | url := root + "/api/v1/nodes" 119 | 120 | b, _, err := queryK8sAPI(url, "GET", nil) 121 | if err != nil { 122 | return nil, fmt.Errorf("can't list nodes: %v", err) 123 | } 124 | 125 | return b, nil 126 | } 127 | 128 | func toggleNode(nodename string, inactive bool) ([]byte, error) { 129 | url := root + "/api/v1/nodes/" + nodename 130 | 131 | j := fmt.Sprintf("{\"spec\": {\"unschedulable\": %t}}", inactive) 132 | b, status, err := queryK8sAPI(url, "PATCH", []byte(j)) 133 | if err != nil { 134 | return nil, fmt.Errorf("can't toggle node: %s inactive: %t %v", nodename, inactive, err) 135 | } 136 | 137 | if status == http.StatusNotFound { 138 | return nil, errItemNotExist 139 | } 140 | 141 | return b, nil 142 | } 143 | 144 | func deleteReplicaSet() ([]byte, error) { 145 | url := root + "/apis/extensions/v1beta1/namespaces/default/replicasets" + "?labelSelector=" + selector 146 | 147 | b, status, err := queryK8sAPI(url, "DELETE", nil) 148 | if err != nil { 149 | return nil, fmt.Errorf("can't delete replica set: %v", err) 150 | } 151 | 152 | if status == http.StatusNotFound { 153 | 154 | return nil, errItemNotExist 155 | } 156 | 157 | return b, nil 158 | 159 | } 160 | 161 | type minimumDeployment struct { 162 | APIVersion string `json:"apiVersion,omitempty"` 163 | Kind string `json:"kind,omitempty"` 164 | Metadata struct { 165 | Name string `json:"name,omitempty"` 166 | } `json:"metadata,omitempty"` 167 | Spec struct { 168 | Replicas int `json:"replicas,omitempty"` 169 | Selector struct { 170 | MatchLabels map[string]string `json:"matchLabels,omitempty"` 171 | } `json:"selector,omitempty"` 172 | Strategy struct { 173 | Type string `json:"type,omitempty"` 174 | } `json:"strategy,omitempty"` 175 | Template struct { 176 | Metadata struct { 177 | Labels map[string]string `json:"labels,omitempty"` 178 | } `json:"metadata,omitempty"` 179 | Spec struct { 180 | TerminationGracePeriodSeconds int `json:"terminationGracePeriodSeconds,omitempty"` 181 | Containers []minimumContainer `json:"containers,omitempty"` 182 | } `json:"spec,omitempty"` 183 | } `json:"template,omitempty"` 184 | } `json:"spec,omitempty"` 185 | } 186 | 187 | type minimumContainer struct { 188 | Image string `json:"image,omitempty"` 189 | ImagePullPolicy string `json:"imagePullPolicy,omitempty"` 190 | Name string `json:"name,omitempty"` 191 | Ports []minimumPort `json:"ports,omitempty"` 192 | } 193 | 194 | type minimumPort struct { 195 | ContainerPort int `json:"containerPort,omitempty"` 196 | Name string `json:"name,omitempty"` 197 | Protocol string `json:"protocol,omitempty"` 198 | } 199 | 200 | func createDeployment() ([]byte, error) { 201 | selflink := "/apis/extensions/v1beta1/namespaces/default/deployments" 202 | url := root + selflink 203 | 204 | image := os.Getenv("APIIMAGE") 205 | if len(image) == 0 { 206 | return nil, fmt.Errorf("env var APIIMAGE not set") 207 | } 208 | 209 | policy := os.Getenv("APIPULLPOLICY") 210 | if len(policy) == 0 { 211 | policy = "IfNotPresent" 212 | } 213 | 214 | var d minimumDeployment 215 | d.APIVersion = "extensions/v1beta1" 216 | d.Kind = "Deployment" 217 | d.Metadata.Name = "api-deployment" 218 | d.Spec.Replicas = 12 219 | d.Spec.Selector.MatchLabels = map[string]string{"app": "api"} 220 | d.Spec.Strategy.Type = "RollingUpdate" 221 | d.Spec.Template.Metadata.Labels = map[string]string{"app": "api"} 222 | d.Spec.Template.Spec.TerminationGracePeriodSeconds = 1 223 | d.Spec.Template.Spec.Containers = []minimumContainer{ 224 | minimumContainer{ 225 | Name: "api", 226 | Image: image, 227 | ImagePullPolicy: policy, 228 | 229 | Ports: []minimumPort{ 230 | minimumPort{ 231 | ContainerPort: 8080, 232 | Name: "http", 233 | Protocol: "TCP", 234 | }, 235 | }, 236 | }, 237 | } 238 | 239 | dbytes, err := json.Marshal(d) 240 | if err != nil { 241 | return nil, fmt.Errorf("could not convert deployment to json: %v", err) 242 | } 243 | 244 | b, status, err := queryK8sAPI(url, "POST", dbytes) 245 | if err != nil { 246 | return nil, fmt.Errorf("can't created deployment: %v", err) 247 | } 248 | 249 | if status == http.StatusNotFound { 250 | return nil, errItemNotExist 251 | } 252 | 253 | if status == http.StatusConflict { 254 | return nil, errItemAlreadyExist 255 | } 256 | 257 | return b, nil 258 | 259 | } 260 | 261 | func deleteDeployment(depname string) ([]byte, error) { 262 | selflink := "/apis/extensions/v1beta1/namespaces/default/deployments/" + depname 263 | url := root + selflink 264 | 265 | b, status, err := queryK8sAPI(url, "DELETE", nil) 266 | if err != nil { 267 | return nil, fmt.Errorf("can't delete deployment: %v", err) 268 | } 269 | 270 | if status == http.StatusNotFound { 271 | return nil, errItemNotExist 272 | } 273 | 274 | return b, nil 275 | 276 | } 277 | -------------------------------------------------------------------------------- /apps/admin/containers/kubernetes_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | package main 12 | 13 | import ( 14 | "bufio" 15 | "encoding/json" 16 | "fmt" 17 | "io/ioutil" 18 | "net/http" 19 | "os" 20 | "strings" 21 | "testing" 22 | 23 | "k8s.io/api/apps/v1beta1" 24 | "k8s.io/api/core/v1" 25 | ) 26 | 27 | var ( 28 | deploymentPath = "/apis/extensions/v1beta1/namespaces/default/deployments" 29 | deploymentName = "api-deployment" 30 | nodePath = "/api/v1/nodes" 31 | nodeName = "gke-whack-a-pod-default-pool-8deaa3a5-b9p7" 32 | podDeletePath = "/api/v1/namespaces/default/pods" 33 | podExistsName = "api-deployment-1435701907-xx9lm" 34 | podExistsSelfLink = "/api/v1/namespaces/default/pods/api-deployment-1435701907-xx9lm" 35 | podListPath = "/api/v1/pods" 36 | ) 37 | 38 | func TestCreateDeploymentNoEnv(t *testing.T) { 39 | client = new(MockClient) 40 | _, err := createDeployment() 41 | if err == nil { 42 | t.Errorf("Expected error due to no ENV variable") 43 | } 44 | } 45 | 46 | func TestCreateDeployment(t *testing.T) { 47 | client = new(MockClient) 48 | os.Setenv("APIIMAGE", "gcr.io/carnivaldemos/api") 49 | b, err := createDeployment() 50 | if err != nil { 51 | t.Errorf("Error getting deployment : %v", err) 52 | } 53 | var dep v1beta1.Deployment 54 | if err := json.Unmarshal(b, &dep); err != nil { 55 | t.Errorf("error turning response to deployment: %v", err) 56 | t.FailNow() 57 | } 58 | 59 | if dep.Name != "api-deployment" { 60 | t.Errorf("createDeployment().Name got: %s, wanted: %s", dep.Name, "api-deplyment") 61 | } 62 | os.Unsetenv("APIIMAGE") 63 | } 64 | 65 | func TestDeleteDeployment(t *testing.T) { 66 | client = new(MockClient) 67 | _, err := deleteDeployment("api-deployment") 68 | if err != nil { 69 | t.Errorf("Error getting deployment : %v", err) 70 | } 71 | } 72 | 73 | func TestToggleNode(t *testing.T) { 74 | 75 | cases := []struct { 76 | node string 77 | inactive bool 78 | }{ 79 | {"gke-whack-a-pod-default-pool-8deaa3a5-b9p7", true}, 80 | {"gke-whack-a-pod-default-pool-8deaa3a5-b9p7", false}, 81 | } 82 | 83 | client = new(MockClient) 84 | 85 | for _, c := range cases { 86 | b, err := toggleNode(c.node, c.inactive) 87 | if err != nil { 88 | t.Errorf("Error getting deployment : %v", err) 89 | } 90 | 91 | var node v1.Node 92 | if err := json.Unmarshal(b, &node); err != nil { 93 | t.Errorf("error turning response to node: %v", err) 94 | t.FailNow() 95 | } 96 | 97 | if node.Spec.Unschedulable != c.inactive { 98 | t.Errorf("toggleNode().Spec.Unschedulable got %t wanted %t", node.Spec.Unschedulable, c.inactive) 99 | } 100 | 101 | } 102 | 103 | } 104 | 105 | func TestListNodes(t *testing.T) { 106 | 107 | client = new(MockClient) 108 | 109 | b, err := listNodes() 110 | if err != nil { 111 | t.Errorf("Error getting deployment : %v", err) 112 | } 113 | 114 | var nodes v1.NodeList 115 | if err := json.Unmarshal(b, &nodes); err != nil { 116 | t.Errorf("error turning response to nodelist: %v", err) 117 | t.FailNow() 118 | } 119 | 120 | if len(nodes.Items) != 2 { 121 | t.Errorf("Wanted 2 nodes got : %d", len(nodes.Items)) 122 | } 123 | 124 | } 125 | 126 | func TestListPods(t *testing.T) { 127 | 128 | client = new(MockClient) 129 | 130 | b, err := listPods() 131 | if err != nil { 132 | t.Errorf("Error getting deployment : %v", err) 133 | } 134 | 135 | var pods v1.PodList 136 | if err := json.Unmarshal(b, &pods); err != nil { 137 | t.Errorf("error turning response to podlist: %v", err) 138 | t.FailNow() 139 | } 140 | 141 | if len(pods.Items) != 12 { 142 | t.Errorf("listPods(): got %d wanted : %d", len(pods.Items), 12) 143 | } 144 | 145 | } 146 | 147 | func TestDeletePods(t *testing.T) { 148 | 149 | client = new(MockClient) 150 | 151 | b, err := deletePods("") 152 | if err != nil { 153 | t.Errorf("Error getting deployment : %v", err) 154 | } 155 | 156 | var pods v1.PodList 157 | if err := json.Unmarshal(b, &pods); err != nil { 158 | t.Errorf("error turning response to podlist: %v", err) 159 | t.FailNow() 160 | } 161 | 162 | if len(pods.Items) != 12 { 163 | t.Errorf("Wanted 12 pods got : %d", len(pods.Items)) 164 | } 165 | 166 | } 167 | 168 | func TestDescribePod(t *testing.T) { 169 | 170 | client = new(MockClient) 171 | self := "/api/v1/namespaces/default/pods/api-deployment-1435701907-xx9lm" 172 | 173 | b, err := describePod(self) 174 | if err != nil { 175 | t.Errorf("Error getting pod : %v", err) 176 | } 177 | 178 | var pod v1.Pod 179 | if err := json.Unmarshal(b, &pod); err != nil { 180 | t.Errorf("error turning response to podlist: %v", err) 181 | t.FailNow() 182 | } 183 | 184 | if pod.ObjectMeta.SelfLink != self { 185 | t.Errorf("Selflink should be %s got : %s", pod.ObjectMeta.SelfLink, self) 186 | } 187 | 188 | } 189 | 190 | func TestDeletePod(t *testing.T) { 191 | 192 | client = new(MockClient) 193 | self := "/api/v1/namespaces/default/pods/api-deployment-1435701907-xx9lm" 194 | 195 | b, err := deletePod(self) 196 | if err != nil { 197 | t.Errorf("Unexpected error getting pod, %v", err) 198 | } 199 | 200 | var pod v1.Pod 201 | if err := json.Unmarshal(b, &pod); err != nil { 202 | t.Errorf("error turning response to podlist: %v", err) 203 | t.FailNow() 204 | } 205 | 206 | if pod.ObjectMeta.SelfLink != self { 207 | t.Errorf("Selflink should be %s got : %s", pod.ObjectMeta.SelfLink, self) 208 | } 209 | 210 | } 211 | 212 | func TestDeletePodDoesNotExist(t *testing.T) { 213 | client = new(MockClient) 214 | self := "/api/v1/namespaces/default/pods/api-deployment-1435701907-FALSE" 215 | 216 | _, err := deletePod(self) 217 | if err != errItemNotExist { 218 | t.Errorf("Unexpected error gerring pod, got %v wanted: %v", err, errItemNotExist) 219 | } 220 | 221 | } 222 | 223 | type MockClient struct { 224 | DoFunc func(req *http.Request) (*http.Response, error) 225 | } 226 | 227 | func (m *MockClient) Do(req *http.Request) (*http.Response, error) { 228 | 229 | r := http.Response{} 230 | r.StatusCode = http.StatusOK 231 | r.Request = req 232 | filename := "" 233 | 234 | switch { 235 | 236 | case req.URL.Path == deploymentPath: 237 | filename = "testdata/deployment_create.json" 238 | 239 | case req.URL.Path == deploymentPath+"/"+deploymentName: 240 | filename = "testdata/deployment_delete.json" 241 | 242 | case req.URL.Path == nodePath: 243 | filename = "testdata/nodes.json" 244 | 245 | case req.URL.Path == podDeletePath+"/"+podExistsName: 246 | filename = "testdata/pod.json" 247 | 248 | case req.URL.Path == podListPath && req.FormValue("labelSelector") == selector: 249 | filename = "testdata/pods.json" 250 | 251 | case req.URL.Path == podDeletePath && req.FormValue("labelSelector") == selector && req.FormValue("fieldSelector") == "spec.nodeName="+nodeName: 252 | filename = "testdata/pods_onenode_only.json" 253 | 254 | case req.URL.Path == podDeletePath && req.FormValue("labelSelector") == selector: 255 | filename = "testdata/pods.json" 256 | 257 | case req.URL.Path == nodePath+"/"+nodeName: 258 | b, err := ioutil.ReadAll(req.Body) 259 | if err != nil { 260 | return nil, fmt.Errorf("could not read request data : %v", err) 261 | } 262 | 263 | filename = "testdata/node_schedulable.json" 264 | if strings.Index(string(b), "true") >= 0 { 265 | filename = "testdata/node_unschedulable.json" 266 | } 267 | default: 268 | filename = "testdata/empty.json" 269 | r.StatusCode = http.StatusNotFound 270 | } 271 | 272 | f, err := os.Open(filename) 273 | if err != nil { 274 | return nil, fmt.Errorf("could not get cached file (%s) for test: %v", filename, err) 275 | } 276 | reader := bufio.NewReader(f) 277 | r.Body = ioutil.NopCloser(reader) 278 | 279 | return &r, nil 280 | 281 | } 282 | -------------------------------------------------------------------------------- /apps/admin/containers/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | // Package main is a Kubernetes API proxy. It exposes a smaller surface of the 12 | // API and limits operations to specifically selected labels, and deployments 13 | package main 14 | 15 | import ( 16 | "crypto/tls" 17 | "crypto/x509" 18 | "fmt" 19 | "io/ioutil" 20 | "log" 21 | "net/http" 22 | "strings" 23 | "time" 24 | 25 | "github.com/gorilla/mux" 26 | ) 27 | 28 | var ( 29 | client httpClient 30 | pool *x509.CertPool 31 | token = "" 32 | errItemNotExist = fmt.Errorf("Item does not exist") 33 | errItemAlreadyExist = fmt.Errorf("Item already exists") 34 | ) 35 | 36 | const ( 37 | root = "https://kubernetes" 38 | selector = "app=api" 39 | defaultTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" 40 | defaultCertPath = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" 41 | ) 42 | 43 | func main() { 44 | log.Printf("starting whack a pod admin api") 45 | var err error 46 | 47 | b, err := ioutil.ReadFile(defaultTokenPath) 48 | if err != nil { 49 | log.Printf("could not get token from file system: %v", err) 50 | } 51 | token = string(b) 52 | 53 | certs, err := ioutil.ReadFile(defaultCertPath) 54 | if err != nil { 55 | log.Printf("could not get token from file system: %v", err) 56 | } 57 | 58 | // This allows me to use a scratch Dockerfile as described here : 59 | // https://medium.com/@kelseyhightower/optimizing-docker-images-for-static-binaries-b5696e26eb07 60 | // But instead of using the Authoritative Certs from a linux install, I'm 61 | // using the certs mounted from the Kuberntes server itself. Since all 62 | // this client does is talk to the Kubernetes server, this should always be 63 | // up to date. 64 | pool = x509.NewCertPool() 65 | pool.AppendCertsFromPEM(certs) 66 | client = &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{RootCAs: pool}}} 67 | 68 | router := mux.NewRouter() 69 | for _, r := range routes { 70 | router.Methods(r.method).Path(r.pattern).Handler(r.handlerFunc) 71 | // Couldn't get a regex working here and wrote this to make it stop 72 | // eating hours of my life. 73 | if strings.Index(r.pattern, "/k8s/") == 0 { 74 | router.Methods(r.method).Path("/admin" + r.pattern).Handler(r.handlerFunc) 75 | } 76 | } 77 | 78 | srv := &http.Server{ 79 | ReadTimeout: 5 * time.Second, 80 | WriteTimeout: 10 * time.Second, 81 | Addr: ":8080", 82 | Handler: router, 83 | } 84 | 85 | srv.ListenAndServe() 86 | 87 | } 88 | 89 | // Route represets a gorrila route from a http call to an application function 90 | type route struct { 91 | method string 92 | pattern string 93 | handlerFunc http.HandlerFunc 94 | } 95 | 96 | var routes = []route{ 97 | {"GET", "/", health}, 98 | {"GET", "/healthz", health}, 99 | {"GET", "/k8s/pods/get", handleAPI(handlePods)}, 100 | {"GET", "/k8s/nodes/get", handleAPI(handleNodes)}, 101 | {"GET", "/k8s/pod/delete", handleAPI(handlePodDelete)}, 102 | {"GET", "/k8s/pods/delete", handleAPI(handlePodsDelete)}, 103 | {"GET", "/k8s/node/drain", handleAPI(handleNodeDrain)}, 104 | {"GET", "/k8s/node/uncordon", handleAPI(handleNodeUncordon)}, 105 | {"GET", "/k8s/deployment/delete", handleAPI(handleDeploymentDelete)}, 106 | {"GET", "/k8s/deployment/create", handleAPI(handleDeploymentCreate)}, 107 | } 108 | 109 | func health(w http.ResponseWriter, r *http.Request) { 110 | r.Close = true 111 | w.WriteHeader(http.StatusOK) 112 | fmt.Fprint(w, "ok") 113 | } 114 | 115 | type apiHandler func(http.ResponseWriter, *http.Request) ([]byte, error) 116 | 117 | func handleAPI(h apiHandler) http.HandlerFunc { 118 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 119 | h.ServeHTTP(w, r) 120 | }) 121 | } 122 | 123 | func (h apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 124 | r.Close = true 125 | w.Header().Add("Access-Control-Allow-Origin", "*") 126 | b, err := h(w, r) 127 | status := http.StatusOK 128 | if err != nil { 129 | status = http.StatusInternalServerError 130 | if err == errItemNotExist { 131 | status = http.StatusAccepted 132 | } 133 | 134 | if err == errItemAlreadyExist { 135 | status = http.StatusAccepted 136 | } 137 | 138 | sendJSON(w, fmt.Sprintf("{\"error\":\"%v\"}", err), status) 139 | log.Printf("%s %d %s", r.Method, status, r.URL) 140 | log.Printf("Error %v", err) 141 | return 142 | } 143 | sendJSON(w, string(b), status) 144 | log.Printf("%s %d %s", r.Method, status, r.URL) 145 | } 146 | 147 | func handlePods(w http.ResponseWriter, r *http.Request) ([]byte, error) { 148 | return listPods() 149 | } 150 | 151 | func handlePodDelete(w http.ResponseWriter, r *http.Request) ([]byte, error) { 152 | b, err := deletePod(r.FormValue("pod")) 153 | if err != nil { 154 | if err == errItemNotExist { 155 | return nil, errItemNotExist 156 | } 157 | 158 | return nil, fmt.Errorf("could not delete k8s object: %v", err) 159 | } 160 | 161 | return b, nil 162 | } 163 | 164 | func handlePodsDelete(w http.ResponseWriter, r *http.Request) ([]byte, error) { 165 | 166 | b, err := deletePods("") 167 | if err != nil && err != errItemNotExist { 168 | return nil, fmt.Errorf("could not delete k8s pods: %v", err) 169 | } 170 | return b, nil 171 | } 172 | 173 | func handleDeploymentCreate(w http.ResponseWriter, r *http.Request) ([]byte, error) { 174 | return createDeployment() 175 | } 176 | 177 | func handleDeploymentDelete(w http.ResponseWriter, r *http.Request) ([]byte, error) { 178 | 179 | _, err := deleteDeployment("api-deployment") 180 | if err != nil && err != errItemNotExist { 181 | return nil, fmt.Errorf("could not delete k8s deployment: %v", err) 182 | } 183 | 184 | _, err = deleteReplicaSet() 185 | if err != nil && err != errItemNotExist { 186 | return nil, fmt.Errorf("could not delete k8s replica set: %v", err) 187 | } 188 | 189 | return handlePodsDelete(w, r) 190 | } 191 | 192 | func handleNodes(w http.ResponseWriter, r *http.Request) ([]byte, error) { 193 | return listNodes() 194 | } 195 | 196 | func handleNodeDrain(w http.ResponseWriter, r *http.Request) ([]byte, error) { 197 | nodename := r.FormValue("node") 198 | 199 | b, err := toggleNode(nodename, true) 200 | if err != nil && err != errItemNotExist { 201 | return nil, fmt.Errorf("could not retrieve k8s node info: %v", err) 202 | } 203 | 204 | _, err = deletePods(nodename) 205 | if err != nil { 206 | return nil, fmt.Errorf("could not remove all pods on node: %v", err) 207 | } 208 | 209 | return b, nil 210 | } 211 | 212 | func handleNodeUncordon(w http.ResponseWriter, r *http.Request) ([]byte, error) { 213 | 214 | b, err := toggleNode(r.FormValue("node"), false) 215 | if err != nil && err != errItemNotExist { 216 | return nil, fmt.Errorf("could uncordon node : %v", err) 217 | } 218 | 219 | return b, nil 220 | } 221 | 222 | func sendJSON(w http.ResponseWriter, content string, status int) { 223 | w.Header().Set("Content-Type", "application/json") 224 | w.WriteHeader(status) 225 | fmt.Fprint(w, content) 226 | } 227 | -------------------------------------------------------------------------------- /apps/admin/containers/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | package main 12 | 13 | import ( 14 | "encoding/json" 15 | "io/ioutil" 16 | "log" 17 | "net/http" 18 | "net/http/httptest" 19 | "os" 20 | "testing" 21 | 22 | "k8s.io/api/apps/v1beta2" 23 | "k8s.io/api/core/v1" 24 | ) 25 | 26 | func TestHealth(t *testing.T) { 27 | log.SetOutput(ioutil.Discard) 28 | req, err := http.NewRequest("GET", "/healthz", nil) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | rr := httptest.NewRecorder() 34 | handler := http.HandlerFunc(health) 35 | handler.ServeHTTP(rr, req) 36 | 37 | if status := rr.Code; status != http.StatusOK { 38 | t.Errorf("wrong status code: got %v want %v", status, http.StatusOK) 39 | } 40 | 41 | expected := `ok` 42 | if rr.Body.String() != expected { 43 | t.Errorf("unexpected body: got %v want %v", rr.Body.String(), expected) 44 | } 45 | } 46 | 47 | func TestHandlePods(t *testing.T) { 48 | log.SetOutput(ioutil.Discard) 49 | client = new(MockClient) 50 | req, err := http.NewRequest("GET", "/admin/k8s/pods/get", nil) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | rr := httptest.NewRecorder() 56 | handler := http.HandlerFunc(handleAPI(handlePods)) 57 | handler.ServeHTTP(rr, req) 58 | 59 | if status := rr.Code; status != http.StatusOK { 60 | t.Errorf("wrong status code: got %v want %v", status, http.StatusOK) 61 | } 62 | 63 | var podlist v1.PodList 64 | if err := json.Unmarshal([]byte(rr.Body.String()), &podlist); err != nil { 65 | t.Errorf("error turning response to podlist: %v", err) 66 | t.FailNow() 67 | } 68 | 69 | if len(podlist.Items) < 12 { 70 | t.Errorf("podlist.Items: got %d want %d", len(podlist.Items), 12) 71 | } 72 | 73 | } 74 | 75 | func TestHandlePodDeleteExisting(t *testing.T) { 76 | log.SetOutput(ioutil.Discard) 77 | client = new(MockClient) 78 | req, err := http.NewRequest("GET", "/admin/k8s/pod/delete?pod="+podExistsSelfLink, nil) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | 83 | rr := httptest.NewRecorder() 84 | handler := http.HandlerFunc(handleAPI(handlePodDelete)) 85 | handler.ServeHTTP(rr, req) 86 | 87 | if status := rr.Code; status != http.StatusOK { 88 | t.Errorf("wrong status code: got %v want %v", status, http.StatusOK) 89 | } 90 | 91 | var pod v1.Pod 92 | if err := json.Unmarshal([]byte(rr.Body.String()), &pod); err != nil { 93 | t.Errorf("error turning response to pod: %v", err) 94 | t.FailNow() 95 | } 96 | 97 | if pod.SelfLink != podExistsSelfLink { 98 | t.Errorf("podlist.SelfLink: got %s want %s", pod.SelfLink, podExistsSelfLink) 99 | } 100 | 101 | } 102 | 103 | func TestHandlePodDeleteNonExisting(t *testing.T) { 104 | log.SetOutput(ioutil.Discard) 105 | client = new(MockClient) 106 | req, err := http.NewRequest("GET", "/admin/k8s/pod/delete?pod=dsadasdasdsa", nil) 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | 111 | rr := httptest.NewRecorder() 112 | handler := http.HandlerFunc(handleAPI(handlePodDelete)) 113 | handler.ServeHTTP(rr, req) 114 | 115 | if status := rr.Code; status != http.StatusAccepted { 116 | t.Errorf("wrong status code: got %v want %v", status, http.StatusAccepted) 117 | } 118 | 119 | } 120 | 121 | func TestHandlePodsDeleteAll(t *testing.T) { 122 | log.SetOutput(ioutil.Discard) 123 | client = new(MockClient) 124 | req, err := http.NewRequest("GET", "/admin/k8s/pods/delete", nil) 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | 129 | rr := httptest.NewRecorder() 130 | handler := http.HandlerFunc(handleAPI(handlePodsDelete)) 131 | handler.ServeHTTP(rr, req) 132 | 133 | if status := rr.Code; status != http.StatusOK { 134 | t.Errorf("wrong status code: got %v want %v", status, http.StatusOK) 135 | } 136 | 137 | var podlist v1.PodList 138 | if err := json.Unmarshal([]byte(rr.Body.String()), &podlist); err != nil { 139 | t.Errorf("error turning response to pod: %v", err) 140 | t.FailNow() 141 | } 142 | 143 | if len(podlist.Items) != 12 { 144 | t.Errorf("len(podlist.Items): got %d want %d", len(podlist.Items), 12) 145 | } 146 | 147 | } 148 | 149 | func TestHandleDeploymentCreate(t *testing.T) { 150 | log.SetOutput(ioutil.Discard) 151 | client = new(MockClient) 152 | os.Setenv("APIIMAGE", "gcr.io/carnivaldemos/api") 153 | req, err := http.NewRequest("GET", "/admin/k8s/deployment/create", nil) 154 | if err != nil { 155 | t.Fatal(err) 156 | } 157 | 158 | rr := httptest.NewRecorder() 159 | handler := http.HandlerFunc(handleAPI(handleDeploymentCreate)) 160 | handler.ServeHTTP(rr, req) 161 | 162 | if status := rr.Code; status != http.StatusOK { 163 | t.Errorf("wrong status code: got %v want %v", status, http.StatusOK) 164 | } 165 | 166 | var d v1beta2.Deployment 167 | if err := json.Unmarshal([]byte(rr.Body.String()), &d); err != nil { 168 | t.Errorf("error turning response to deployment: %v", err) 169 | t.FailNow() 170 | } 171 | 172 | if d.Name != deploymentName { 173 | t.Errorf("Deployment Name: got %s want %s", d.Name, deploymentName) 174 | } 175 | os.Unsetenv("APIIMAGE") 176 | } 177 | 178 | func TestHandleDeploymentDelete(t *testing.T) { 179 | log.SetOutput(ioutil.Discard) 180 | client = new(MockClient) 181 | req, err := http.NewRequest("GET", "/admin/k8s/deployment/delete", nil) 182 | if err != nil { 183 | t.Fatal(err) 184 | } 185 | 186 | rr := httptest.NewRecorder() 187 | handler := http.HandlerFunc(handleAPI(handleDeploymentDelete)) 188 | handler.ServeHTTP(rr, req) 189 | 190 | if status := rr.Code; status != http.StatusOK { 191 | t.Errorf("wrong status code: got %v want %v", status, http.StatusOK) 192 | } 193 | 194 | var podlist v1.PodList 195 | if err := json.Unmarshal([]byte(rr.Body.String()), &podlist); err != nil { 196 | t.Errorf("error turning response to pod: %v", err) 197 | t.FailNow() 198 | } 199 | 200 | if len(podlist.Items) < 12 { 201 | t.Errorf("podlist.Items: got %d want %d", len(podlist.Items), 12) 202 | } 203 | } 204 | 205 | func TestHandleDeploymentCreateNoEnvSet(t *testing.T) { 206 | log.SetOutput(ioutil.Discard) 207 | client = new(MockClient) 208 | req, err := http.NewRequest("GET", "/admin/k8s/deployment/create", nil) 209 | if err != nil { 210 | t.Fatal(err) 211 | } 212 | 213 | rr := httptest.NewRecorder() 214 | handler := http.HandlerFunc(handleAPI(handleDeploymentCreate)) 215 | handler.ServeHTTP(rr, req) 216 | 217 | if status := rr.Code; status != http.StatusInternalServerError { 218 | t.Errorf("wrong status code: got %v want %v", status, http.StatusInternalServerError) 219 | } 220 | 221 | } 222 | 223 | func TestHandleNodes(t *testing.T) { 224 | log.SetOutput(ioutil.Discard) 225 | client = new(MockClient) 226 | req, err := http.NewRequest("GET", "/admin/k8s/nodes/get", nil) 227 | if err != nil { 228 | t.Fatal(err) 229 | } 230 | 231 | rr := httptest.NewRecorder() 232 | handler := http.HandlerFunc(handleAPI(handleNodes)) 233 | handler.ServeHTTP(rr, req) 234 | 235 | if status := rr.Code; status != http.StatusOK { 236 | t.Errorf("wrong status code: got %v want %v", status, http.StatusOK) 237 | } 238 | 239 | var nodeList v1.NodeList 240 | if err := json.Unmarshal([]byte(rr.Body.String()), &nodeList); err != nil { 241 | t.Errorf("error turning response to podlist: %v", err) 242 | t.FailNow() 243 | } 244 | 245 | if len(nodeList.Items) < 2 { 246 | t.Errorf("nodelist.Items: got %d want %d", len(nodeList.Items), 2) 247 | } 248 | 249 | } 250 | 251 | func TestHandleNodeDrain(t *testing.T) { 252 | log.SetOutput(ioutil.Discard) 253 | client = new(MockClient) 254 | req, err := http.NewRequest("GET", "/admin/k8s/node/drain?node="+nodeName, nil) 255 | if err != nil { 256 | t.Fatal(err) 257 | } 258 | 259 | rr := httptest.NewRecorder() 260 | handler := http.HandlerFunc(handleAPI(handleNodeDrain)) 261 | handler.ServeHTTP(rr, req) 262 | 263 | if status := rr.Code; status != http.StatusOK { 264 | t.Errorf("wrong status code: got %v want %v", status, http.StatusOK) 265 | } 266 | 267 | var node v1.Node 268 | if err := json.Unmarshal([]byte(rr.Body.String()), &node); err != nil { 269 | t.Errorf("error turning response to podlist: %v", err) 270 | t.FailNow() 271 | } 272 | 273 | if !node.Spec.Unschedulable { 274 | t.Errorf("node.Spec.Unschedulable: got %t want %t", node.Spec.Unschedulable, true) 275 | } 276 | 277 | } 278 | 279 | func TestHandleNodeUncordon(t *testing.T) { 280 | log.SetOutput(ioutil.Discard) 281 | client = new(MockClient) 282 | req, err := http.NewRequest("GET", "/admin/k8s/node/uncordon?node="+nodeName, nil) 283 | if err != nil { 284 | t.Fatal(err) 285 | } 286 | 287 | rr := httptest.NewRecorder() 288 | handler := http.HandlerFunc(handleAPI(handleNodeUncordon)) 289 | handler.ServeHTTP(rr, req) 290 | 291 | if status := rr.Code; status != http.StatusOK { 292 | t.Errorf("wrong status code: got %v want %v", status, http.StatusOK) 293 | } 294 | 295 | var node v1.Node 296 | if err := json.Unmarshal([]byte(rr.Body.String()), &node); err != nil { 297 | t.Errorf("error turning response to podlist: %v", err) 298 | t.FailNow() 299 | } 300 | 301 | if node.Spec.Unschedulable { 302 | t.Errorf("node.Spec.Unschedulable: got %t want %t", node.Spec.Unschedulable, false) 303 | } 304 | 305 | } 306 | -------------------------------------------------------------------------------- /apps/admin/containers/testdata/deployment_create.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "extensions/v1beta1", 3 | "kind": "Deployment", 4 | "metadata": { 5 | "annotations": { 6 | "deployment.kubernetes.io/revision": "1" 7 | }, 8 | "creationTimestamp": "2017-10-19T06:09:47Z", 9 | "generation": 1, 10 | "labels": { 11 | "app": "api" 12 | }, 13 | "name": "api-deployment", 14 | "namespace": "default", 15 | "resourceVersion": "38914618", 16 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/deployments/api-deployment", 17 | "uid": "1bf2055d-b494-11e7-ab94-42010af0005b" 18 | }, 19 | "spec": { 20 | "replicas": 1, 21 | "selector": { 22 | "matchLabels": { 23 | "app": "api" 24 | } 25 | }, 26 | "strategy": { 27 | "rollingUpdate": { 28 | "maxSurge": 1, 29 | "maxUnavailable": 1 30 | }, 31 | "type": "RollingUpdate" 32 | }, 33 | "template": { 34 | "metadata": { 35 | "creationTimestamp": null, 36 | "labels": { 37 | "app": "api" 38 | } 39 | }, 40 | "spec": { 41 | "containers": [ 42 | { 43 | "image": "gcr.io/carnivaldemos/api", 44 | "imagePullPolicy": "Always", 45 | "name": "api-deployment", 46 | "ports": [ 47 | { 48 | "containerPort": 8080, 49 | "protocol": "TCP" 50 | } 51 | ], 52 | "resources": {}, 53 | "terminationMessagePath": "/dev/termination-log", 54 | "terminationMessagePolicy": "File" 55 | } 56 | ], 57 | "dnsPolicy": "ClusterFirst", 58 | "restartPolicy": "Always", 59 | "schedulerName": "default-scheduler", 60 | "securityContext": {}, 61 | "terminationGracePeriodSeconds": 30 62 | } 63 | } 64 | }, 65 | "status": { 66 | "availableReplicas": 12, 67 | "conditions": [ 68 | { 69 | "lastTransitionTime": "2017-10-19T06:09:47Z", 70 | "lastUpdateTime": "2017-10-19T06:09:47Z", 71 | "message": "Deployment has minimum availability.", 72 | "reason": "MinimumReplicasAvailable", 73 | "status": "True", 74 | "type": "Available" 75 | } 76 | ], 77 | "observedGeneration": 12, 78 | "readyReplicas": 12, 79 | "replicas": 12, 80 | "updatedReplicas": 12 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /apps/admin/containers/testdata/deployment_delete.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "extensions/v1beta1", 3 | "kind": "Deployment", 4 | "metadata": { 5 | "annotations": { 6 | "deployment.kubernetes.io/revision": "1" 7 | }, 8 | "creationTimestamp": "2017-10-19T06:09:47Z", 9 | "generation": 1, 10 | "labels": { 11 | "app": "api" 12 | }, 13 | "name": "api-deployment", 14 | "namespace": "default", 15 | "resourceVersion": "38914618", 16 | "selfLink": "/apis/extensions/v1beta1/namespaces/default/deployments/api-deployment", 17 | "uid": "1bf2055d-b494-11e7-ab94-42010af0005b" 18 | }, 19 | "spec": { 20 | "replicas": 1, 21 | "selector": { 22 | "matchLabels": { 23 | "app": "api" 24 | } 25 | }, 26 | "strategy": { 27 | "rollingUpdate": { 28 | "maxSurge": 1, 29 | "maxUnavailable": 1 30 | }, 31 | "type": "RollingUpdate" 32 | }, 33 | "template": { 34 | "metadata": { 35 | "creationTimestamp": null, 36 | "labels": { 37 | "app": "api" 38 | } 39 | }, 40 | "spec": { 41 | "containers": [ 42 | { 43 | "image": "gcr.io/carnivaldemos/api", 44 | "imagePullPolicy": "Always", 45 | "name": "api-deployment", 46 | "ports": [ 47 | { 48 | "containerPort": 8080, 49 | "protocol": "TCP" 50 | } 51 | ], 52 | "resources": {}, 53 | "terminationMessagePath": "/dev/termination-log", 54 | "terminationMessagePolicy": "File" 55 | } 56 | ], 57 | "dnsPolicy": "ClusterFirst", 58 | "restartPolicy": "Always", 59 | "schedulerName": "default-scheduler", 60 | "securityContext": {}, 61 | "terminationGracePeriodSeconds": 30 62 | } 63 | } 64 | }, 65 | "status": { 66 | "availableReplicas": 12, 67 | "conditions": [ 68 | { 69 | "lastTransitionTime": "2017-10-19T06:09:47Z", 70 | "lastUpdateTime": "2017-10-19T06:09:47Z", 71 | "message": "Deployment has minimum availability.", 72 | "reason": "MinimumReplicasAvailable", 73 | "status": "True", 74 | "type": "Available" 75 | } 76 | ], 77 | "observedGeneration": 12, 78 | "readyReplicas": 12, 79 | "replicas": 12, 80 | "updatedReplicas": 12 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /apps/admin/containers/testdata/empty.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /apps/admin/containers/testdata/node_schedulable.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "apiVersion": "v1", 4 | "kind": "Node", 5 | "metadata": { 6 | "annotations": { 7 | "node.alpha.kubernetes.io/ttl": "0", 8 | "volumes.kubernetes.io/controller-managed-attach-detach": "true" 9 | }, 10 | "creationTimestamp": "2017-03-20T17:00:56Z", 11 | "labels": { 12 | "beta.kubernetes.io/arch": "amd64", 13 | "beta.kubernetes.io/instance-type": "n1-standard-4", 14 | "beta.kubernetes.io/os": "linux", 15 | "cloud.google.com/gke-nodepool": "default-pool", 16 | "failure-domain.beta.kubernetes.io/region": "us-central1", 17 | "failure-domain.beta.kubernetes.io/zone": "us-central1-c", 18 | "kubernetes.io/hostname": "gke-whack-a-pod-default-pool-8deaa3a5-b9p7" 19 | }, 20 | "name": "gke-whack-a-pod-default-pool-8deaa3a5-b9p7", 21 | "namespace": "", 22 | "resourceVersion": "39104372", 23 | "selfLink": "/api/v1/nodesgke-whack-a-pod-default-pool-8deaa3a5-b9p7", 24 | "uid": "c8b956be-0d8e-11e7-b473-42010af000cf" 25 | }, 26 | "spec": { 27 | "externalID": "6540334367623722115", 28 | "podCIDR": "10.120.0.0/24", 29 | "providerID": "gce://carnivaldemos/us-central1-c/gke-whack-a-pod-default-pool-8deaa3a5-b9p7" 30 | }, 31 | "status": { 32 | "addresses": [ 33 | { 34 | "address": "10.128.0.2", 35 | "type": "InternalIP" 36 | }, 37 | { 38 | "address": "35.188.8.76", 39 | "type": "ExternalIP" 40 | }, 41 | { 42 | "address": "gke-whack-a-pod-default-pool-8deaa3a5-b9p7", 43 | "type": "Hostname" 44 | } 45 | ], 46 | "allocatable": { 47 | "alpha.kubernetes.io/nvidia-gpu": "0", 48 | "cpu": "4", 49 | "memory": "15381708Ki", 50 | "pods": "110" 51 | }, 52 | "capacity": { 53 | "alpha.kubernetes.io/nvidia-gpu": "0", 54 | "cpu": "4", 55 | "memory": "15381708Ki", 56 | "pods": "110" 57 | }, 58 | "conditions": [ 59 | { 60 | "lastHeartbeatTime": "2017-03-20T17:01:19Z", 61 | "lastTransitionTime": "2017-03-20T17:01:19Z", 62 | "message": "RouteController created a route", 63 | "reason": "RouteCreated", 64 | "status": "False", 65 | "type": "NetworkUnavailable" 66 | }, 67 | { 68 | "lastHeartbeatTime": "2017-10-21T01:36:16Z", 69 | "lastTransitionTime": "2017-03-20T17:00:56Z", 70 | "message": "kubelet has sufficient disk space available", 71 | "reason": "KubeletHasSufficientDisk", 72 | "status": "False", 73 | "type": "OutOfDisk" 74 | }, 75 | { 76 | "lastHeartbeatTime": "2017-10-21T01:36:16Z", 77 | "lastTransitionTime": "2017-03-20T17:00:56Z", 78 | "message": "kubelet has sufficient memory available", 79 | "reason": "KubeletHasSufficientMemory", 80 | "status": "False", 81 | "type": "MemoryPressure" 82 | }, 83 | { 84 | "lastHeartbeatTime": "2017-10-21T01:36:16Z", 85 | "lastTransitionTime": "2017-03-20T17:00:56Z", 86 | "message": "kubelet has no disk pressure", 87 | "reason": "KubeletHasNoDiskPressure", 88 | "status": "False", 89 | "type": "DiskPressure" 90 | }, 91 | { 92 | "lastHeartbeatTime": "2017-10-21T01:36:16Z", 93 | "lastTransitionTime": "2017-03-20T17:01:26Z", 94 | "message": "kubelet is posting ready status. AppArmor enabled", 95 | "reason": "KubeletReady", 96 | "status": "True", 97 | "type": "Ready" 98 | } 99 | ], 100 | "daemonEndpoints": { 101 | "kubeletEndpoint": { 102 | "Port": 10250 103 | } 104 | }, 105 | "images": [ 106 | { 107 | "names": [ 108 | "\u003cnone\u003e@\u003cnone\u003e", 109 | "\u003cnone\u003e:\u003cnone\u003e" 110 | ], 111 | "sizeBytes": 471983629 112 | }, 113 | { 114 | "names": [ 115 | "\u003cnone\u003e@\u003cnone\u003e", 116 | "\u003cnone\u003e:\u003cnone\u003e" 117 | ], 118 | "sizeBytes": 471983503 119 | }, 120 | { 121 | "names": [ 122 | "\u003cnone\u003e@\u003cnone\u003e", 123 | "\u003cnone\u003e:\u003cnone\u003e" 124 | ], 125 | "sizeBytes": 471983302 126 | }, 127 | { 128 | "names": [ 129 | "\u003cnone\u003e@\u003cnone\u003e", 130 | "\u003cnone\u003e:\u003cnone\u003e" 131 | ], 132 | "sizeBytes": 471983302 133 | }, 134 | { 135 | "names": [ 136 | "\u003cnone\u003e@\u003cnone\u003e", 137 | "\u003cnone\u003e:\u003cnone\u003e" 138 | ], 139 | "sizeBytes": 471983290 140 | }, 141 | { 142 | "names": [ 143 | "\u003cnone\u003e@\u003cnone\u003e", 144 | "\u003cnone\u003e:\u003cnone\u003e" 145 | ], 146 | "sizeBytes": 471983275 147 | }, 148 | { 149 | "names": [ 150 | "\u003cnone\u003e@\u003cnone\u003e", 151 | "\u003cnone\u003e:\u003cnone\u003e" 152 | ], 153 | "sizeBytes": 471983194 154 | }, 155 | { 156 | "names": [ 157 | "\u003cnone\u003e@\u003cnone\u003e", 158 | "\u003cnone\u003e:\u003cnone\u003e" 159 | ], 160 | "sizeBytes": 471979999 161 | }, 162 | { 163 | "names": [ 164 | "\u003cnone\u003e@\u003cnone\u003e", 165 | "\u003cnone\u003e:\u003cnone\u003e" 166 | ], 167 | "sizeBytes": 471979987 168 | }, 169 | { 170 | "names": [ 171 | "\u003cnone\u003e@\u003cnone\u003e", 172 | "\u003cnone\u003e:\u003cnone\u003e" 173 | ], 174 | "sizeBytes": 471978343 175 | }, 176 | { 177 | "names": [ 178 | "\u003cnone\u003e@\u003cnone\u003e", 179 | "\u003cnone\u003e:\u003cnone\u003e" 180 | ], 181 | "sizeBytes": 471978337 182 | }, 183 | { 184 | "names": [ 185 | "\u003cnone\u003e@\u003cnone\u003e", 186 | "\u003cnone\u003e:\u003cnone\u003e" 187 | ], 188 | "sizeBytes": 471978322 189 | }, 190 | { 191 | "names": [ 192 | "\u003cnone\u003e@\u003cnone\u003e", 193 | "\u003cnone\u003e:\u003cnone\u003e" 194 | ], 195 | "sizeBytes": 471978229 196 | }, 197 | { 198 | "names": [ 199 | "\u003cnone\u003e@\u003cnone\u003e", 200 | "\u003cnone\u003e:\u003cnone\u003e" 201 | ], 202 | "sizeBytes": 471978172 203 | }, 204 | { 205 | "names": [ 206 | "\u003cnone\u003e@\u003cnone\u003e", 207 | "\u003cnone\u003e:\u003cnone\u003e" 208 | ], 209 | "sizeBytes": 471978151 210 | }, 211 | { 212 | "names": [ 213 | "\u003cnone\u003e@\u003cnone\u003e", 214 | "\u003cnone\u003e:\u003cnone\u003e" 215 | ], 216 | "sizeBytes": 471978046 217 | }, 218 | { 219 | "names": [ 220 | "\u003cnone\u003e@\u003cnone\u003e", 221 | "\u003cnone\u003e:\u003cnone\u003e" 222 | ], 223 | "sizeBytes": 471977977 224 | }, 225 | { 226 | "names": [ 227 | "\u003cnone\u003e@\u003cnone\u003e", 228 | "\u003cnone\u003e:\u003cnone\u003e" 229 | ], 230 | "sizeBytes": 471977950 231 | }, 232 | { 233 | "names": [ 234 | "\u003cnone\u003e@\u003cnone\u003e", 235 | "\u003cnone\u003e:\u003cnone\u003e" 236 | ], 237 | "sizeBytes": 471977950 238 | }, 239 | { 240 | "names": [ 241 | "\u003cnone\u003e@\u003cnone\u003e", 242 | "\u003cnone\u003e:\u003cnone\u003e" 243 | ], 244 | "sizeBytes": 471977944 245 | }, 246 | { 247 | "names": [ 248 | "\u003cnone\u003e@\u003cnone\u003e", 249 | "\u003cnone\u003e:\u003cnone\u003e" 250 | ], 251 | "sizeBytes": 471977944 252 | }, 253 | { 254 | "names": [ 255 | "\u003cnone\u003e@\u003cnone\u003e", 256 | "\u003cnone\u003e:\u003cnone\u003e" 257 | ], 258 | "sizeBytes": 471977884 259 | }, 260 | { 261 | "names": [ 262 | "\u003cnone\u003e@\u003cnone\u003e", 263 | "\u003cnone\u003e:\u003cnone\u003e" 264 | ], 265 | "sizeBytes": 471977881 266 | }, 267 | { 268 | "names": [ 269 | "\u003cnone\u003e@\u003cnone\u003e", 270 | "\u003cnone\u003e:\u003cnone\u003e" 271 | ], 272 | "sizeBytes": 471977863 273 | }, 274 | { 275 | "names": [ 276 | "\u003cnone\u003e@\u003cnone\u003e", 277 | "\u003cnone\u003e:\u003cnone\u003e" 278 | ], 279 | "sizeBytes": 471977863 280 | }, 281 | { 282 | "names": [ 283 | "\u003cnone\u003e@\u003cnone\u003e", 284 | "\u003cnone\u003e:\u003cnone\u003e" 285 | ], 286 | "sizeBytes": 471977860 287 | }, 288 | { 289 | "names": [ 290 | "\u003cnone\u003e@\u003cnone\u003e", 291 | "\u003cnone\u003e:\u003cnone\u003e" 292 | ], 293 | "sizeBytes": 471977848 294 | }, 295 | { 296 | "names": [ 297 | "\u003cnone\u003e@\u003cnone\u003e", 298 | "\u003cnone\u003e:\u003cnone\u003e" 299 | ], 300 | "sizeBytes": 471977305 301 | }, 302 | { 303 | "names": [ 304 | "\u003cnone\u003e@\u003cnone\u003e", 305 | "\u003cnone\u003e:\u003cnone\u003e" 306 | ], 307 | "sizeBytes": 471977278 308 | }, 309 | { 310 | "names": [ 311 | "\u003cnone\u003e@\u003cnone\u003e", 312 | "\u003cnone\u003e:\u003cnone\u003e" 313 | ], 314 | "sizeBytes": 471977269 315 | }, 316 | { 317 | "names": [ 318 | "\u003cnone\u003e@\u003cnone\u003e", 319 | "\u003cnone\u003e:\u003cnone\u003e" 320 | ], 321 | "sizeBytes": 471977251 322 | }, 323 | { 324 | "names": [ 325 | "\u003cnone\u003e@\u003cnone\u003e", 326 | "\u003cnone\u003e:\u003cnone\u003e" 327 | ], 328 | "sizeBytes": 471977248 329 | }, 330 | { 331 | "names": [ 332 | "\u003cnone\u003e@\u003cnone\u003e", 333 | "\u003cnone\u003e:\u003cnone\u003e" 334 | ], 335 | "sizeBytes": 471977248 336 | }, 337 | { 338 | "names": [ 339 | "\u003cnone\u003e@\u003cnone\u003e", 340 | "\u003cnone\u003e:\u003cnone\u003e" 341 | ], 342 | "sizeBytes": 471977245 343 | }, 344 | { 345 | "names": [ 346 | "\u003cnone\u003e@\u003cnone\u003e", 347 | "\u003cnone\u003e:\u003cnone\u003e" 348 | ], 349 | "sizeBytes": 471977239 350 | }, 351 | { 352 | "names": [ 353 | "\u003cnone\u003e@\u003cnone\u003e", 354 | "\u003cnone\u003e:\u003cnone\u003e" 355 | ], 356 | "sizeBytes": 471976675 357 | }, 358 | { 359 | "names": [ 360 | "\u003cnone\u003e@\u003cnone\u003e", 361 | "\u003cnone\u003e:\u003cnone\u003e" 362 | ], 363 | "sizeBytes": 471976234 364 | }, 365 | { 366 | "names": [ 367 | "\u003cnone\u003e@\u003cnone\u003e", 368 | "\u003cnone\u003e:\u003cnone\u003e" 369 | ], 370 | "sizeBytes": 471976234 371 | }, 372 | { 373 | "names": [ 374 | "\u003cnone\u003e@\u003cnone\u003e", 375 | "\u003cnone\u003e:\u003cnone\u003e" 376 | ], 377 | "sizeBytes": 471976126 378 | }, 379 | { 380 | "names": [ 381 | "\u003cnone\u003e@\u003cnone\u003e", 382 | "\u003cnone\u003e:\u003cnone\u003e" 383 | ], 384 | "sizeBytes": 471974611 385 | }, 386 | { 387 | "names": [ 388 | "\u003cnone\u003e@\u003cnone\u003e", 389 | "\u003cnone\u003e:\u003cnone\u003e" 390 | ], 391 | "sizeBytes": 471974608 392 | }, 393 | { 394 | "names": [ 395 | "\u003cnone\u003e@\u003cnone\u003e", 396 | "\u003cnone\u003e:\u003cnone\u003e" 397 | ], 398 | "sizeBytes": 471974605 399 | }, 400 | { 401 | "names": [ 402 | "\u003cnone\u003e@\u003cnone\u003e", 403 | "\u003cnone\u003e:\u003cnone\u003e" 404 | ], 405 | "sizeBytes": 471974398 406 | }, 407 | { 408 | "names": [ 409 | "\u003cnone\u003e@\u003cnone\u003e", 410 | "\u003cnone\u003e:\u003cnone\u003e" 411 | ], 412 | "sizeBytes": 471973969 413 | }, 414 | { 415 | "names": [ 416 | "\u003cnone\u003e@\u003cnone\u003e", 417 | "\u003cnone\u003e:\u003cnone\u003e" 418 | ], 419 | "sizeBytes": 471973726 420 | }, 421 | { 422 | "names": [ 423 | "\u003cnone\u003e@\u003cnone\u003e", 424 | "\u003cnone\u003e:\u003cnone\u003e" 425 | ], 426 | "sizeBytes": 471970832 427 | }, 428 | { 429 | "names": [ 430 | "\u003cnone\u003e@\u003cnone\u003e", 431 | "\u003cnone\u003e:\u003cnone\u003e" 432 | ], 433 | "sizeBytes": 471970820 434 | }, 435 | { 436 | "names": [ 437 | "\u003cnone\u003e@\u003cnone\u003e", 438 | "\u003cnone\u003e:\u003cnone\u003e" 439 | ], 440 | "sizeBytes": 471970812 441 | }, 442 | { 443 | "names": [ 444 | "\u003cnone\u003e@\u003cnone\u003e", 445 | "\u003cnone\u003e:\u003cnone\u003e" 446 | ], 447 | "sizeBytes": 471970810 448 | }, 449 | { 450 | "names": [ 451 | "\u003cnone\u003e@\u003cnone\u003e", 452 | "\u003cnone\u003e:\u003cnone\u003e" 453 | ], 454 | "sizeBytes": 471970768 455 | } 456 | ], 457 | "nodeInfo": { 458 | "architecture": "amd64", 459 | "bootID": "020a803c-76eb-4de6-ad2b-4b52af3f529d", 460 | "containerRuntimeVersion": "docker://1.11.2", 461 | "kernelVersion": "4.4.21+", 462 | "kubeProxyVersion": "v1.5.3", 463 | "kubeletVersion": "v1.5.3", 464 | "machineID": "9ae15998aaa220842be1486ea9751460", 465 | "operatingSystem": "linux", 466 | "osImage": "Container-Optimized OS from Google", 467 | "systemUUID": "9AE15998-AAA2-2084-2BE1-486EA9751460" 468 | } 469 | } 470 | } 471 | -------------------------------------------------------------------------------- /apps/admin/containers/testdata/node_unschedulable.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "apiVersion": "v1", 4 | "kind": "Node", 5 | "metadata": { 6 | "annotations": { 7 | "node.alpha.kubernetes.io/ttl": "0", 8 | "volumes.kubernetes.io/controller-managed-attach-detach": "true" 9 | }, 10 | "creationTimestamp": "2017-03-20T17:00:56Z", 11 | "labels": { 12 | "beta.kubernetes.io/arch": "amd64", 13 | "beta.kubernetes.io/instance-type": "n1-standard-4", 14 | "beta.kubernetes.io/os": "linux", 15 | "cloud.google.com/gke-nodepool": "default-pool", 16 | "failure-domain.beta.kubernetes.io/region": "us-central1", 17 | "failure-domain.beta.kubernetes.io/zone": "us-central1-c", 18 | "kubernetes.io/hostname": "gke-whack-a-pod-default-pool-8deaa3a5-b9p7" 19 | }, 20 | "name": "gke-whack-a-pod-default-pool-8deaa3a5-b9p7", 21 | "namespace": "", 22 | "resourceVersion": "39104372", 23 | "selfLink": "/api/v1/nodesgke-whack-a-pod-default-pool-8deaa3a5-b9p7", 24 | "uid": "c8b956be-0d8e-11e7-b473-42010af000cf" 25 | }, 26 | "spec": { 27 | "externalID": "6540334367623722115", 28 | "podCIDR": "10.120.0.0/24", 29 | "providerID": "gce://carnivaldemos/us-central1-c/gke-whack-a-pod-default-pool-8deaa3a5-b9p7", 30 | "unschedulable": true 31 | }, 32 | "status": { 33 | "addresses": [ 34 | { 35 | "address": "10.128.0.2", 36 | "type": "InternalIP" 37 | }, 38 | { 39 | "address": "35.188.8.76", 40 | "type": "ExternalIP" 41 | }, 42 | { 43 | "address": "gke-whack-a-pod-default-pool-8deaa3a5-b9p7", 44 | "type": "Hostname" 45 | } 46 | ], 47 | "allocatable": { 48 | "alpha.kubernetes.io/nvidia-gpu": "0", 49 | "cpu": "4", 50 | "memory": "15381708Ki", 51 | "pods": "110" 52 | }, 53 | "capacity": { 54 | "alpha.kubernetes.io/nvidia-gpu": "0", 55 | "cpu": "4", 56 | "memory": "15381708Ki", 57 | "pods": "110" 58 | }, 59 | "conditions": [ 60 | { 61 | "lastHeartbeatTime": "2017-03-20T17:01:19Z", 62 | "lastTransitionTime": "2017-03-20T17:01:19Z", 63 | "message": "RouteController created a route", 64 | "reason": "RouteCreated", 65 | "status": "False", 66 | "type": "NetworkUnavailable" 67 | }, 68 | { 69 | "lastHeartbeatTime": "2017-10-21T01:36:16Z", 70 | "lastTransitionTime": "2017-03-20T17:00:56Z", 71 | "message": "kubelet has sufficient disk space available", 72 | "reason": "KubeletHasSufficientDisk", 73 | "status": "False", 74 | "type": "OutOfDisk" 75 | }, 76 | { 77 | "lastHeartbeatTime": "2017-10-21T01:36:16Z", 78 | "lastTransitionTime": "2017-03-20T17:00:56Z", 79 | "message": "kubelet has sufficient memory available", 80 | "reason": "KubeletHasSufficientMemory", 81 | "status": "False", 82 | "type": "MemoryPressure" 83 | }, 84 | { 85 | "lastHeartbeatTime": "2017-10-21T01:36:16Z", 86 | "lastTransitionTime": "2017-03-20T17:00:56Z", 87 | "message": "kubelet has no disk pressure", 88 | "reason": "KubeletHasNoDiskPressure", 89 | "status": "False", 90 | "type": "DiskPressure" 91 | }, 92 | { 93 | "lastHeartbeatTime": "2017-10-21T01:36:16Z", 94 | "lastTransitionTime": "2017-03-20T17:01:26Z", 95 | "message": "kubelet is posting ready status. AppArmor enabled", 96 | "reason": "KubeletReady", 97 | "status": "True", 98 | "type": "Ready" 99 | } 100 | ], 101 | "daemonEndpoints": { 102 | "kubeletEndpoint": { 103 | "Port": 10250 104 | } 105 | }, 106 | "images": [ 107 | { 108 | "names": [ 109 | "\u003cnone\u003e@\u003cnone\u003e", 110 | "\u003cnone\u003e:\u003cnone\u003e" 111 | ], 112 | "sizeBytes": 471983629 113 | }, 114 | { 115 | "names": [ 116 | "\u003cnone\u003e@\u003cnone\u003e", 117 | "\u003cnone\u003e:\u003cnone\u003e" 118 | ], 119 | "sizeBytes": 471983503 120 | }, 121 | { 122 | "names": [ 123 | "\u003cnone\u003e@\u003cnone\u003e", 124 | "\u003cnone\u003e:\u003cnone\u003e" 125 | ], 126 | "sizeBytes": 471983302 127 | }, 128 | { 129 | "names": [ 130 | "\u003cnone\u003e@\u003cnone\u003e", 131 | "\u003cnone\u003e:\u003cnone\u003e" 132 | ], 133 | "sizeBytes": 471983302 134 | }, 135 | { 136 | "names": [ 137 | "\u003cnone\u003e@\u003cnone\u003e", 138 | "\u003cnone\u003e:\u003cnone\u003e" 139 | ], 140 | "sizeBytes": 471983290 141 | }, 142 | { 143 | "names": [ 144 | "\u003cnone\u003e@\u003cnone\u003e", 145 | "\u003cnone\u003e:\u003cnone\u003e" 146 | ], 147 | "sizeBytes": 471983275 148 | }, 149 | { 150 | "names": [ 151 | "\u003cnone\u003e@\u003cnone\u003e", 152 | "\u003cnone\u003e:\u003cnone\u003e" 153 | ], 154 | "sizeBytes": 471983194 155 | }, 156 | { 157 | "names": [ 158 | "\u003cnone\u003e@\u003cnone\u003e", 159 | "\u003cnone\u003e:\u003cnone\u003e" 160 | ], 161 | "sizeBytes": 471979999 162 | }, 163 | { 164 | "names": [ 165 | "\u003cnone\u003e@\u003cnone\u003e", 166 | "\u003cnone\u003e:\u003cnone\u003e" 167 | ], 168 | "sizeBytes": 471979987 169 | }, 170 | { 171 | "names": [ 172 | "\u003cnone\u003e@\u003cnone\u003e", 173 | "\u003cnone\u003e:\u003cnone\u003e" 174 | ], 175 | "sizeBytes": 471978343 176 | }, 177 | { 178 | "names": [ 179 | "\u003cnone\u003e@\u003cnone\u003e", 180 | "\u003cnone\u003e:\u003cnone\u003e" 181 | ], 182 | "sizeBytes": 471978337 183 | }, 184 | { 185 | "names": [ 186 | "\u003cnone\u003e@\u003cnone\u003e", 187 | "\u003cnone\u003e:\u003cnone\u003e" 188 | ], 189 | "sizeBytes": 471978322 190 | }, 191 | { 192 | "names": [ 193 | "\u003cnone\u003e@\u003cnone\u003e", 194 | "\u003cnone\u003e:\u003cnone\u003e" 195 | ], 196 | "sizeBytes": 471978229 197 | }, 198 | { 199 | "names": [ 200 | "\u003cnone\u003e@\u003cnone\u003e", 201 | "\u003cnone\u003e:\u003cnone\u003e" 202 | ], 203 | "sizeBytes": 471978172 204 | }, 205 | { 206 | "names": [ 207 | "\u003cnone\u003e@\u003cnone\u003e", 208 | "\u003cnone\u003e:\u003cnone\u003e" 209 | ], 210 | "sizeBytes": 471978151 211 | }, 212 | { 213 | "names": [ 214 | "\u003cnone\u003e@\u003cnone\u003e", 215 | "\u003cnone\u003e:\u003cnone\u003e" 216 | ], 217 | "sizeBytes": 471978046 218 | }, 219 | { 220 | "names": [ 221 | "\u003cnone\u003e@\u003cnone\u003e", 222 | "\u003cnone\u003e:\u003cnone\u003e" 223 | ], 224 | "sizeBytes": 471977977 225 | }, 226 | { 227 | "names": [ 228 | "\u003cnone\u003e@\u003cnone\u003e", 229 | "\u003cnone\u003e:\u003cnone\u003e" 230 | ], 231 | "sizeBytes": 471977950 232 | }, 233 | { 234 | "names": [ 235 | "\u003cnone\u003e@\u003cnone\u003e", 236 | "\u003cnone\u003e:\u003cnone\u003e" 237 | ], 238 | "sizeBytes": 471977950 239 | }, 240 | { 241 | "names": [ 242 | "\u003cnone\u003e@\u003cnone\u003e", 243 | "\u003cnone\u003e:\u003cnone\u003e" 244 | ], 245 | "sizeBytes": 471977944 246 | }, 247 | { 248 | "names": [ 249 | "\u003cnone\u003e@\u003cnone\u003e", 250 | "\u003cnone\u003e:\u003cnone\u003e" 251 | ], 252 | "sizeBytes": 471977944 253 | }, 254 | { 255 | "names": [ 256 | "\u003cnone\u003e@\u003cnone\u003e", 257 | "\u003cnone\u003e:\u003cnone\u003e" 258 | ], 259 | "sizeBytes": 471977884 260 | }, 261 | { 262 | "names": [ 263 | "\u003cnone\u003e@\u003cnone\u003e", 264 | "\u003cnone\u003e:\u003cnone\u003e" 265 | ], 266 | "sizeBytes": 471977881 267 | }, 268 | { 269 | "names": [ 270 | "\u003cnone\u003e@\u003cnone\u003e", 271 | "\u003cnone\u003e:\u003cnone\u003e" 272 | ], 273 | "sizeBytes": 471977863 274 | }, 275 | { 276 | "names": [ 277 | "\u003cnone\u003e@\u003cnone\u003e", 278 | "\u003cnone\u003e:\u003cnone\u003e" 279 | ], 280 | "sizeBytes": 471977863 281 | }, 282 | { 283 | "names": [ 284 | "\u003cnone\u003e@\u003cnone\u003e", 285 | "\u003cnone\u003e:\u003cnone\u003e" 286 | ], 287 | "sizeBytes": 471977860 288 | }, 289 | { 290 | "names": [ 291 | "\u003cnone\u003e@\u003cnone\u003e", 292 | "\u003cnone\u003e:\u003cnone\u003e" 293 | ], 294 | "sizeBytes": 471977848 295 | }, 296 | { 297 | "names": [ 298 | "\u003cnone\u003e@\u003cnone\u003e", 299 | "\u003cnone\u003e:\u003cnone\u003e" 300 | ], 301 | "sizeBytes": 471977305 302 | }, 303 | { 304 | "names": [ 305 | "\u003cnone\u003e@\u003cnone\u003e", 306 | "\u003cnone\u003e:\u003cnone\u003e" 307 | ], 308 | "sizeBytes": 471977278 309 | }, 310 | { 311 | "names": [ 312 | "\u003cnone\u003e@\u003cnone\u003e", 313 | "\u003cnone\u003e:\u003cnone\u003e" 314 | ], 315 | "sizeBytes": 471977269 316 | }, 317 | { 318 | "names": [ 319 | "\u003cnone\u003e@\u003cnone\u003e", 320 | "\u003cnone\u003e:\u003cnone\u003e" 321 | ], 322 | "sizeBytes": 471977251 323 | }, 324 | { 325 | "names": [ 326 | "\u003cnone\u003e@\u003cnone\u003e", 327 | "\u003cnone\u003e:\u003cnone\u003e" 328 | ], 329 | "sizeBytes": 471977248 330 | }, 331 | { 332 | "names": [ 333 | "\u003cnone\u003e@\u003cnone\u003e", 334 | "\u003cnone\u003e:\u003cnone\u003e" 335 | ], 336 | "sizeBytes": 471977248 337 | }, 338 | { 339 | "names": [ 340 | "\u003cnone\u003e@\u003cnone\u003e", 341 | "\u003cnone\u003e:\u003cnone\u003e" 342 | ], 343 | "sizeBytes": 471977245 344 | }, 345 | { 346 | "names": [ 347 | "\u003cnone\u003e@\u003cnone\u003e", 348 | "\u003cnone\u003e:\u003cnone\u003e" 349 | ], 350 | "sizeBytes": 471977239 351 | }, 352 | { 353 | "names": [ 354 | "\u003cnone\u003e@\u003cnone\u003e", 355 | "\u003cnone\u003e:\u003cnone\u003e" 356 | ], 357 | "sizeBytes": 471976675 358 | }, 359 | { 360 | "names": [ 361 | "\u003cnone\u003e@\u003cnone\u003e", 362 | "\u003cnone\u003e:\u003cnone\u003e" 363 | ], 364 | "sizeBytes": 471976234 365 | }, 366 | { 367 | "names": [ 368 | "\u003cnone\u003e@\u003cnone\u003e", 369 | "\u003cnone\u003e:\u003cnone\u003e" 370 | ], 371 | "sizeBytes": 471976234 372 | }, 373 | { 374 | "names": [ 375 | "\u003cnone\u003e@\u003cnone\u003e", 376 | "\u003cnone\u003e:\u003cnone\u003e" 377 | ], 378 | "sizeBytes": 471976126 379 | }, 380 | { 381 | "names": [ 382 | "\u003cnone\u003e@\u003cnone\u003e", 383 | "\u003cnone\u003e:\u003cnone\u003e" 384 | ], 385 | "sizeBytes": 471974611 386 | }, 387 | { 388 | "names": [ 389 | "\u003cnone\u003e@\u003cnone\u003e", 390 | "\u003cnone\u003e:\u003cnone\u003e" 391 | ], 392 | "sizeBytes": 471974608 393 | }, 394 | { 395 | "names": [ 396 | "\u003cnone\u003e@\u003cnone\u003e", 397 | "\u003cnone\u003e:\u003cnone\u003e" 398 | ], 399 | "sizeBytes": 471974605 400 | }, 401 | { 402 | "names": [ 403 | "\u003cnone\u003e@\u003cnone\u003e", 404 | "\u003cnone\u003e:\u003cnone\u003e" 405 | ], 406 | "sizeBytes": 471974398 407 | }, 408 | { 409 | "names": [ 410 | "\u003cnone\u003e@\u003cnone\u003e", 411 | "\u003cnone\u003e:\u003cnone\u003e" 412 | ], 413 | "sizeBytes": 471973969 414 | }, 415 | { 416 | "names": [ 417 | "\u003cnone\u003e@\u003cnone\u003e", 418 | "\u003cnone\u003e:\u003cnone\u003e" 419 | ], 420 | "sizeBytes": 471973726 421 | }, 422 | { 423 | "names": [ 424 | "\u003cnone\u003e@\u003cnone\u003e", 425 | "\u003cnone\u003e:\u003cnone\u003e" 426 | ], 427 | "sizeBytes": 471970832 428 | }, 429 | { 430 | "names": [ 431 | "\u003cnone\u003e@\u003cnone\u003e", 432 | "\u003cnone\u003e:\u003cnone\u003e" 433 | ], 434 | "sizeBytes": 471970820 435 | }, 436 | { 437 | "names": [ 438 | "\u003cnone\u003e@\u003cnone\u003e", 439 | "\u003cnone\u003e:\u003cnone\u003e" 440 | ], 441 | "sizeBytes": 471970812 442 | }, 443 | { 444 | "names": [ 445 | "\u003cnone\u003e@\u003cnone\u003e", 446 | "\u003cnone\u003e:\u003cnone\u003e" 447 | ], 448 | "sizeBytes": 471970810 449 | }, 450 | { 451 | "names": [ 452 | "\u003cnone\u003e@\u003cnone\u003e", 453 | "\u003cnone\u003e:\u003cnone\u003e" 454 | ], 455 | "sizeBytes": 471970768 456 | } 457 | ], 458 | "nodeInfo": { 459 | "architecture": "amd64", 460 | "bootID": "020a803c-76eb-4de6-ad2b-4b52af3f529d", 461 | "containerRuntimeVersion": "docker://1.11.2", 462 | "kernelVersion": "4.4.21+", 463 | "kubeProxyVersion": "v1.5.3", 464 | "kubeletVersion": "v1.5.3", 465 | "machineID": "9ae15998aaa220842be1486ea9751460", 466 | "operatingSystem": "linux", 467 | "osImage": "Container-Optimized OS from Google", 468 | "systemUUID": "9AE15998-AAA2-2084-2BE1-486EA9751460" 469 | } 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /apps/admin/containers/testdata/pod.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Pod", 4 | "metadata": { 5 | "generateName": "api-deployment-1435701907-", 6 | "labels": { 7 | "app": "api" 8 | }, 9 | "name": "api-deployment-1435701907-xx9lm", 10 | "namespace": "default", 11 | "ownerReferences": [ 12 | { 13 | "apiVersion": "extensions/v1beta1", 14 | "controller": true, 15 | "kind": "ReplicaSet", 16 | "name": "api-deployment-1435701907" 17 | } 18 | ], 19 | "selfLink": "/api/v1/namespaces/default/pods/api-deployment-1435701907-xx9lm" 20 | }, 21 | "spec": { 22 | "containers": [ 23 | { 24 | "image": "gcr.io/carnivaldemos/api", 25 | "imagePullPolicy": "Always", 26 | "name": "api-deployment", 27 | "ports": [ 28 | { 29 | "containerPort": 8080, 30 | "protocol": "TCP" 31 | } 32 | ] 33 | } 34 | ], 35 | "dnsPolicy": "ClusterFirst", 36 | "nodeName": "gke-whack-a-pod-default-pool-8deaa3a5-znz4", 37 | "restartPolicy": "Always", 38 | "schedulerName": "default-scheduler", 39 | "terminationGracePeriodSeconds": 30, 40 | "tolerations": [ 41 | { 42 | "effect": "NoExecute", 43 | "key": "node.alpha.kubernetes.io/notReady", 44 | "operator": "Exists", 45 | "tolerationSeconds": 300 46 | }, 47 | { 48 | "effect": "NoExecute", 49 | "key": "node.alpha.kubernetes.io/unreachable", 50 | "operator": "Exists", 51 | "tolerationSeconds": 300 52 | } 53 | ], 54 | "volumes": [ 55 | { 56 | "name": "default-token-4q4nh", 57 | "secret": { 58 | "defaultMode": 420, 59 | "secretName": "default-token-4q4nh" 60 | } 61 | } 62 | ] 63 | }, 64 | "status": { 65 | "hostIP": "10.128.0.3", 66 | "phase": "Running", 67 | "podIP": "10.120.1.246" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /apps/admin/docker/Makefile: -------------------------------------------------------------------------------- 1 | BASEDIR = $(shell pwd) 2 | 3 | .DEFAULT_GOAL := app 4 | 5 | app: clean build serve 6 | 7 | 8 | main: 9 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o "$(BASEDIR)/../containers/main" "$(BASEDIR)/../containers/main.go" "$(BASEDIR)/../containers/kubernetes.go" 10 | 11 | build: main 12 | docker build -t admin "$(BASEDIR)/../containers/." 13 | 14 | serve: 15 | docker run --name=admin -d -P -p 8080:8080 admin 16 | 17 | clean: 18 | -docker stop admin 19 | -docker rm admin 20 | -docker rmi admin -------------------------------------------------------------------------------- /apps/admin/kubernetes/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | BASEDIR = $(shell pwd) 15 | 16 | include ../../../Makefile.properties 17 | 18 | 19 | reset: clean.deployment app 20 | say "App Reset" 21 | 22 | reset.safe: 23 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o "$(BASEDIR)/../containers/main" "$(BASEDIR)/../containers/main.go" "$(BASEDIR)/../containers/kubernetes.go" 24 | gcloud container builds submit "$(BASEDIR)/../containers/." --tag=$(DOCKERREPO)/admin 25 | -kubectl delete deployment admin-deployment 26 | -kubectl run admin-deployment --image=$(DOCKERREPO)/admin --replicas=1 --port=8080 --labels=app=admin --env="APIIMAGE=$(DOCKERREPO)/api" 27 | say "app refresh complete" 28 | 29 | app: build deploy 30 | 31 | main: 32 | go get github.com/gorilla/mux 33 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o "$(BASEDIR)/../containers/main" "$(BASEDIR)/../containers/main.go" "$(BASEDIR)/../containers/kubernetes.go" 34 | 35 | build: env main 36 | gcloud container builds submit "$(BASEDIR)/../containers/." --tag=$(DOCKERREPO)/admin 37 | 38 | build.dockerhub: main 39 | docker build "$(BASEDIR)/../containers/." --tag=$(DOCKERREPO):admin 40 | docker push $(DOCKERREPO):admin 41 | 42 | build.generic: main 43 | docker build "$(BASEDIR)/../containers/." --tag=$(DOCKERREPO)/whackapod-admin 44 | docker push $(DOCKERREPO)/whackapod-admin 45 | 46 | deploy: env creds deployment service 47 | 48 | deploy.minikube: deployment service 49 | 50 | deploy.minikube.dockerhub: 51 | kubectl run admin-deployment --image=$(DOCKERREPO):admin --replicas=1 --port=8080 --labels=app=admin --env="APIIMAGE=$(DOCKERREPO):api" 52 | kubectl expose deployment admin-deployment --name=admin --target-port=8080 --type=NodePort --labels="app=admin" 53 | kubectl create serviceaccount wap-admin 54 | kubectl create clusterrolebinding wap-admin --clusterrole=cluster-admin --serviceaccount=default:wap-admin 55 | kubectl set serviceaccount deployment admin-deployment wap-admin 56 | 57 | deploy.generic: 58 | kubectl run admin-deployment --image=$(DOCKERREPO)/whackapod-admin --replicas=1 --port=8080 --labels=app=admin --env="APIIMAGE=$(DOCKERREPO)/whackapod-api" 59 | kubectl expose deployment admin-deployment --name=admin --target-port=8080 --type=NodePort --labels="app=admin" 60 | kubectl create serviceaccount wap-admin 61 | kubectl create clusterrolebinding wap-admin --clusterrole=cluster-admin --serviceaccount=$$(kubectl config view -o jsonpath="{.contexts[?(@.name==\"$$(kubectl config current-context)\")].context.namespace}"):wap-admin 62 | kubectl set serviceaccount deployment admin-deployment wap-admin 63 | 64 | test: 65 | cd "$(BASEDIR)/../containers" && go test 66 | 67 | deployment: 68 | kubectl run admin-deployment --image=$(DOCKERREPO)/admin --replicas=1 --port=8080 --labels=app=admin --env="APIIMAGE=$(DOCKERREPO)/api" 69 | 70 | service: 71 | kubectl expose deployment admin-deployment --name=admin --target-port=8080 --type=NodePort --labels="app=admin" 72 | 73 | clean: env creds clean.deployment clean.service 74 | 75 | clean.minikube: clean.deployment clean.service 76 | 77 | clean.minikube.dockerhub: clean.deployment clean.service 78 | -kubectl delete serviceaccount wap-admin 79 | -kubectl delete clusterrolebinding wap-admin 80 | 81 | clean.deployment: 82 | -kubectl delete deployment admin-deployment 83 | 84 | clean.service: 85 | -kubectl delete service admin 86 | 87 | clean.generic: clean.deployment clean.service 88 | -kubectl delete serviceaccount wap-admin 89 | -kubectl delete clusterrolebinding wap-admin 90 | 91 | retry: clean build deploy 92 | 93 | config: env 94 | echo "No custom config needed for admin" 95 | -------------------------------------------------------------------------------- /apps/api/containers/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All Rights Reserved. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | FROM scratch 15 | ADD main / 16 | CMD ["/main"] -------------------------------------------------------------------------------- /apps/api/containers/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math/rand" 7 | "net/http" 8 | "os" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | var srv = http.Server{ 14 | ReadTimeout: 5 * time.Second, 15 | WriteTimeout: 10 * time.Second, 16 | Addr: ":8080", 17 | Handler: handler(), 18 | } 19 | 20 | func main() { 21 | log.Printf("starting whack a pod color api") 22 | srv.ListenAndServe() 23 | } 24 | 25 | func handler() http.Handler { 26 | r := http.NewServeMux() 27 | r.HandleFunc("/", health) 28 | r.HandleFunc("/healthz", health) 29 | r.HandleFunc("/api/healthz", health) 30 | r.HandleFunc("/api/color", color) 31 | r.HandleFunc("/api/color/", color) 32 | r.HandleFunc("/api/color-complete", colorComplete) 33 | r.HandleFunc("/api/color-complete/", colorComplete) 34 | r.HandleFunc("/color", color) 35 | r.HandleFunc("/color/", color) 36 | r.HandleFunc("/color-complete", colorComplete) 37 | r.HandleFunc("/color-complete/", colorComplete) 38 | 39 | return r 40 | } 41 | 42 | func health(w http.ResponseWriter, r *http.Request) { 43 | w.Header().Add("Access-Control-Allow-Origin", "*") 44 | w.WriteHeader(http.StatusOK) 45 | fmt.Fprint(w, "ok") 46 | } 47 | 48 | func color(w http.ResponseWriter, r *http.Request) { 49 | w.Header().Add("Access-Control-Allow-Origin", "*") 50 | w.WriteHeader(http.StatusOK) 51 | fmt.Fprint(w, hexColorString()) 52 | } 53 | 54 | func colorComplete(w http.ResponseWriter, r *http.Request) { 55 | status := http.StatusOK 56 | msg := "" 57 | 58 | h, err := os.Hostname() 59 | if err != nil { 60 | msg = fmt.Sprintf("{\"error\":\"could retrieve hostname: %v\"}", err) 61 | status = http.StatusInternalServerError 62 | } else { 63 | msg = fmt.Sprintf("{\"color\":\"%s\", \"name\":\"%s\"}", hexColorString(), h) 64 | } 65 | 66 | w.Header().Add("Access-Control-Allow-Origin", "*") 67 | w.Header().Set("Content-Type", "application/json") 68 | w.WriteHeader(status) 69 | fmt.Fprint(w, msg) 70 | } 71 | 72 | func hexColorString() string { 73 | rand.Seed(time.Now().UnixNano()) 74 | i := rand.Intn(16777215) // = 0xFFFFFF the highest hex color value allowed. 75 | return "#" + fmt.Sprintf("%06s", strconv.FormatInt(int64(i), 16)) 76 | } 77 | -------------------------------------------------------------------------------- /apps/api/containers/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "strconv" 9 | "testing" 10 | ) 11 | 12 | // Because we are getting random values early tests were flaky. 13 | // Repeating test many times allowed me to see intermittent 14 | // errors 15 | var trials = 1000 16 | 17 | func TestHexColorString(t *testing.T) { 18 | 19 | for i := 0; i < trials; i++ { 20 | 21 | actual := hexColorString() 22 | 23 | if err := validateColor(actual); err != nil { 24 | t.Errorf("%v", err) 25 | } 26 | 27 | } 28 | 29 | } 30 | 31 | func validateColor(s string) error { 32 | if string(s[0]) != "#" { 33 | return fmt.Errorf("Hex color show begin with '#' got %s", string(s[0])) 34 | } 35 | 36 | if len(s) != 7 { 37 | return fmt.Errorf("Hex color should be 7 characters, got %d %s", len(s), s) 38 | } 39 | 40 | num, err := strconv.ParseInt(s[1:6], 16, 32) 41 | if err != nil { 42 | return fmt.Errorf("Error converting result to number: %v", err) 43 | } 44 | 45 | if num < 0 || num > 16777215 { 46 | return fmt.Errorf("Hex should be > 0 (000000) and 16777215 (ffffff) got: %d", num) 47 | } 48 | return nil 49 | } 50 | 51 | func TestHealthHandler(t *testing.T) { 52 | req, err := http.NewRequest("GET", "/healthz", nil) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | rr := httptest.NewRecorder() 58 | handler := http.HandlerFunc(health) 59 | handler.ServeHTTP(rr, req) 60 | 61 | if status := rr.Code; status != http.StatusOK { 62 | t.Errorf("wrong status code: got %v want %v", status, http.StatusOK) 63 | } 64 | 65 | expected := `ok` 66 | if rr.Body.String() != expected { 67 | t.Errorf("unexpected body: got %v want %v", rr.Body.String(), expected) 68 | } 69 | } 70 | 71 | func TestColorHandler(t *testing.T) { 72 | 73 | req, err := http.NewRequest("GET", "/api/color", nil) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | 78 | rr := httptest.NewRecorder() 79 | handler := http.HandlerFunc(color) 80 | handler.ServeHTTP(rr, req) 81 | 82 | if statusGot := rr.Code; statusGot != http.StatusOK { 83 | t.Errorf("wrong status code %s: got %d want %d", "/api/color", statusGot, http.StatusOK) 84 | } 85 | 86 | colorGot := rr.Body.String() 87 | if err := validateColor(colorGot); err != nil { 88 | t.Errorf("Invalid color got: %s, err: %v", colorGot, err) 89 | } 90 | 91 | } 92 | 93 | type result struct { 94 | Color string `json:"color"` 95 | Name string `json:"name"` 96 | } 97 | 98 | func TestColorCompleteHandler(t *testing.T) { 99 | req, err := http.NewRequest("GET", "/api/color-complete", nil) 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | 104 | rr := httptest.NewRecorder() 105 | handler := http.HandlerFunc(colorComplete) 106 | handler.ServeHTTP(rr, req) 107 | 108 | if status := rr.Code; status != http.StatusOK { 109 | t.Errorf("wrong status code: got %v want %v", status, http.StatusOK) 110 | } 111 | 112 | var got result 113 | actual := rr.Body.Bytes() 114 | if err := json.Unmarshal(actual, &got); err != nil { 115 | t.Errorf("could not parse server response: %v", err) 116 | } 117 | 118 | if err := validateColor(got.Color); err != nil { 119 | t.Errorf("Invalid color got: %s, err: %v", got.Color, err) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /apps/api/docker/Makefile: -------------------------------------------------------------------------------- 1 | BASEDIR = $(shell pwd) 2 | 3 | main: 4 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o "$(BASEDIR)/../containers/main" "$(BASEDIR)/../containers/main.go" 5 | 6 | build: main 7 | docker build -t api "$(BASEDIR)/../containers/." 8 | 9 | serve: 10 | docker run --name=api -d -P -p 8080:8080 api 11 | 12 | clean: 13 | -docker stop api 14 | -docker rm api 15 | -docker rmi api -------------------------------------------------------------------------------- /apps/api/kubernetes/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | BASEDIR = $(shell pwd) 15 | 16 | include ../../../Makefile.properties 17 | 18 | app: build deploy 19 | 20 | reset: clean.deployment app 21 | say "app refresh complete" 22 | 23 | reset.safe: env creds 24 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o "$(BASEDIR)/../containers/main" "$(BASEDIR)/../containers/main.go" 25 | gcloud container builds submit "$(BASEDIR)/../containers/." --tag=$(DOCKERREPO)/api 26 | -kubectl delete deployment api-deployment 27 | -kubectl run api-deployment --image=$(DOCKERREPO)/api --replicas=12 --port=8080 --labels=app=api 28 | say "app refresh complete" 29 | 30 | main: 31 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o "$(BASEDIR)/../containers/main" "$(BASEDIR)/../containers/main.go" 32 | 33 | build: env main 34 | gcloud container builds submit "$(BASEDIR)/../containers/." --tag=$(DOCKERREPO)/api 35 | 36 | build.dockerhub: main 37 | docker build "$(BASEDIR)/../containers/." --tag=$(DOCKERREPO):api 38 | docker push $(DOCKERREPO):api 39 | 40 | build.generic: main 41 | docker build "$(BASEDIR)/../containers/." --tag=$(DOCKERREPO)/whackapod-api 42 | docker push $(DOCKERREPO)/whackapod-api 43 | 44 | deploy: env creds deployment service 45 | 46 | deploy.minikube: deployment service 47 | 48 | deploy.minikube.dockerhub: 49 | kubectl run api-deployment --image=$(DOCKERREPO):api --replicas=12 --port=8080 --labels=app=api 50 | kubectl expose deployment api-deployment --name=api --target-port=8080 --type=NodePort --labels="app=api" 51 | 52 | deploy.generic: 53 | kubectl run api-deployment --image=$(DOCKERREPO)/whackapod-api --replicas=12 --port=8080 --labels=app=api 54 | kubectl expose deployment api-deployment --name=api --target-port=8080 --type=NodePort --labels="app=api" 55 | 56 | test: 57 | cd "$(BASEDIR)/../containers" && go test 58 | 59 | deployment: 60 | kubectl run api-deployment --image=$(DOCKERREPO)/api --replicas=12 --port=8080 --labels=app=api 61 | 62 | service: 63 | kubectl expose deployment api-deployment --name=api --target-port=8080 --type=NodePort --labels="app=api" 64 | 65 | clean: env creds clean.deployment clean.service 66 | clean.generic: clean.deployment clean.service 67 | 68 | clean.minikube: clean.deployment clean.service 69 | 70 | clean.deployment: 71 | -kubectl delete deployment api-deployment 72 | 73 | clean.service: 74 | -kubectl delete service api 75 | 76 | retry: clean build deploy 77 | 78 | config: env 79 | @echo Creating API Yaml files based on samples and setting in your Makefile.properties 80 | @cp "$(BASEDIR)/api-deployment.sample.yaml" "$(BASEDIR)/api-deployment.yaml" 81 | $(call rewritefile,"$(BASEDIR)/api-deployment.yaml",%PROJECT%,$(PROJECT)) 82 | 83 | define rewritefile 84 | @sed s/$(2)/$(3)/g <""$(1)"" >"$(BASEDIR)/.temp" 85 | @cp "$(BASEDIR)/.temp" $(1) 86 | @rm "$(BASEDIR)/.temp" 87 | endef 88 | -------------------------------------------------------------------------------- /apps/game/containers/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All Rights Reserved. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | FROM nginx 15 | COPY default.conf /etc/nginx/conf.d 16 | COPY default /usr/share/nginx/html 17 | RUN chmod -R 755 /usr/share/nginx/html 18 | -------------------------------------------------------------------------------- /apps/game/containers/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | server_name localhost; 4 | 5 | 6 | location / { 7 | root /usr/share/nginx/html; 8 | index index.html index.htm; 9 | } 10 | 11 | error_page 500 502 503 504 /50x.html; 12 | location = /50x.html { 13 | root /usr/share/nginx/html; 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /apps/game/containers/default/advanced.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | Whack a Pod! 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 |

32 |
33 |
Service
34 |
35 | 36 | 37 | 38 | kubectl create -f whack-a-pod-deployment.yaml 39 | 40 |
41 | 42 | 43 |
44 | 45 | 74 | 75 | 100 | 101 | 102 |
103 | 104 | 105 | -------------------------------------------------------------------------------- /apps/game/containers/default/assets/audio/countdown.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/audio/countdown.mp3 -------------------------------------------------------------------------------- /apps/game/containers/default/assets/audio/explosion.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/audio/explosion.wav -------------------------------------------------------------------------------- /apps/game/containers/default/assets/audio/license.txt: -------------------------------------------------------------------------------- 1 | Sounds taken from: 2 | https://offers.adobe.com/en/na/audition/offers/audition_dlc/AdobeAuditionDLCSFX.html 3 | 4 | Which states: 5 | Your download and use of the content on this page is governed by and subject to 6 | the Adobe Software License Agreement ("EULA") for Adobe Audition and other 7 | related Adobe software, available at: http://www.adobe.com/products/eulas/ 8 | 9 | Subject to the restrictions stated in the EULA, you may use, display, modify, 10 | reproduce, and distribute the associated Content File. However, you may not 11 | distribute the Content File on a standalone basis (i.e., in circumstances in 12 | which the Content File constitute the primary value of the product being 13 | distributed), and Customer may not claim any trademark rights in the Content 14 | File or derivative works thereof. 15 | 16 | http://www.adobe.com/legal/terms.html 17 | 18 | 2.5 Content Files. “Content Files” means Adobe-provided sample files such as 19 | stock images or sounds. Unless the documentation or specific license associated 20 | with the Content Files state otherwise, you may use, display, modify, reproduce, 21 | and distribute any of the Content Files. However, you may not distribute the 22 | Content Files on a stand-alone basis (i.e., in circumstances in which the 23 | Content Files constitute the primary value of the product being distributed), 24 | and you must not claim any trademark rights in the Content Files or derivative 25 | works of the Content Files. -------------------------------------------------------------------------------- /apps/game/containers/default/assets/audio/pop.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/audio/pop.wav -------------------------------------------------------------------------------- /apps/game/containers/default/assets/audio/startup.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/audio/startup.mp3 -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/alert.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/background.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/background2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/background2.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/bomb_waiting2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/bomb_waiting2.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/cursor.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/cursor_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/cursor_down.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/cursor_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/cursor_inactive.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/cursor_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/cursor_next.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/cursor_next_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/cursor_next_down.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/cursor_next_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/cursor_next_inactive.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/cursordown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/cursordown.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/holder_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/holder_background.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/license.txt: -------------------------------------------------------------------------------- 1 | All images contained herein are licensed using Adobe Stock extended licensing. 2 | 3 | Details are posted here: https://stock.adobe.com/license-terms 4 | 5 | 6 | 7 | Date Author ID Media Type License 8 | 6/20/17, 9:59 PM © wenchiawang #96054428 Image Extended 9 | 6/20/17, 9:55 PM © sjhuls #83208628 Image Extended 10 | 6/20/17, 9:55 PM © Khvost #67401230 Image Extended 11 | 6/20/17, 9:55 PM © Daevid #24047880 Image Extended 12 | 6/20/17, 9:54 PM © studiostoks #104672324 Image Extended -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/main.css: -------------------------------------------------------------------------------- 1 | /*Copyright 2017 Google Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License.*/ 14 | 15 | body{ 16 | background: url("background2.png"); 17 | background-size: cover; 18 | color: #333; 19 | font-family: 'Alegreya Sans SC', sans-serif; 20 | } 21 | .holder{ 22 | padding-top: 250px; 23 | width: 1200px; 24 | margin: 180px auto 5px auto; 25 | background: url("holder_background.png"); 26 | background-size: cover; 27 | border: 10px solid #333; 28 | min-height: 550px; 29 | position: relative; 30 | } 31 | 32 | .holder p{ 33 | margin: 0 50px; 34 | } 35 | 36 | .report{ 37 | position: absolute; 38 | left: 20px; 39 | top: -180px; 40 | width: 600px; 41 | height: 400px; 42 | padding-top: 110px; 43 | transition: color 0.2s ease; 44 | font-family: 'Rye', cursive; 45 | font-size: 2.8rem; 46 | color: #222; 47 | text-align: center; 48 | background: url("sign.png"); 49 | background-size: contain; 50 | background-repeat: no-repeat; 51 | } 52 | .report span{ 53 | margin: 0 100px; 54 | display: block; 55 | } 56 | 57 | .scoreboard{ 58 | width: 300px; 59 | height: 200px; 60 | padding-top: 55px; 61 | font-family: 'Rye', cursive; 62 | font-size: 1.4rem; 63 | color: #222; 64 | text-align: center; 65 | background: url("sign.png"); 66 | background-size: contain; 67 | background-repeat: no-repeat; 68 | position: absolute; 69 | right: 20px; 70 | top: -100px; 71 | } 72 | .scoreboard table{ 73 | margin: 0 60px; 74 | } 75 | .scoreboard th{ 76 | text-align: right; 77 | } 78 | .scoreboard td{ 79 | font-size: 2rem; 80 | } 81 | 82 | .pods{ 83 | cursor: url("cursor_inactive.png"), auto; 84 | margin: 0px 50px 0 50px; 85 | display: block; 86 | position: relative; 87 | z-index: 25; 88 | user-select: none; 89 | } 90 | 91 | 92 | 93 | .pod{ 94 | cursor: url("cursor.png"), pointer; 95 | width: 78px; 96 | height: 83px; 97 | margin: 5px 5px 35px 5px; 98 | background-size: contain; 99 | background-repeat: no-repeat; 100 | display: inline-block; 101 | cursor: url("cursor.png"), pointer; 102 | text-align: center; 103 | vertical-align: bottom; 104 | font-family: 'Alegreya Sans SC', sans-serif; 105 | font-size: 1.5rem; 106 | position: relative; 107 | user-select: none; 108 | } 109 | 110 | .pod span{ 111 | position: absolute; 112 | bottom: -25px; 113 | width: 100%; 114 | display: block; 115 | left: 0; 116 | text-align: center; 117 | 118 | } 119 | 120 | 121 | .pending { 122 | background-image: url("pending.png"); 123 | color:yellow; 124 | } 125 | 126 | .running { 127 | background-image: url("running.png"); 128 | color:#555; 129 | } 130 | 131 | .terminating { 132 | background-image: url("terminating.png"); 133 | color:red; 134 | } 135 | 136 | #bomb{ 137 | width: 200px; 138 | height: 199px; 139 | } 140 | 141 | .bombpanel{ 142 | display:none; 143 | position: absolute; 144 | right: 0; 145 | top: 50%; 146 | width: 50%; 147 | } 148 | 149 | .bombpanel p{ 150 | position: absolute; 151 | top: 40px; 152 | right:0; 153 | width: 60%; 154 | font-size: 2rem; 155 | } 156 | 157 | .alert{ 158 | position: absolute; 159 | right:15%; 160 | top: -15%; 161 | width: 50%; 162 | background: url("alert.png"); 163 | background-size: cover; 164 | background-repeat: no-repeat; 165 | font-family: 'Rye', cursive; 166 | font-size: 1.8rem; 167 | width: 571px; 168 | height: 526px; 169 | margin: 5px auto; 170 | text-align: center; 171 | color: #444; 172 | z-index: 50; 173 | display: none; 174 | } 175 | 176 | .alert .msg{ 177 | position: absolute; 178 | top: 180px; 179 | margin: 0 140px 0 140px; 180 | } 181 | 182 | .overlay{ 183 | font-family: 'Source Code Pro', monospace; 184 | font-size: 14px; 185 | background-color: #ccc; 186 | border: 1px solid red; 187 | display: none; 188 | } 189 | 190 | .overlay p{ 191 | margin: 10px; 192 | padding: 10px; 193 | } 194 | 195 | .overlay table{ 196 | margin: 10px; 197 | padding: 10px; 198 | } 199 | 200 | 201 | .overlay th{ 202 | text-align: left; 203 | font-weight: 500; 204 | } 205 | 206 | .overlay th, .overlay td{ 207 | padding: 2px 4px; 208 | } 209 | 210 | .overlay td.number{ 211 | text-align: right; 212 | font-weight: 900; 213 | } 214 | 215 | 216 | .overlay-report{ 217 | position:absolute; 218 | top: -180px; 219 | left: 40px; 220 | width: 550px; 221 | } 222 | 223 | .overlay-pods{ 224 | position: absolute; 225 | width: 620px; 226 | right: 20px; 227 | top: 110px; 228 | 229 | } 230 | 231 | .overlay-scoreboard{ 232 | position: absolute; 233 | width: 320px; 234 | right: 20px; 235 | top: -180px; 236 | 237 | } 238 | 239 | .explain{ 240 | position: fixed; 241 | right: 0; 242 | top: 0; 243 | margin: 5px; 244 | padding: 5px; 245 | background-color: #ccc; 246 | border: 1px solid red; 247 | opacity: 0.5; 248 | cursor: pointer; 249 | 250 | } 251 | 252 | .explain-show{ 253 | background-color: #fff; 254 | border: 1px solid red; 255 | opacity: 1; 256 | 257 | } 258 | 259 | .pods-table{ 260 | margin: 0px 50px 0 50px; 261 | cursor: url("cursor_inactive.png"), auto; 262 | } 263 | 264 | .modal{ 265 | min-height: 100%; 266 | width: 100%; 267 | background-color: rgba(66,66,66,.5); 268 | position: absolute; 269 | top: 0; 270 | left: 0; 271 | display: none; 272 | z-index: 98; 273 | } 274 | 275 | .modal-message{ 276 | font-family: 'Source Code Pro', monospace; 277 | height: 300px; 278 | width: 600px; 279 | background-color: #fff; 280 | margin: 200px auto; 281 | padding: 20px; 282 | border: 3px solid #333; 283 | z-index: 99; 284 | } 285 | 286 | .modal-message code{ 287 | background-color: #000; 288 | color: #0f0; 289 | display: block; 290 | margin: 3px 0; 291 | padding: 2px; 292 | border: 1px solid #999; 293 | } 294 | 295 | -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/next.css: -------------------------------------------------------------------------------- 1 | /*Copyright 2017 Google Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License.*/ 14 | 15 | body{ 16 | color: #333; 17 | font-family: 'Roboto', sans-serif; 18 | font-weight: 300; 19 | } 20 | 21 | 22 | .holder{ 23 | padding-top: 250px; 24 | width: 60%; 25 | margin: 50px 0 5px 0; 26 | background-size: cover; 27 | position: relative; 28 | border-radius: 4px; 29 | overflow: 0; 30 | } 31 | 32 | .holder p{ 33 | margin: 0 25px; 34 | } 35 | 36 | .holder p:first-of-type{ 37 | margin-top: -90px; 38 | } 39 | 40 | 41 | .report{ 42 | position: absolute; 43 | left: 70px; 44 | top: -40px; 45 | width: 150px; 46 | height: 135px; 47 | padding-top: 140px; 48 | transition: color 0.2s ease; 49 | -webkit-transition: -webkit-filter 0.2s ease; 50 | color: #222; 51 | text-align: center; 52 | background: url("service_inactive.png"); 53 | background-size: contain; 54 | background-repeat: no-repeat; 55 | -webkit-filter: drop-shadow(2px 2px 3px #333); 56 | } 57 | 58 | #report_message{ 59 | position: absolute; 60 | top: 120px; 61 | left: 45px; 62 | font-weight: 700; 63 | } 64 | 65 | .report span{ 66 | font-weight: 900; 67 | display: block; 68 | width: 300px; 69 | position: relative; 70 | top: 15px; 71 | left: -75px; 72 | } 73 | 74 | .service_up{ 75 | background: url("service_up.png"); 76 | background-size: contain; 77 | background-repeat: no-repeat; 78 | } 79 | 80 | .service_down{ 81 | background: url("service_down.png"); 82 | background-size: contain; 83 | background-repeat: no-repeat; 84 | } 85 | 86 | 87 | 88 | .scoreboard{ 89 | width: 200px; 90 | height: 100px; 91 | padding-top: 25px; 92 | color: #222; 93 | text-align: center; 94 | position: absolute; 95 | right: 20px; 96 | top: -35px; 97 | border-radius: 2px; 98 | 99 | } 100 | .scoreboard table{ 101 | margin: 0 60px; 102 | } 103 | .scoreboard th{ 104 | text-align: right; 105 | } 106 | .scoreboard td{ 107 | font-size: 2rem; 108 | } 109 | 110 | 111 | 112 | 113 | .pod{ 114 | cursor: url("cursor_next.png"), pointer; 115 | width: 80px; 116 | height: 80px; 117 | margin: 5px 5px 35px 5px; 118 | background-color: #000000; 119 | color: #fff; 120 | display: inline-block; 121 | text-align: center; 122 | vertical-align: bottom; 123 | position: relative; 124 | user-select: none; 125 | border-radius: 50px; 126 | border: 5px solid #ccc; 127 | transition: border-color .5s, border-width .5s, -webkit-filter 0.5s ; 128 | } 129 | 130 | .pod span{ 131 | position: relative; 132 | top: 30px; 133 | width: 100%; 134 | display: block; 135 | left: 0; 136 | text-align: center; 137 | font-weight: 700; 138 | } 139 | 140 | .pods-table{ 141 | margin: -110px 50px 0 50px; 142 | width: 380px; 143 | cursor: url("cursor_next_inactive.png"), auto; 144 | position: relative; 145 | } 146 | 147 | 148 | 149 | .pending { 150 | background-color: #fabb05; 151 | cursor: url("cursor_next_inactive.png"), auto; 152 | -webkit-filter: drop-shadow(1px 1px 1px rgba(66,66,66,.8)); 153 | } 154 | 155 | .running { 156 | background-color:#0f9058; 157 | cursor: url("cursor_next.png"), auto; 158 | -webkit-filter: drop-shadow(4px 4px 5px rgba(66,66,66,.5)); 159 | } 160 | 161 | .hit { 162 | -webkit-filter: drop-shadow(1px 1px 1px rgba(66,66,66,.8)); 163 | cursor: url("cursor_next_down.png"), auto; 164 | } 165 | 166 | .running:active { 167 | -webkit-filter: drop-shadow(1px 1px 1px rgba(66,66,66,.8)); 168 | cursor: url("cursor_next_down.png"), auto; 169 | } 170 | 171 | .terminating { 172 | background-color: #ccc; 173 | cursor: url("cursor_next_inactive.png"), auto; 174 | -webkit-filter: drop-shadow(1px 1px 1px rgba(66,66,66,.8)); 175 | } 176 | 177 | .pods-explain{ 178 | position: absolute; 179 | top: 430px; 180 | left: 500px; 181 | display: none; 182 | } 183 | 184 | th.pending, th.running, th.terminating{ 185 | color: #fff; 186 | padding: 5px 10px; 187 | -webkit-filter: none; 188 | } 189 | 190 | th.pending{ 191 | background-color: #fabb05; 192 | } 193 | th.running{ 194 | background-color:#0f9058; 195 | } 196 | th.terminating{ 197 | background-color: #ea4335; 198 | } 199 | 200 | #bomb{ 201 | top: 40px; 202 | width: 330px; 203 | height: 311px; 204 | position: relative; 205 | margin: 0 auto; 206 | -webkit-animation-name: orangeShadowPulse; 207 | -webkit-animation-duration: 1s; 208 | -webkit-animation-iteration-count: infinite; 209 | } 210 | 211 | @-webkit-keyframes orangeShadowPulse { 212 | from { -webkit-filter: drop-shadow(3px 3px 5px #E17C26); } 213 | 50% { -webkit-filter: drop-shadow(3px 3px 5px #FABB05); } 214 | to { -webkit-filter: drop-shadow(3px 3px 5px #E17C26); } 215 | } 216 | 217 | @-webkit-keyframes orangeTextPulse { 218 | from { color: #E17C26; } 219 | 50% { color: #FABB05; } 220 | to { color: #E17C26; } 221 | } 222 | 223 | .bombpanel{ 224 | display:none; 225 | position: absolute; 226 | right: -10%; 227 | top: 30px; 228 | width: 400px; 229 | } 230 | 231 | .bombpanel p{ 232 | position: relative; 233 | text-align: center; 234 | left:40px; 235 | width: 50%; 236 | top: 40px; 237 | font-size: 1.2rem; 238 | font-weight: 900; 239 | font-style: italic; 240 | -webkit-animation-name: orangeTextPulse; 241 | -webkit-animation-duration: 1s; 242 | -webkit-animation-iteration-count: infinite; 243 | } 244 | 245 | .modal{ 246 | min-height: 100%; 247 | width: 100%; 248 | background-color: rgba(66,66,66,.5); 249 | position: absolute; 250 | top: 0; 251 | left: 0; 252 | display: none; 253 | z-index: 98; 254 | } 255 | 256 | .modal-message{ 257 | font-weight: 300; 258 | width: 500px; 259 | background-color: #fff; 260 | margin: 100px auto; 261 | padding: 20px; 262 | border: 3px solid #333; 263 | z-index: 99; 264 | box-shadow: 3px 3px 5px rgba(0,0,0,.5); 265 | border-radius: 4px; 266 | position: relative; 267 | } 268 | 269 | code{ 270 | background-color: #000; 271 | color: #0f0; 272 | display: block; 273 | margin: 3px 0; 274 | padding: 2px; 275 | border: 1px solid #999; 276 | } 277 | 278 | .overlay{ 279 | font-family: 'Source Code Pro', monospace; 280 | font-size: 14px; 281 | background-color: #ccc; 282 | border: 1px solid red; 283 | display: none; 284 | } 285 | 286 | .overlay p{ 287 | margin: 10px; 288 | padding: 10px; 289 | } 290 | 291 | .overlay table{ 292 | margin: 10px; 293 | padding: 10px; 294 | } 295 | 296 | 297 | .overlay th{ 298 | text-align: left; 299 | font-weight: 500; 300 | } 301 | 302 | .overlay th, .overlay td{ 303 | padding: 2px 4px; 304 | } 305 | 306 | .overlay td.number{ 307 | text-align: right; 308 | font-weight: 900; 309 | } 310 | 311 | 312 | .overlay-report{ 313 | position:absolute; 314 | top: -180px; 315 | left: 40px; 316 | width: 550px; 317 | } 318 | 319 | .overlay-pods{ 320 | position: absolute; 321 | width: 620px; 322 | right: 20px; 323 | top: 110px; 324 | 325 | } 326 | 327 | .overlay-scoreboard{ 328 | position: absolute; 329 | width: 320px; 330 | right: 20px; 331 | top: -180px; 332 | 333 | } 334 | 335 | 336 | .alert{ 337 | position: absolute; 338 | right: 10px; 339 | top: 70px; 340 | width: 360px; 341 | height: 30px; 342 | margin: 5px auto; 343 | text-align: center; 344 | color: #444; 345 | z-index: 50; 346 | display: none; 347 | border: 2px solid #4285F4; 348 | background-color: #f0f0f0; 349 | box-shadow: 3px 3px 5px rgba(66,66,66,.5); 350 | border-radius: 4px; 351 | font-weight: 900; 352 | font-size: 1.3rem; 353 | padding: 20px; 354 | 355 | } 356 | 357 | 358 | .totals{ 359 | font-weight: 700; 360 | } 361 | 362 | #logwindow{ 363 | width: 40%; 364 | margin: 0; 365 | position: absolute; 366 | right: 0; 367 | top: 0; 368 | height: 100%; 369 | background-color: #000000; 370 | color: #0f0; 371 | border-radius: 4px; 372 | border: 1px solid mediumslateblue; 373 | overflow: hidden; 374 | font-size: .5rem; 375 | } 376 | 377 | pre{ 378 | font-family: 'Source Code Pro', monospace; 379 | } 380 | 381 | pre .phase{ 382 | font-weight: 900; 383 | background-color: #0f0; 384 | color: #000; 385 | } 386 | 387 | button{ 388 | margin : 0; 389 | padding : 0; 390 | border : 0; 391 | background : transparent; 392 | font-family : inherit; 393 | font-size : 1em; 394 | cursor : pointer; 395 | } 396 | 397 | button::-moz-focus-inner{ 398 | padding : 0; 399 | border : 0; 400 | } 401 | 402 | button:focus {outline:0;} 403 | 404 | button{ 405 | cursor: url("cursor_next.png"), pointer; 406 | width: 80px; 407 | height: 80px; 408 | margin: 5px 5px 5px 5px; 409 | background-color: #000000; 410 | color: #fff; 411 | display: inline-block; 412 | text-align: center; 413 | vertical-align: bottom; 414 | position: relative; 415 | user-select: none; 416 | border-radius: 50px; 417 | border: 5px solid #ddd; 418 | background-color: #4285F4; 419 | position: relative; 420 | } 421 | 422 | button#deploy-end{ 423 | background-color: #ea4335; 424 | } 425 | 426 | button.small{ 427 | cursor: pointer; 428 | position: absolute; 429 | right: -18px; 430 | top: -30px; 431 | width: 50px; 432 | height: 50px; 433 | margin: 5px 5px 5px 5px; 434 | background-color: #ea4335; 435 | } 436 | 437 | .modal button.small{ 438 | cursor: pointer; 439 | position: absolute; 440 | right: -15px; 441 | top:-15px; 442 | width: 60px; 443 | height: 60px; 444 | margin: 5px 5px 5px 5px; 445 | background-color: #ea4335; 446 | } 447 | 448 | button.small.reset{ 449 | background-color: #4285F4; 450 | } 451 | 452 | #nodes{ 453 | display: table; 454 | border-spacing: 15px; 455 | } 456 | 457 | .node{ 458 | border: 5px solid #ddd; 459 | border-radius: 5px; 460 | position: relative; 461 | margin-bottom: 5px; 462 | width: 45%; 463 | display: inline-block; 464 | vertical-align: top; 465 | margin-right: 20px; 466 | position:relative; 467 | padding-bottom: 100px; 468 | display: table-cell; 469 | } 470 | 471 | .node p { 472 | position: relative; 473 | top: 100px; 474 | } 475 | 476 | 477 | .node .pod{ 478 | top: 100px; 479 | } 480 | 481 | #endpoint{ 482 | position: absolute; 483 | left: 250px; 484 | top: 20px; 485 | width: 450px; 486 | } 487 | 488 | #service{ 489 | border: 5px dotted #ddd; 490 | border-radius: 5px; 491 | position: relative; 492 | top: -230px; 493 | height: 160px; 494 | width: 92%; 495 | margin-bottom: -220px; 496 | 497 | } 498 | 499 | #deploy-end{ 500 | display: none; 501 | } 502 | 503 | .kube-label{ 504 | font-family: 'Source Code Pro', monospace; 505 | position: absolute; 506 | bottom: 2px; 507 | right: 2px; 508 | } 509 | 510 | .pod .kube-label{ 511 | bottom: 2px; 512 | right: 25px; 513 | font-size: 10px; 514 | } 515 | 516 | 517 | dl{ 518 | font-size: 18px; 519 | } 520 | dt{ 521 | font-weight: bold; 522 | font-family: 'Source Code Pro', monospace; 523 | 524 | } 525 | dd{ 526 | width: 700px; 527 | margin-bottom: 5px; 528 | } 529 | 530 | .responder{ 531 | border-color: #FABB05; 532 | animation: pulse 2s infinite; 533 | -webkit-filter: drop-shadow(4px 4px 5px rgba(33,33,33,.8)); 534 | } 535 | 536 | 537 | @keyframes pulse { 538 | 0% { 539 | border-color: #FABB05; 540 | } 541 | 50% { 542 | border-color: #FFFFFF; 543 | } 544 | 100% { 545 | border-color: #FABB05; 546 | } 547 | 548 | } 549 | 550 | 551 | nav a{ 552 | position: fixed; 553 | display: block; 554 | top: 10px; 555 | right: 41%; 556 | z-index: 100; 557 | border: 1px solid #ccc; 558 | background-color: #333; 559 | padding: 3px 10px; 560 | color: #FFF; 561 | } 562 | 563 | #deployment{ 564 | width: 500px; 565 | display: inline-block; 566 | vertical-align: middle; 567 | margin-top: -60px; 568 | } 569 | 570 | .text-reveal{ 571 | height: auto; 572 | width: auto; 573 | border-radius: 8px; 574 | padding: 10px 20px; 575 | cursor: pointer; 576 | border-width: 3px; 577 | } -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/pending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/pending.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/pending_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/pending_next.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/running.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/running_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/running_next.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/service_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/service_down.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/service_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/service_inactive.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/service_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/service_up.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/sign.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/terminating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/terminating.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/css/terminating_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/css/terminating_next.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/img/bomb_explode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/img/bomb_explode.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/img/bomb_explode_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/img/bomb_explode_next.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/img/bomb_waiting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/img/bomb_waiting.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/img/bomb_waiting_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/apps/game/containers/default/assets/img/bomb_waiting_next.png -------------------------------------------------------------------------------- /apps/game/containers/default/assets/img/license.txt: -------------------------------------------------------------------------------- 1 | All images contained herein are licensed using Adobe Stock extended licensing. 2 | 3 | Details are posted here: https://stock.adobe.com/license-terms 4 | 5 | 6 | 7 | Date Author ID Media Type License 8 | 6/20/17, 9:59 PM © wenchiawang #96054428 Image Extended 9 | 6/20/17, 9:55 PM © sjhuls #83208628 Image Extended 10 | 6/20/17, 9:55 PM © Khvost #67401230 Image Extended 11 | 6/20/17, 9:55 PM © Daevid #24047880 Image Extended 12 | 6/20/17, 9:54 PM © studiostoks #104672324 Image Extended -------------------------------------------------------------------------------- /apps/game/containers/default/assets/js/advanced.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | var api = new API(servicehost); 16 | var logwindow = new LOGWINDOW(); 17 | var deploymentAPI = new DEPLOYMENTAPI(adminhost, logwindow); 18 | var pods = new PODS(); 19 | var game = new GAME(); 20 | var score = new SCORE(); 21 | var nodes = []; 22 | var pods_shown = []; 23 | var fails_threshold = 20; 24 | 25 | 26 | document.addEventListener('DOMContentLoaded', function() { 27 | api.timeout = 10000; 28 | deploymentAPI.Delete(); 29 | deploymentAPI.ResetNodes(); 30 | setReport(""); 31 | $("#deploy-start").click(startDeployment); 32 | $("#deploy-end").click(endDeployment); 33 | $("#endpoint").html(api.URL()); 34 | $("#show-pod-yaml").click(showPodModal); 35 | $("#close-pod-modal").click(hidePodModal); 36 | $("#show-service-yaml").click(showServiceModal); 37 | $("#close-service-modal").click(hideServiceModal); 38 | }); 39 | 40 | function showModal(id){ 41 | var modal = $(id); 42 | modal.fadeIn('slow'); 43 | } 44 | 45 | function hideModal(id){ 46 | var modal = $(id); 47 | modal.fadeOut(); 48 | } 49 | 50 | function showPodModal(e){ 51 | showModal("#pod-yaml"); 52 | } 53 | 54 | function hidePodModal(e){ 55 | hideModal("#pod-yaml"); 56 | } 57 | 58 | function showServiceModal(e){ 59 | showModal("#service-yaml"); 60 | } 61 | 62 | function hideServiceModal(e){ 63 | hideModal("#service-yaml"); 64 | } 65 | 66 | function setReport(msg, color){ 67 | if (typeof color == "undefined") color = "#333333"; 68 | var report = $(".report"); 69 | 70 | report.css("-webkit-filter", "drop-shadow(2px 2px 3px " + color + ")"); 71 | report.css("color", color); 72 | var msgholder = $("#report_message"); 73 | if (msgholder.length == 0){ 74 | report.after('
' + msg + '
') 75 | } else { 76 | msgholder.html(msg); 77 | } 78 | 79 | 80 | if(game.GetState() == "running"){ 81 | if(game.IsServiceDown()) { 82 | $(".report").addClass("service_down"); 83 | $(".report").removeClass("service_up"); 84 | } else{ 85 | $(".report").addClass("service_up"); 86 | $(".report").removeClass("service_down"); 87 | } 88 | } 89 | 90 | } 91 | 92 | function startDeployment(){ 93 | $("#deploy-start").hide(); 94 | $("#deploy-start").css('z-index', 1); 95 | $("#deploy-end").show(); 96 | $("#deploy-end").css('z-index', 300); 97 | deploymentAPI.Create(initGame,genericError); 98 | setReport(""); 99 | $("#deployment").html("kubectl delete -f whack-a-pod-deployment.yaml") 100 | } 101 | 102 | function initGame(e){ 103 | game.Start(getColor, showScore, getPods, getTimeLeft); 104 | logwindow.Log(e); 105 | 106 | } 107 | 108 | function getColor(){ 109 | api.ColorComplete(handleColor, handleColorError) 110 | } 111 | 112 | function handleColor(e){ 113 | if (game.GetState() == "started"){ 114 | game.Init(); 115 | setTimeout(function(){$(".pods-explain").fadeOut();}, 3000); 116 | } 117 | 118 | if (game.GetState() == "running"){ 119 | game.SetServiceUp(); 120 | setReport("Service call result: "+ e.color, e.color); 121 | $(".responder").removeClass("responder"); 122 | $("#"+e.name).addClass("responder"); 123 | 124 | } 125 | if (api.ResetFails()){ 126 | console.log("Soft service has recovered."); 127 | } 128 | } 129 | 130 | function handleColorError(e,textStatus, errorThrown){ 131 | if (game.GetState() == "running") { 132 | if (api.IsHardFail()){ 133 | console.log("Hard service fail."); 134 | setReport("Kubernetes service is DOWN!", "#FF0000"); 135 | $(".responder").removeClass("responder"); 136 | alertYouKilledIt(); 137 | } else { 138 | console.log("Soft service fail. Retry"); 139 | } 140 | } 141 | } 142 | 143 | function getPods(){ 144 | deploymentAPI.Get(handlePods, handlePodsError); 145 | } 146 | 147 | function handlePods(e,b){ 148 | 149 | if (game.GetState() == "done") { 150 | return; 151 | } 152 | 153 | drawPods(e); 154 | 155 | } 156 | 157 | function drawPods(pods){ 158 | 159 | var pods_active = []; 160 | //create node UI if it doesn't exist 161 | for (var i = 0; i < pods.items.length; i++){ 162 | var pod = new POD(pods.items[i]); 163 | logwindow.Log((pod)); 164 | 165 | if ((pod.host == null) || (pod.host.length == 0)){ 166 | continue; 167 | } 168 | 169 | pods_active.push(pod.name); 170 | 171 | if (nodes.indexOf(pod.host) < 0){ 172 | nodes.push(pod.host); 173 | createNodeUI(pod.host); 174 | } 175 | 176 | if (pods_shown.indexOf(pod.name) < 0){ 177 | pods_shown.push(pod.name); 178 | createPodUI(pod); 179 | } 180 | 181 | var $pod = $("#"+ pod.name); 182 | $pod.addClass(pod.phase); 183 | 184 | if (pod.phase == "running") { 185 | if (!$pod.hasClass("bound")){ 186 | $pod.click(whackHandler); 187 | $pod.addClass("bound"); 188 | } 189 | } else{ 190 | $pod.click(); 191 | } 192 | 193 | if (pod.ShouldRemove()){ 194 | $pod.remove(); 195 | } 196 | 197 | } 198 | for (var i = 0; i < pods_shown.length; i++){ 199 | if (pods_active.indexOf(pods_shown[i]) < 0){ 200 | $("#"+ pods_shown[i]).remove(); 201 | } 202 | } 203 | 204 | } 205 | 206 | 207 | function createPodUI(pod){ 208 | var hostID = "node_"+pod.host; 209 | 210 | var div = document.createElement("div"); 211 | div.id = pod.name; 212 | div.dataset.selflink = pod.selflink; 213 | div.classList.add("pod"); 214 | 215 | var span = document.createElement("span"); 216 | span.innerHTML = pod.shortname; 217 | span.dataset.selflink = pod.selflink; 218 | div.append(span); 219 | 220 | var label = document.createElement("div"); 221 | label.classList.add("kube-label"); 222 | label.classList.add("kube-pod"); 223 | label.innerHTML= "Pod"; 224 | div.append(label); 225 | 226 | 227 | $("#pod-" + pod.holder).append(div); 228 | $("#"+ hostID).append(div); 229 | 230 | // logwindow.Log((pod)); 231 | 232 | } 233 | 234 | 235 | function createNodeUI(name){ 236 | var id = "node_"+name; 237 | var label = name.split("-")[name.split("-").length -1 ]; 238 | var $div = $('

Node: ' + label + '

Node
'); 239 | var $holder = $("#nodes"); 240 | $div.appendTo("#nodes") 241 | $killbtn = $("#kill-" + id) 242 | 243 | if(name=="minikube"){ 244 | $killbtn.hide(); 245 | } else{ 246 | $killbtn.click(killNode); 247 | $killbtn.data("node", name); 248 | } 249 | 250 | } 251 | 252 | function killNode(e){ 253 | var $killbtn = $("#" + e.currentTarget.id); 254 | var node = $killbtn.data("node"); 255 | deploymentAPI.DrainNode(node); 256 | $killbtn.click(resetNode); 257 | $killbtn.addClass("reset"); 258 | $killbtn.text("+"); 259 | } 260 | 261 | 262 | function resetNode(e){ 263 | var $killbtn = $("#" + e.currentTarget.id); 264 | var node = $killbtn.data("node"); 265 | deploymentAPI.UncordonNode(node); 266 | $killbtn.removeClass("reset"); 267 | $killbtn.text("X"); 268 | $killbtn.click(killNode); 269 | 270 | } 271 | 272 | function handlePodsError(e){ 273 | // $(".pods").html(""); 274 | if (typeof e != "string"){ 275 | console.log("Error getting pods:", e.statusText); 276 | } else{ 277 | console.log("Error getting pods:", e); 278 | } 279 | 280 | } 281 | 282 | function genericError(e){ 283 | console.log("Error: ", e); 284 | } 285 | 286 | function showScore(){ 287 | } 288 | 289 | function getTimeLeft(){ 290 | } 291 | 292 | function alertYouKilledIt(){ 293 | if (!game.IsServiceDown() && game.GetState() == "running"){ 294 | console.log("Killed it."); 295 | game.SetServiceDown(); 296 | score.KnockDown() 297 | } 298 | } 299 | 300 | 301 | function whackHandler(e){ 302 | if (e.target.id == ""){ 303 | $("#" + e.target.parentNode.id ).addClass("terminating"); 304 | } else{ 305 | $("#" + e.target.id ).addClass("terminating"); 306 | } 307 | 308 | killPod(e.target.dataset.selflink) 309 | } 310 | 311 | 312 | function killPod(selflink){ 313 | deploymentAPI.DeletePod(selflink, killHandler, podError); 314 | } 315 | 316 | function killHandler(e){ 317 | logwindow.Log(e); 318 | } 319 | 320 | function podError(e){ 321 | 322 | // console.log("Pod already gone? :", e); 323 | } 324 | 325 | function endDeployment(){ 326 | deploymentAPI.Delete(); 327 | game.Stop(); 328 | setReport(""); 329 | location.reload(); 330 | } 331 | -------------------------------------------------------------------------------- /apps/game/containers/default/assets/js/lib.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | var servicehost = ""; 16 | var adminhost = ""; 17 | 18 | 19 | function SCORE(){ 20 | var total = 0; 21 | var pods = 0; 22 | var knockdowns = 0; 23 | 24 | this.KnockDown = function(){ 25 | knockdowns++; 26 | total += 100; 27 | }; 28 | 29 | this.KillPod = function(){ 30 | pods++; 31 | total++; 32 | }; 33 | 34 | this.GetTotal = function(){ 35 | return total; 36 | }; 37 | 38 | this.GetPods = function(){ 39 | return pods; 40 | }; 41 | 42 | this.GetKnockDowns = function(){ 43 | return knockdowns; 44 | }; 45 | 46 | } 47 | 48 | function PODS(){ 49 | this.max = 12; 50 | var podsArray = []; 51 | var DoNotReAdd = new Object(); 52 | 53 | var initPods = function(){ 54 | podsArray = []; 55 | for (var i = 0; i < this.max; i++){ 56 | podsArray[i] = ""; 57 | } 58 | }; 59 | 60 | this.SetMax = function(max){ 61 | this.max = max; 62 | initPods(); 63 | }; 64 | initPods(); 65 | 66 | this.FindEmpty = function(){ 67 | for (var i = 0; i < this.max; i++){ 68 | if (typeof podsArray[i] !== 'object'){ 69 | return i; 70 | } 71 | } 72 | return -1; 73 | }; 74 | 75 | this.IsPodPresent = function(name){ 76 | for (var i = 0; i < podsArray.length; i++){ 77 | if (typeof podsArray[i] !== 'object'){ 78 | continue; 79 | } 80 | 81 | if(name == podsArray[i].name){ 82 | return true; 83 | } 84 | } 85 | return false; 86 | }; 87 | 88 | this.Get = function(input){ 89 | if (typeof input == "string") { 90 | var name = input; 91 | for (var i = 0; i < podsArray.length; i++){ 92 | if (typeof podsArray[i] !== 'object'){ 93 | continue; 94 | } 95 | 96 | if(name == podsArray[i].name){ 97 | return podsArray[i]; 98 | } 99 | } 100 | } 101 | 102 | if (typeof input == "number") { 103 | return podsArray[input]; 104 | } 105 | 106 | return ; 107 | }; 108 | 109 | this.Set = function(pod){ 110 | if (DoNotReAdd.hasOwnProperty('pod.name')){ 111 | return; 112 | } 113 | podsArray[pod.holder] = pod; 114 | }; 115 | 116 | this.Delete = function(pod){ 117 | DoNotReAdd[pod.name] = true; 118 | podsArray[pod.holder] = ""; 119 | }; 120 | 121 | this.Count = function(){ 122 | return podsArray.length; 123 | }; 124 | 125 | this.Add = function(json){ 126 | var name = json.metadata.name; 127 | var pod = new POD(json); 128 | 129 | if (this.IsPodPresent(name)){ 130 | pod = this.Get(name); 131 | } else{ 132 | var target = this.FindEmpty(); 133 | pod.holder = target; 134 | } 135 | 136 | pod.SetPhase(json); 137 | 138 | if (pod.holder != -1){ 139 | this.Set(pod); 140 | } 141 | } 142 | 143 | 144 | 145 | } 146 | 147 | function NODE(json){ 148 | this.name = json.metadata.name; 149 | this.selflink = json.metadata.selfLink; 150 | this.type = "Node"; 151 | this.status = "Ready"; 152 | 153 | if (typeof json.spec.unschedulable != "undefined"){ 154 | this.status = "Ready,SchedulingDisabled"; 155 | } 156 | 157 | 158 | this.SetShortName = function(){ 159 | var nodenameArr = this.name.split("-"); 160 | this.shortname = nodenameArr[nodenameArr.length-1]; 161 | } 162 | 163 | this.SetShortName(); 164 | } 165 | 166 | 167 | function POD(json){ 168 | 169 | this.selflink = json.metadata.selfLink; 170 | this.type = "Pod"; 171 | this.terminateThreshold = 1000; 172 | this.phase = ""; 173 | this.holder = ""; 174 | this.shortname = ""; 175 | 176 | if (typeof json.spec != "undefined" && typeof json.spec.nodeName != "undefined"){ 177 | this.host = json.spec.nodeName; 178 | } 179 | 180 | if (typeof json.metadata.name != "undefined"){ 181 | this.name = json.metadata.name; 182 | } 183 | 184 | if (typeof json.status != "undefined"){ 185 | this.hostIP = json.status.hostIP; 186 | } 187 | 188 | 189 | if (typeof this.host == "undefined"){ 190 | this.host = ""; 191 | } 192 | 193 | this.phase =""; 194 | this.startTerminate =""; 195 | 196 | this.SetShortName = function(){ 197 | var nodenameArr = this.name.split("-"); 198 | this.shortname = nodenameArr[nodenameArr.length-1]; 199 | } 200 | 201 | this.ShouldRemove = function(){ 202 | if (this.phase == "terminating"){ 203 | var now = new Date(); 204 | if ( now - this.startTerminate > this.terminateThreshold){ 205 | return true; 206 | } 207 | } 208 | return false; 209 | } 210 | 211 | this.SetPhase = function(json){ 212 | var podPhase = json.status.phase ? json.status.phase.toLowerCase() : ''; 213 | this.phase = podPhase; 214 | 215 | if ((podPhase != "terminating") && (typeof json.metadata.deletionTimestamp != "undefined")) { 216 | this.phase = "terminating"; 217 | this.startTerminate = new Date(); 218 | } 219 | } 220 | this.SetShortName(); 221 | this.SetPhase(json); 222 | } 223 | 224 | function PODSUI(pods, logwindow){ 225 | var pods = pods; 226 | if (typeof(logwindow)==='undefined') logwindow = new LOGWINDOW(); 227 | 228 | var alreadyShown = new Object(); 229 | alreadyShown.terminating = new Object(); 230 | 231 | this.ClearTerminating = function(){ 232 | for (var i = 0; i < pods.Count(); i++){ 233 | var podObj = pods.Get(i); 234 | if (podObj.ShouldRemove() ){ 235 | pods.Delete(podObj); 236 | var poddiv = document.getElementById(podObj.name); 237 | if (poddiv != null){ 238 | poddiv.parentNode.removeChild(poddiv); 239 | } 240 | 241 | } 242 | } 243 | } 244 | 245 | this.ClearMissing = function(podNames){ 246 | var podsDOM = document.querySelectorAll('.pod'), i; 247 | for (i = 0; i < podsDOM.length; ++i) { 248 | if (podNames.lastIndexOf(podsDOM[i].id) < 0){ 249 | pods.Delete(podsDOM[i].id); 250 | //TODO: uncomment. 251 | podsDOM[i].parentNode.removeChild(podsDOM[i]); 252 | } 253 | } 254 | } 255 | 256 | this.ClearAll = function(){ 257 | for (var i = 0; i < pods.Count(); i++){ 258 | var podObj = pods.Get(i); 259 | var poddiv = document.getElementById(podObj.name); 260 | if (poddiv){ 261 | poddiv.parentNode.removeChild(poddiv); 262 | } 263 | } 264 | } 265 | 266 | this.AddPod = function(pod, hitHandler){ 267 | 268 | var div = document.getElementById(pod.name); 269 | 270 | if (!div){ 271 | div = document.createElement("div"); 272 | div.id = pod.name; 273 | div.dataset.selflink = pod.selflink; 274 | div.classList.add("pod"); 275 | var span = document.createElement("span"); 276 | span.innerHTML = pod.shortname; 277 | span.dataset.selflink = pod.selflink; 278 | div.append(span); 279 | $("#pod-" + pod.holder).append(div); 280 | logwindow.Log(pod); 281 | } 282 | 283 | div.classList.add(pod.phase); 284 | 285 | if (pod.phase == "running"){ 286 | div.addEventListener("click", hitHandler); 287 | } else{ 288 | div.removeEventListener("click", hitHandler); 289 | } 290 | 291 | } 292 | 293 | this.DrawPods = function(json, whackHandler){ 294 | 295 | var podNames = []; 296 | for (var i = 0; i < json.items.length; i++){ 297 | podNames.push(json.items[i].metadata.name); 298 | } 299 | 300 | this.ClearTerminating(); 301 | this.ClearMissing(podNames); 302 | 303 | for (var i = 0; i < json.items.length; i++){ 304 | pods.Add(json.items[i]); 305 | } 306 | 307 | for (var i = 0; i < pods.Count(); i++){ 308 | var pod = pods.Get(i); 309 | this.AddPod(pod,whackHandler); 310 | logwindow.Log(pod); 311 | } 312 | } 313 | } 314 | 315 | function API(hostname){ 316 | 317 | this.debug = false; 318 | this.fails = 0; 319 | this.fail_threshold = 2; 320 | var apihostname = window.location.host; 321 | this.timeout = 5000; 322 | var apiprotocol = window.location.protocol + "//"; 323 | if (apihostname.length == 0){ 324 | apiprotocol =""; 325 | } 326 | var uri_color = "/api/color/"; 327 | var uri_color_complete = "/api/color-complete/"; 328 | 329 | var ajaxProxy = function(url, successHandler, errorHandler, timeout) { 330 | timeout = typeof timeout !== 'undefined' ? timeout : this.timeout; 331 | var connections = $.ajax({ 332 | url: url, 333 | success: successHandler, 334 | error: errorHandler, 335 | timeout: timeout 336 | 337 | }); 338 | if (this.debug){ 339 | console.log("Called: ", url); 340 | } 341 | }; 342 | 343 | var getColorURI = function(){ 344 | return apiprotocol + apihostname + uri_color; 345 | }; 346 | 347 | var getColorCompleteURI = function(){ 348 | return apiprotocol + apihostname + uri_color_complete; 349 | }; 350 | 351 | this.Color = function(successHandler, errorHandler){ 352 | ajaxProxy(getColorURI(), successHandler, errorHandler, 400); 353 | }; 354 | 355 | this.ColorComplete = function(successHandler, errorHandler){ 356 | ajaxProxy(getColorCompleteURI(), successHandler, errorHandler, 400); 357 | }; 358 | 359 | this.URL = getColorURI; 360 | 361 | this.IsHardFail = function(){ 362 | if (this.fails > this.fail_threshold){ 363 | return true; 364 | } else { 365 | this.fails++; 366 | return false; 367 | } 368 | }; 369 | 370 | this.ResetFails = function(){ 371 | if (this.fails != 0){ 372 | this.fails = 0; 373 | return true; 374 | } 375 | return false; 376 | }; 377 | 378 | } 379 | 380 | function DEPLOYMENTAPI(hostname, logwindow){ 381 | if (typeof(logwindow)==='undefined') logwindow = new LOGWINDOW(); 382 | 383 | this.debug = false; 384 | var apihostname = window.location.host; 385 | this.timeout = 5000; 386 | var apiprotocol = window.location.protocol + "//"; 387 | if (apihostname.length == 0){ 388 | apiprotocol =""; 389 | } 390 | var uri_getnodes = "/admin/k8s/nodes/get"; 391 | var uri_get = "/admin/k8s/pods/get"; 392 | var uri_delete = "/admin/k8s/deployment/delete"; 393 | var uri_create = "/admin/k8s/deployment/create"; 394 | var uri_deletepod = "/admin/k8s/pod/delete?pod="; 395 | var uri_drain = "/admin/k8s/node/drain?node="; 396 | var uri_uncordon = "/admin/k8s/node/uncordon?node="; 397 | 398 | 399 | var getPodsURI = function(){ 400 | return apiprotocol + apihostname + uri_get; 401 | }; 402 | 403 | var getNodesURI = function(){ 404 | return apiprotocol + apihostname + uri_getnodes; 405 | }; 406 | 407 | var getDeleteURI = function(){ 408 | return apiprotocol + apihostname + uri_delete; 409 | }; 410 | 411 | var getDeletePodURI = function(){ 412 | return apiprotocol + apihostname + uri_deletepod; 413 | }; 414 | 415 | var getCreateURI = function(){ 416 | return apiprotocol + apihostname + uri_create; 417 | }; 418 | 419 | var getDrainURI = function(){ 420 | return apiprotocol + apihostname + uri_drain; 421 | }; 422 | 423 | var getUncordonURI = function(){ 424 | return apiprotocol + apihostname + uri_uncordon; 425 | }; 426 | 427 | var success = function(e){ 428 | if (typeof(logwindow)!='undefined') { 429 | logwindow.Log(e); 430 | } 431 | }; 432 | 433 | var error = function(e){ 434 | if (typeof e.status != "undefined" && e.status == 404){ 435 | console.log("Item not found which in most cases is expected."); 436 | } else{ 437 | console.log("Failure: " , e); 438 | } 439 | 440 | }; 441 | 442 | var ajaxProxy = function(url) { 443 | $.ajax({ 444 | url: url, 445 | success: success, 446 | error: error, 447 | timeout: this.timeout 448 | 449 | }); 450 | if (this.debug){ 451 | console.log("Called: ", url); 452 | } 453 | 454 | }; 455 | 456 | this.Delete = function(){ 457 | ajaxProxy(getDeleteURI()); 458 | }; 459 | 460 | this.DeletePod = function(pod, successHandler, errorHandler){ 461 | var url = getDeletePodURI() + pod; 462 | $.ajax({ 463 | url: url, 464 | success: successHandler, 465 | error: errorHandler, 466 | timeout: this.timeout 467 | }); 468 | if (this.debug){ 469 | console.log("Called: ", url); 470 | } 471 | }; 472 | 473 | this.DrainNode = function(node, successHandler, errorHandler){ 474 | var url = getDrainURI() + node 475 | $.ajax({ 476 | url: url, 477 | success: successHandler, 478 | error: errorHandler, 479 | timeout: this.timeout 480 | 481 | }); 482 | if (this.debug){ 483 | console.log("Called: ", url); 484 | } 485 | }; 486 | 487 | this.UncordonNode = function(node, successHandler, errorHandler){ 488 | var url = getUncordonURI() + node 489 | $.ajax({ 490 | url: url, 491 | success: successHandler, 492 | error: errorHandler, 493 | timeout: this.timeout 494 | 495 | }); 496 | if (this.debug){ 497 | console.log("Called: ", url); 498 | } 499 | }; 500 | 501 | this.Create = function(successHandler, errorHandler){ 502 | var url = getCreateURI(); 503 | $.ajax({ 504 | url: url, 505 | success: successHandler, 506 | error: errorHandler, 507 | timeout: this.timeout 508 | 509 | }); 510 | if (this.debug){ 511 | console.log("Called: ", url); 512 | } 513 | }; 514 | 515 | this.Get = function(successHandler, errorHandler){ 516 | var url = getPodsURI(); 517 | $.ajax({ 518 | url: url, 519 | success: successHandler, 520 | error: errorHandler, 521 | timeout: this.timeout 522 | 523 | }); 524 | if (this.debug){ 525 | console.log("Called: ", url); 526 | } 527 | }; 528 | 529 | this.GetNodes = function(successHandler, errorHandler){ 530 | var url = getPodsURI(); 531 | $.ajax({ 532 | url: url, 533 | success: successHandler, 534 | error: errorHandler, 535 | timeout: this.timeout 536 | 537 | }); 538 | if (this.debug){ 539 | console.log("Called: ", url); 540 | } 541 | }; 542 | 543 | this.ResetNodes = function(){ 544 | this.GetNodes(handleRefreshNodes); 545 | 546 | }; 547 | 548 | var handleRefreshNodes = function(nodes, ex){ 549 | for (var i = 0; i < nodes.items.length; i++){ 550 | var node = nodes.items[i]; 551 | var url = getUncordonURI() + node.metadata.name; 552 | ajaxProxy(url); 553 | } 554 | }; 555 | 556 | 557 | } 558 | 559 | function GAME(){ 560 | var state = "new"; 561 | var bombShowed= false; 562 | var serviceDown = true; 563 | 564 | this.gameInterval = ""; 565 | this.scoreInterval = ""; 566 | this.podsInterval = ""; 567 | this.clockInterval = ""; 568 | 569 | this.HasBombShowed = function(){ 570 | return bombShowed; 571 | } 572 | 573 | this.SetBombShowed = function(){ 574 | bombShowed = true;; 575 | } 576 | 577 | 578 | this.IsServiceDown = function(){ 579 | return serviceDown; 580 | } 581 | 582 | this.SetServiceDown = function(){ 583 | serviceDown = true; 584 | } 585 | 586 | this.SetServiceUp = function(){ 587 | if (this.state != "done"){ 588 | serviceDown = false; 589 | } 590 | } 591 | 592 | 593 | this.GetState = function(){ 594 | return state; 595 | } 596 | 597 | this.Start = function(colorFunction, scoreFunction, podsFunction, clockFunction){ 598 | this.gameInterval = setInterval(colorFunction, 300); 599 | this.scoreInterval = setInterval(scoreFunction, 10); 600 | this.podsInterval = setInterval(podsFunction, 100); 601 | this.clockInterval = setInterval(clockFunction, 100); 602 | state = "started"; 603 | startTime = Date.now(); 604 | } 605 | 606 | this.Init = function(){ 607 | console.log("Init called.") 608 | state = "running"; 609 | this.SetServiceUp(); 610 | } 611 | 612 | this.Stop = function(){ 613 | state = "done"; 614 | window.clearInterval(this.gameInterval); 615 | window.clearInterval(this.scoreInterval); 616 | window.clearInterval(this.podsInterval); 617 | window.clearInterval(this.clockInterval); 618 | this.SetServiceDown(); 619 | } 620 | 621 | } 622 | 623 | function SOUNDS(){ 624 | 625 | var hit = ""; 626 | var hit2 = ""; 627 | var explosion = ""; 628 | var countdown = ""; 629 | var startup = ""; 630 | 631 | var makeSource = function(file,volume,loop){ 632 | if (typeof(loop)==='undefined') loop = false; 633 | if (typeof(volume)==='undefined') volume = 0.3; 634 | 635 | var fileExt = file.split('.').pop(); 636 | var type = "audio/wav"; 637 | 638 | if (fileExt == "mp3"){ 639 | type = "audio/mpeg"; 640 | } 641 | 642 | var result = new Audio(); 643 | var src = document.createElement("source"); 644 | src.type = type; 645 | src.src = file; 646 | result.preload = "auto"; 647 | result.append(src); 648 | result.volume = volume; 649 | return result; 650 | }; 651 | 652 | this.SetWhack = function(filename,volume){ 653 | hit = makeSource(filename,volume); 654 | hit2 = makeSource(filename,volume); 655 | }; 656 | 657 | this.SetExplosion = function(filename,volume){ 658 | explosion = makeSource(filename,volume); 659 | }; 660 | 661 | this.SetCountdown = function(filename,volume){ 662 | countdown = makeSource(filename,volume); 663 | }; 664 | 665 | this.SetStartup = function(filename,volume){ 666 | startup = makeSource(filename,volume); 667 | }; 668 | 669 | this.PlayWhack = function(filename,volume){ 670 | if (!hit.paused){ 671 | hit2.play(); 672 | } else{ 673 | hit.play(); 674 | } 675 | }; 676 | 677 | this.PlayExplosion = function(filename,volume){ 678 | explosion.play(); 679 | }; 680 | 681 | this.PlayCountdown = function(filename,volume){ 682 | countdown.play(); 683 | }; 684 | 685 | this.PlayStartup = function(filename,volume){ 686 | startup.play(); 687 | }; 688 | 689 | } 690 | 691 | function CLOCK(d, handler){ 692 | var start_time = new Date(); 693 | var duration = d; 694 | var completeHandler = handler; 695 | 696 | var shutItDown = function(){ 697 | completeHandler(); 698 | window.clearInterval(watcher); 699 | }; 700 | 701 | var checkComplete = function(){ 702 | var diff = new Date() - Date.parse(start_time); 703 | var count = Math.floor(diff/1000); 704 | if (count > duration){ 705 | shutItDown(); 706 | } 707 | }; 708 | 709 | this.getTimeLeft = function(){ 710 | var diff = new Date() - Date.parse(start_time); 711 | var count = Math.floor(diff/1000); 712 | var result = duration - count; 713 | if (result == 0){ 714 | shutItDown(); 715 | } 716 | return result; 717 | }; 718 | var watcher = setInterval(checkComplete, 200); 719 | } 720 | 721 | function BOMBUI(waitingimg, explodeimg){ 722 | var waiting = waitingimg; 723 | var explode = explodeimg; 724 | 725 | this.Explode = function(){ 726 | document.querySelector("#bomb").src = explode; 727 | setTimeout(this.Reset, 3000); 728 | 729 | }; 730 | 731 | this.Show = function(){ 732 | $(".bombpanel").show(); 733 | }; 734 | 735 | this.Reset = function(){ 736 | document.querySelector("#bomb").src = waiting; 737 | $(".bombpanel").hide(); 738 | var timeToCallShow = Math.random() * 500000; 739 | setTimeout(this.Show, timeToCallShow); 740 | }; 741 | 742 | } 743 | 744 | function PODLIST(json){ 745 | this.selflink = json.metadata.selfLink; 746 | 747 | this.items = []; 748 | 749 | for (var i=0; i< json.items.length; i++){ 750 | var item = new POD(json.items[i]); 751 | this.items.push(item) 752 | } 753 | 754 | } 755 | 756 | 757 | function LOGWINDOW(){ 758 | var alreadyShown = {}; 759 | alreadyShown.terminating = {}; 760 | alreadyShown.pending = {}; 761 | alreadyShown.running = {}; 762 | 763 | 764 | var IsAlreadyShown = function(pod){ 765 | if (typeof(alreadyShown[pod.phase][pod.name])==='undefined'){ 766 | return false; 767 | } 768 | return true; 769 | }; 770 | 771 | var IsError = function(e){ 772 | if (e.kind == "Status"){ 773 | return true; 774 | } 775 | if (typeof e.error != "undefined"){ 776 | return true; 777 | } 778 | return false; 779 | }; 780 | 781 | this.Log = function(ev){ 782 | 783 | 784 | var e = jQuery.extend(true, {}, ev); 785 | var item = e; 786 | if (e.kind == "Pod"){ 787 | item = new POD(e); 788 | } 789 | 790 | if (e.kind == "Node"){ 791 | item = new NODE(e); 792 | } 793 | 794 | if (e.kind == "PodList"){ 795 | item = new PODLIST(e); 796 | } 797 | 798 | if (typeof e.metadata != "undefined"){ 799 | if (e.metadata.selfLink.indexOf("Pod") > -1 ) { 800 | item = new POD(e); 801 | } 802 | 803 | if (e.metadata.selfLink.indexOf("Node") > -1 ) { 804 | item = new NODE(e); 805 | } 806 | } 807 | 808 | if (IsError(item)){ 809 | return; 810 | } 811 | 812 | 813 | 814 | if (item.type === "Pod"){ 815 | if (IsAlreadyShown(item)){ 816 | return; 817 | } 818 | 819 | alreadyShown[item.phase][item.name] = ""; 820 | delete item.terminateThreshold; 821 | delete item.holder; 822 | if (item.startTerminate == ""){ 823 | delete item.startTerminate; 824 | } 825 | } 826 | 827 | 828 | 829 | var output = JSON.stringify(item,null,2); 830 | var textArray = output.split("\n"); 831 | 832 | for (var i = textArray.length -1; i >= 0; i--){ 833 | if (textArray[i].length < 3){ 834 | continue; 835 | } 836 | var css_class = ""; 837 | var content = '
' + textArray[i] + '
'; 838 | if (textArray[i].indexOf("phase") >= 0){ 839 | css_class = "phase"; 840 | content = '
' + textArray[i] + '
'; 841 | } 842 | $(content).prependTo("#logwindow").hide().delay( (textArray.length - i) * 50 ).slideDown(); 843 | } 844 | 845 | var consoleLength = $("#logwindow div").length; 846 | if (consoleLength > 2000){ 847 | var diff = -(consoleLength - 2000); 848 | $('#logwindow > div').slice(diff).remove(); 849 | console.log("Log window trimmed"); 850 | } 851 | 852 | }; 853 | } 854 | -------------------------------------------------------------------------------- /apps/game/containers/default/assets/js/main.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | var default_duration = 40; 15 | 16 | // These are set in config.js, and are specific to your cluster setup 17 | var api = new API(servicehost); 18 | var deploymentAPI = new DEPLOYMENTAPI(adminhost); 19 | var pods = new PODS(); 20 | var podsUI = new PODSUI(pods); 21 | var bombUI = new BOMBUI("assets/img/bomb_waiting.png", "assets/img/bomb_explode.png"); 22 | var game = new GAME(); 23 | var clock = ""; 24 | var score = new SCORE(); 25 | var sounds = new SOUNDS(); 26 | sounds.SetWhack("assets/audio/pop.wav",.5); 27 | sounds.SetExplosion("assets/audio/explosion.wav",.5); 28 | sounds.SetCountdown("assets/audio/countdown.mp3",.5); 29 | sounds.SetStartup("assets/audio/startup.mp3",.5); 30 | 31 | document.addEventListener('DOMContentLoaded', function() { 32 | $("#start-modal").show(); 33 | $(".timer").html(default_duration); 34 | setReport("Kubernetes service not started yet."); 35 | deploymentAPI.Delete(); 36 | var interval = Math.random() * 200000; 37 | document.querySelector("#bomb").addEventListener("click", bombClickHandler); 38 | document.querySelector("#deploy-start").addEventListener("click", startDeployment); 39 | document.querySelector("#restart").addEventListener("click", restart); 40 | }); 41 | 42 | function setReport(msg, color){ 43 | if (typeof color == "undefined") color = "#333333"; 44 | var report = document.querySelector(".report"); 45 | report.innerHTML = "" + msg + ""; 46 | report.style.color = color; 47 | } 48 | 49 | function endDeployment(){ 50 | deploymentAPI.Delete(); 51 | game.Stop(); 52 | showTotals(); 53 | podsUI.ClearAll(); 54 | setReport("Kubernetes service went away!"); 55 | showModal("#end-modal"); 56 | } 57 | 58 | function showTotals(){ 59 | $("#total-pods").html(score.GetPods() + " pods"); 60 | $("#total-knockdowns").html(score.GetKnockDowns() + " service disruptions"); 61 | $("#total-score").html(score.GetTotal() + " points"); 62 | } 63 | 64 | function startDeployment(){ 65 | deploymentAPI.Create(initGame,genericError); 66 | hideModal("#start-modal"); 67 | setReport("Kubernetes starting up."); 68 | } 69 | 70 | function initGame(){ 71 | game.Start(getColor, showScore, getPods, getTimeLeft); 72 | } 73 | 74 | function getColor(){ 75 | api.Color(handleColor, handleColorError) 76 | } 77 | 78 | function getTimeLeft(){ 79 | if (clock != ""){ 80 | $(".timer").html(clock.getTimeLeft()); 81 | if (clock.getTimeLeft() <= 4){ 82 | sounds.PlayCountdown(); 83 | } 84 | } 85 | } 86 | 87 | function handleColor(e){ 88 | if (game.GetState() == "started"){ 89 | game.Init(); 90 | clock = new CLOCK(default_duration, endDeployment); 91 | sounds.PlayStartup(); 92 | } 93 | 94 | if (game.GetState() == "running"){ 95 | game.SetServiceUp(); 96 | setReport("Kubernetes service is UP!", e); 97 | } 98 | 99 | } 100 | 101 | function handleColorError(e,textStatus, errorThrown){ 102 | if (game.GetState() == "running") { 103 | setReport("Kubernetes service is DOWN!", "#FF0000"); 104 | alertYouKilledIt(); 105 | } 106 | } 107 | 108 | function showScore(){ 109 | document.querySelector(".scoreboard .total").innerHTML = score.GetTotal(); 110 | if (score.GetTotal() >= 25 && !game.HasBombShowed()){ 111 | bombUI.Show(); 112 | game.SetBombShowed(); 113 | } 114 | } 115 | 116 | function getPods(){ 117 | deploymentAPI.Get(handlePods, handlePodsError); 118 | } 119 | 120 | function handlePods(e){ 121 | if (game.GetState() == "done") { 122 | return; 123 | } 124 | 125 | podsUI.DrawPods(e, whackHandler); 126 | } 127 | 128 | function handlePodsError(e){ 129 | $(".pods").html(""); 130 | console.log("Error getting pods:", e); 131 | } 132 | 133 | function alertYouKilledIt(){ 134 | if (!game.IsServiceDown() && game.GetState() == "running"){ 135 | console.log("Killed it."); 136 | game.SetServiceDown(); 137 | score.KnockDown() 138 | $(".alert .msg").html("You knocked down the service."); 139 | $(".alert").show(); 140 | setTimeout(hideAlert, 3000); 141 | } 142 | } 143 | 144 | function whackHandler(e){ 145 | sounds.PlayWhack(); 146 | killPod(e.target.dataset.selflink) 147 | } 148 | 149 | function killPod(selflink){ 150 | deploymentAPI.DeletePod(selflink, score.KillPod, podError); 151 | } 152 | 153 | function bombClickHandler(e){ 154 | deploymentAPI.Get(bombBlastHandler, genericError); 155 | } 156 | 157 | function bombBlastHandler(e){ 158 | sounds.PlayExplosion(); 159 | for (var i = 0; i < e.items.length; i++){ 160 | var pod = e.items[i]; 161 | if (pod.status.phase == "Running"){ 162 | killPod(pod.metadata.selfLink); 163 | } 164 | } 165 | bombUI.Explode(); 166 | } 167 | 168 | // Add functions below to lib.js 169 | function genericError(e){ 170 | console.log("Error: ", e); 171 | } 172 | 173 | function hideModal(id){ 174 | var modal = $(id); 175 | modal.fadeOut(); 176 | } 177 | 178 | function podError(e){ 179 | console.log("Pod already gone? :", e); 180 | } 181 | 182 | function showModal(id){ 183 | var modal = $(id); 184 | modal.fadeIn('slow'); 185 | } 186 | 187 | function hideAlert(){ 188 | $(".alert").fadeOut('slow'); 189 | } -------------------------------------------------------------------------------- /apps/game/containers/default/assets/js/next.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | var default_duration = 40; 15 | 16 | // These are set in config.js, and are specific to your cluster setup 17 | var api = new API(servicehost); 18 | var logwindow = new LOGWINDOW(); 19 | var deploymentAPI = new DEPLOYMENTAPI(adminhost, logwindow); 20 | var pods = new PODS(); 21 | var podsUI = new PODSUI(pods, logwindow); 22 | var bombUI = new BOMBUI("assets/img/bomb_waiting_next.png", "assets/img/bomb_explode_next.png"); 23 | var game = new GAME(); 24 | var clock = ""; 25 | var score = new SCORE(); 26 | var sounds = new SOUNDS(); 27 | var fails_threshold = 9; 28 | sounds.SetWhack("assets/audio/pop.wav",.5); 29 | sounds.SetExplosion("assets/audio/explosion.wav",.5); 30 | sounds.SetCountdown("assets/audio/countdown.mp3",.5); 31 | sounds.SetStartup("assets/audio/startup.mp3",.5); 32 | 33 | 34 | document.addEventListener('DOMContentLoaded', function() { 35 | $("#start-modal").show(); 36 | $(".timer").html(default_duration); 37 | setReport(""); 38 | deploymentAPI.Delete(); 39 | var interval = Math.random() * 200000; 40 | $("#bomb").click(bombClickHandler); 41 | $("#deploy-start").click(startDeployment); 42 | $("#restart").click(restart); 43 | $("#deploy-start").focus(); 44 | }); 45 | 46 | 47 | function setReport(msg, color){ 48 | if (typeof color == "undefined") color = "#333333"; 49 | var report = $(".report"); 50 | 51 | report.css("-webkit-filter", "drop-shadow(2px 2px 3px " + color + ")"); 52 | report.css("color", color); 53 | var msgholder = $("#report_message"); 54 | if (msgholder.length == 0){ 55 | report.after('
' + msg + '
') 56 | } else { 57 | msgholder.html(msg); 58 | } 59 | 60 | 61 | if(game.GetState() == "running"){ 62 | if(game.IsServiceDown()) { 63 | $(".report").addClass("service_down"); 64 | $(".report").removeClass("service_up"); 65 | } else{ 66 | $(".report").addClass("service_up"); 67 | $(".report").removeClass("service_down"); 68 | } 69 | } 70 | 71 | } 72 | 73 | function endDeployment(){ 74 | deploymentAPI.Delete(); 75 | game.Stop(); 76 | showTotals(); 77 | podsUI.ClearAll(); 78 | setReport(""); 79 | $(".report").removeClass("service_up"); 80 | $(".report").removeClass("service_down"); 81 | showModal("#end-modal"); 82 | setTimeout(function(){location.reload();}, 15000); 83 | } 84 | 85 | function showTotals(){ 86 | $("#total-pods").html(score.GetPods() + " pods"); 87 | $("#total-knockdowns").html(score.GetKnockDowns() + " service disruptions"); 88 | $("#total-score").html(score.GetTotal() + " points"); 89 | } 90 | 91 | function startDeployment(){ 92 | deploymentAPI.Create(initGame,genericError); 93 | hideModal("#start-modal"); 94 | $(".pods-explain").fadeIn(); 95 | 96 | setReport(""); 97 | } 98 | 99 | function initGame(e){ 100 | game.Start(getColor, showScore, getPods, getTimeLeft); 101 | logwindow.Log(e); 102 | 103 | } 104 | 105 | function getColor(){ 106 | api.Color(handleColor, handleColorError) 107 | } 108 | 109 | function getTimeLeft(){ 110 | if (clock != ""){ 111 | $(".timer").html(clock.getTimeLeft()); 112 | if (clock.getTimeLeft() <= 4){ 113 | sounds.PlayCountdown(); 114 | } 115 | } 116 | } 117 | 118 | function handleColor(e){ 119 | if (game.GetState() == "started"){ 120 | game.Init(); 121 | clock = new CLOCK(default_duration, endDeployment); 122 | sounds.PlayStartup(); 123 | setTimeout(function(){$(".pods-explain").fadeOut();}, 3000); 124 | } 125 | 126 | if (game.GetState() == "running"){ 127 | game.SetServiceUp(); 128 | setReport("", e); 129 | } 130 | if (api.fails > 0){ 131 | api.fails = 0; 132 | console.log("Soft service has recovered."); 133 | } 134 | 135 | 136 | } 137 | 138 | function handleColorError(e,textStatus, errorThrown){ 139 | if (game.GetState() == "running") { 140 | if (api.fails > fails_threshold){ 141 | console.log("Hard service fail."); 142 | setReport("Kubernetes service is DOWN!", "#FF0000"); 143 | alertYouKilledIt(); 144 | } else { 145 | console.log("Soft service fail. Retry"); 146 | api.fails++; 147 | } 148 | 149 | } 150 | 151 | } 152 | 153 | function showScore(){ 154 | document.querySelector(".scoreboard .total").innerHTML = score.GetTotal(); 155 | if (score.GetTotal() >= 25 && !game.HasBombShowed()){ 156 | bombUI.Show(); 157 | game.SetBombShowed(); 158 | } 159 | } 160 | 161 | function getPods(){ 162 | deploymentAPI.Get(handlePods, handlePodsError); 163 | } 164 | 165 | function handlePods(e){ 166 | if (game.GetState() == "done") { 167 | return; 168 | } 169 | 170 | podsUI.DrawPods(e, whackHandler); 171 | } 172 | 173 | function handlePodsError(e){ 174 | $(".pods").html(""); 175 | console.log("Error getting pods:", e); 176 | } 177 | 178 | function alertYouKilledIt(){ 179 | if (!game.IsServiceDown() && game.GetState() == "running"){ 180 | console.log("Killed it."); 181 | game.SetServiceDown(); 182 | score.KnockDown() 183 | $(".alert .msg").html("You knocked down the service."); 184 | $(".alert").show(); 185 | setTimeout(hideAlert, 3000); 186 | } 187 | } 188 | 189 | function whackHandler(e){ 190 | sounds.PlayWhack(); 191 | if (e.target.id == ""){ 192 | $("#" + e.target.parentNode.id ).addClass("terminating"); 193 | } else{ 194 | $("#" + e.target.id ).addClass("terminating"); 195 | } 196 | 197 | 198 | killPod(e.target.dataset.selflink) 199 | } 200 | 201 | function killPod(selflink){ 202 | deploymentAPI.DeletePod(selflink, killHandler, podError); 203 | } 204 | 205 | function killHandler(e){ 206 | score.KillPod(); 207 | logwindow.Log(e); 208 | } 209 | 210 | function bombClickHandler(e){ 211 | deploymentAPI.Get(bombBlastHandler, genericError); 212 | } 213 | 214 | function bombBlastHandler(e){ 215 | sounds.PlayExplosion(); 216 | for (var i = 0; i < e.items.length; i++){ 217 | var pod = e.items[i]; 218 | if (pod.status.phase == "Running"){ 219 | killPod(pod.metadata.selfLink); 220 | } 221 | } 222 | bombUI.Explode(); 223 | } 224 | 225 | 226 | function showModal(id){ 227 | var modal = $(id); 228 | modal.fadeIn('slow'); 229 | } 230 | 231 | function hideModal(id){ 232 | var modal = $(id); 233 | modal.fadeOut(); 234 | } 235 | 236 | function restart(){ 237 | location.reload(); 238 | } 239 | 240 | function hideAlert(){ 241 | $(".alert").fadeOut('slow'); 242 | } 243 | 244 | function podError(e){ 245 | console.log("Pod already gone? :", e); 246 | } 247 | 248 | function genericError(e){ 249 | console.log("Error: ", e); 250 | } -------------------------------------------------------------------------------- /apps/game/containers/default/healthz/index.html: -------------------------------------------------------------------------------- 1 | ok -------------------------------------------------------------------------------- /apps/game/containers/default/index.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | Whack a Pod! 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 |

You will see random flashes of green colors while the service is up.
32 | Manage to kill the service and this box goes red.

33 | 34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 |
58 |
59 |
60 |
61 |
62 | 63 |

Click the bomb to whack all the pods at once!

64 |
65 |
66 | 67 | 68 | 70 | 71 | 72 | 74 | 75 |
Time: 69 | 0
Score: 73 | 0
76 |
77 |
78 |

We have a Kubernetes service 79 | that returns a random color of green. You can see the color of this 80 | sign changing constantly because we constantly poll it. That's how 81 | we know Kubernetes is keeping the service up.

82 |
83 |
84 |

Each one of these moles represents an actual Kuberntes pod. When 85 | you whack them, you kill the pod. The goal is to try and knock down 86 | enough pods that you disrupt the service. Notice:

87 | 88 |
    89 |
  • How fast Kubernetes pods come back up
  • 90 |
  • How hard it is to disrupt the service.
  • 91 |
  • How fast the service comes back up after you manage to disrupt it
  • 92 |
93 |
94 |
95 |

Scoring 96 | 97 | 98 | 99 |
Knock down pod:1point
Disrupt service:100points
100 |

101 | 102 |
103 | 104 | 123 | 135 | 136 | -------------------------------------------------------------------------------- /apps/game/containers/default/next.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | Whack a Pod! 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
Can't hit… yet.
Hit away.
Already Hit.
67 |
68 |
69 |
70 |
71 |
72 | 73 |

Click the star to whack all the pods at once!

74 |
75 |
76 | 77 | 78 | 80 | 81 | 82 | 84 | 85 |
Time: 79 | 0
Score: 83 | 0
86 |
87 |
88 |

We have a Kubernetes service 89 | that returns a random color of green. You can see the color of this 90 | sign changing constantly because we constantly poll it. That's how 91 | we know Kubernetes is keeping the service up.

92 |
93 |
94 |

Each one of these moles represents an actual Kuberntes pod. When 95 | you whack them, you kill the pod. The goal is to try and knock down 96 | enough pods that you disrupt the service. Notice:

97 | 98 |
    99 |
  • How fast Kubernetes pods come back up
  • 100 |
  • How hard it is to disrupt the service.
  • 101 |
  • How fast the service comes back up after you manage to disrupt it
  • 102 |
103 |
104 |
105 |

Scoring 106 | 107 | 108 | 109 |
Knock down pod:1point
Disrupt service:100points
110 |

111 | 112 |
113 | 114 | 132 | 144 | 145 |
146 | 147 | 148 | -------------------------------------------------------------------------------- /apps/game/docker/Makefile: -------------------------------------------------------------------------------- 1 | BASEDIR = $(shell pwd) 2 | 3 | .DEFAULT_GOAL := app 4 | 5 | app: clean build serve 6 | 7 | 8 | 9 | build: 10 | docker build -t game "$(BASEDIR)/../containers/." 11 | 12 | serve: 13 | docker run --name=game -d -P -p 8080:8080 game 14 | 15 | clean: 16 | -docker stop game 17 | -docker rm game 18 | -docker rmi game -------------------------------------------------------------------------------- /apps/game/kubernetes/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | BASEDIR = $(shell pwd) 15 | 16 | include ../../../Makefile.properties 17 | 18 | app: build deploy 19 | 20 | reset: clean.deployment app 21 | say "app refresh complete" 22 | 23 | 24 | reset.safe: env creds 25 | gcloud container builds submit "$(BASEDIR)/../containers/." --tag=$(DOCKERREPO)/game 26 | -kubectl delete deployment game-deployment 27 | kubectl run game-deployment --image=$(DOCKERREPO)/game --replicas=4 --port=8080 --labels=app=game 28 | say "app refresh complete" 29 | 30 | build: env 31 | gcloud container builds submit "$(BASEDIR)/../containers/." --tag=$(DOCKERREPO)/game 32 | 33 | build.dockerhub: 34 | docker build "$(BASEDIR)/../containers/." --tag=$(DOCKERREPO):game 35 | docker push $(DOCKERREPO):game 36 | 37 | build.generic: 38 | docker build "$(BASEDIR)/../containers/." --tag=$(DOCKERREPO)/whackapod-game 39 | docker push $(DOCKERREPO)/whackapod-game 40 | 41 | deploy: env creds deployment service 42 | 43 | deploy.minikube: deployment service 44 | 45 | deploy.minikube.dockerhub: 46 | kubectl run game-deployment --image=$(DOCKERREPO):game --replicas=4 --port=8080 --labels=app=game 47 | kubectl expose deployment game-deployment --name=game --target-port=8080 --type=NodePort --labels="app=game" 48 | 49 | deploy.generic: 50 | kubectl run game-deployment --image=$(DOCKERREPO)/whackapod-game --replicas=4 --port=8080 --labels=app=game 51 | kubectl expose deployment game-deployment --name=game --target-port=8080 --type=NodePort --labels="app=game" 52 | 53 | deployment: 54 | kubectl run game-deployment --image=$(DOCKERREPO)/game --replicas=4 --port=8080 --labels=app=game 55 | 56 | service: 57 | kubectl expose deployment game-deployment --name=game --target-port=8080 --type=NodePort --labels="app=game" 58 | 59 | clean: env creds clean.deployment clean.service 60 | 61 | clean.generic: clean.deployment clean.service 62 | 63 | clean.minikube: clean.deployment clean.service 64 | 65 | clean.deployment: 66 | -kubectl delete deployment game-deployment 67 | 68 | clean.service: 69 | -kubectl delete service game 70 | 71 | 72 | retry: clean build deploy 73 | 74 | config: env 75 | -------------------------------------------------------------------------------- /apps/ingress/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | BASEDIR = $(shell pwd) 15 | 16 | include ../../Makefile.properties 17 | 18 | deploy: env creds 19 | kubectl apply -f "$(BASEDIR)/ingress.yaml" 20 | 21 | deploy.minikube: 22 | kubectl apply -f "$(BASEDIR)/ingress.minikube.yaml" 23 | 24 | deploy.generic: 25 | kubectl apply -f "$(BASEDIR)/ingress.generic.yaml" 26 | 27 | config: env 28 | @echo Creating INGRESS Yaml files based on samples and setting in your Makefile.properties 29 | @cp "$(BASEDIR)/ingress.sample.yaml" "$(BASEDIR)/ingress.yaml" 30 | $(call rewritefile,"$(BASEDIR)/ingress.yaml",%INGRESSNAME%,$(INGRESSNAME)) 31 | 32 | clean: env creds 33 | -kubectl delete -f "$(BASEDIR)/ingress.yaml" 34 | 35 | clean.minikube: 36 | -kubectl delete -f "$(BASEDIR)/ingress.minikube.yaml" 37 | 38 | clean.generic: 39 | -kubectl delete -f "$(BASEDIR)/ingress.generic.yaml" 40 | 41 | define rewritefile 42 | @sed s/$(2)/$(3)/g <""$(1)"" >"$(BASEDIR)/.temp" 43 | @cp "$(BASEDIR)/.temp" $(1) 44 | @rm "$(BASEDIR)/.temp" 45 | endef 46 | -------------------------------------------------------------------------------- /apps/ingress/ingress.generic.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All Rights Reserved. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | apiVersion: extensions/v1beta1 15 | kind: Ingress 16 | metadata: 17 | name: wap-ingress 18 | annotations: 19 | nginx.ingress.kubernetes.io/rewrite-target: / 20 | spec: 21 | rules: 22 | - http: 23 | paths: 24 | - path: /api/* 25 | backend: 26 | serviceName: api 27 | servicePort: 8080 28 | - path: /admin/* 29 | backend: 30 | serviceName: admin 31 | servicePort: 8080 32 | - path: /* 33 | backend: 34 | serviceName: game 35 | servicePort: 8080 36 | host: whackapod.example.com 37 | -------------------------------------------------------------------------------- /apps/ingress/ingress.minikube.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All Rights Reserved. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | apiVersion: extensions/v1beta1 15 | kind: Ingress 16 | metadata: 17 | name: wap-ingress 18 | annotations: 19 | ingress.kubernetes.io/rewrite-target: / 20 | spec: 21 | rules: 22 | - host: minikube.wap 23 | http: 24 | paths: 25 | - path: /api/ 26 | backend: 27 | serviceName: api 28 | servicePort: 8080 29 | - path: /admin/ 30 | backend: 31 | serviceName: admin 32 | servicePort: 8080 33 | - path: / 34 | backend: 35 | serviceName: game 36 | servicePort: 8080 37 | -------------------------------------------------------------------------------- /apps/ingress/ingress.sample.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All Rights Reserved. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | apiVersion: extensions/v1beta1 15 | kind: Ingress 16 | metadata: 17 | name: wap-ingress 18 | annotations: 19 | kubernetes.io/ingress.global-static-ip-name: %INGRESSNAME% 20 | spec: 21 | rules: 22 | - http: 23 | paths: 24 | - path: /api/* 25 | backend: 26 | serviceName: api 27 | servicePort: 8080 28 | - path: /admin/* 29 | backend: 30 | serviceName: admin 31 | servicePort: 8080 32 | - path: /* 33 | backend: 34 | serviceName: game 35 | servicePort: 8080 -------------------------------------------------------------------------------- /infrastructure/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | BASEDIR = $(shell pwd) 15 | 16 | include ../Makefile.properties 17 | NODEDISKSIZE="200" 18 | NODECOUNT="2" 19 | NODETYPE="n1-standard-4" 20 | 21 | # Builds the entire infrastructure you need to run this set of demos. 22 | build: env build.cluster ip 23 | 24 | # Requests a GKE cluster 25 | build.cluster: 26 | -gcloud container clusters create $(CLUSTER) --num-nodes $(NODECOUNT) \ 27 | --disk-size=$(NODEDISKSIZE) --machine-type=$(NODETYPE) --zone=$(ZONE) 28 | 29 | # Runs all the clean commands. 30 | clean: env clean.cluster clean.repo clean.ip 31 | 32 | # deletes the cluster. 33 | clean.cluster: 34 | gcloud container clusters delete $(CLUSTER) -q 35 | 36 | # Gets rid of all of the images in GKR 37 | clean.repo: 38 | -gsutil -m rm -r gs://artifacts.$(PROJECT).appspot.com 39 | 40 | ip: 41 | gcloud compute --project $(PROJECT) addresses create $(INGRESSNAME) --global 42 | 43 | clean.ip: 44 | gcloud compute --project $(PROJECT) addresses delete $(INGRESSNAME) 45 | 46 | minikube: 47 | minikube start 48 | 49 | clean.minikube: 50 | minikube start -------------------------------------------------------------------------------- /screenshots/advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/screenshots/advanced.png -------------------------------------------------------------------------------- /screenshots/game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/screenshots/game.png -------------------------------------------------------------------------------- /screenshots/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpryan/whack_a_pod/c2053a1917e70a0c28f478d183beb58e8697af78/screenshots/next.png --------------------------------------------------------------------------------