├── .gitignore ├── README.md └── guestbook ├── Procfile ├── README.md ├── resources-with-secrets └── guestbook-controller.yaml ├── resources ├── guestbook-controller.yaml ├── guestbook-service.yaml ├── redis-master-controller.yaml ├── redis-master-service.yaml ├── redis-slave-controller.yaml └── redis-slave-service.yaml ├── v1 ├── Dockerfile ├── Makefile ├── guestbook │ └── Dockerfile ├── main.go └── public │ ├── index.html │ ├── script.js │ └── style.css └── v2 ├── Dockerfile ├── Makefile ├── guestbook └── Dockerfile ├── main.go └── public ├── index.html ├── script.js └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | mycluster 2 | toronto 3 | guestbook/secret 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This tutorial will demo a number of technologies. The requirements for completing each section will be introduced in the section. 4 | 5 | This presentation was given at LinuxCon 2016 in Toronto. There was a live tutorial given and the video is not yet available. However, there are [slides](https://speakerdeck.com/philips/coreos-a-tutorial-on-hyperscale-infrastructure). 6 | 7 | ## etcd Basics 8 | 9 | **Pre-requisites** 10 | 11 | - [etcd and etcdctl](https://github.com/coreos/etcd/releases/tag/v3.0.6) for your platform 12 | 13 | First, run `etcd` in a terminal window. 14 | 15 | ``` 16 | ./etcd 17 | ... 18 | ``` 19 | 20 | Storing and retrieving values is done simply using the put/get subcommands. 21 | 22 | ``` 23 | export ETCDCTL_API=3 24 | ./etcdctl put foo bar 25 | ``` 26 | 27 | ``` 28 | ./etcdctl get foo 29 | ``` 30 | 31 | With the `-w` flag additional information can be found. Notice the "revision"? With etcd all keys are revisioned and you can use this revision number to get old values of keys, setup multi-key transactions, and view all changes since a certain time. 32 | 33 | Using two differnt put calls we will create a couple of known revisions for the key `foo`. 34 | 35 | ``` 36 | ./etcdctl put foo bar -w json 37 | {"header":{"cluster_id":17237436991929493444,"member_id":9372538179322589801,"revision":9,"raft_term":2}} 38 | ``` 39 | 40 | ``` 41 | ./etcdctl put foo toronto -w json 42 | {"header":{"cluster_id":17237436991929493444,"member_id":9372538179322589801,"revision":11,"raft_term":2}} 43 | ``` 44 | 45 | With the revision number etcd can "time-travel" and look at old values of `foo`: 46 | 47 | ``` 48 | ./etcdctl get foo --rev 9 49 | foo 50 | bar 51 | ``` 52 | 53 | ``` 54 | ./etcdctl get foo --rev 11 55 | foo 56 | toronto 57 | ``` 58 | 59 | ``` 60 | ./etcdctl get foo -w json 61 | {"header":{"cluster_id":17237436991929493444,"member_id":9372538179322589801,"revision":11,"raft_term":2},"kvs":[{"key":"Zm9v","create_revision":2,"mod_revision":11,"version":10,"value":"dG9yb250bw=="}],"count":1} 62 | ``` 63 | 64 | ## etcd Clustering 65 | 66 | **Pre-requisites** 67 | 68 | - A working [Go environment](https://golang.org/doc/install) 69 | - Follow the upstream guide to [setup a local cluster](https://github.com/coreos/etcd/blob/master/Documentation/dev-guide/local_cluster.md#local-multi-member-cluster) 70 | 71 | After setting up the pre-requisites a three node etcd cluster will be running. 72 | 73 | Members of the cluster can be listed like this: 74 | 75 | ``` 76 | ./etcdctl member list 77 | 8211f1d0f64f3269, started, infra1, http://127.0.0.1:12380, http://127.0.0.1:2379 78 | 91bc3c398fb3c146, started, infra2, http://127.0.0.1:22380, http://127.0.0.1:22379 79 | fd422379fda50e48, started, infra3, http://127.0.0.1:32380, http://127.0.0.1:32379 80 | ``` 81 | 82 | ## Building an Application 83 | 84 | **Pre-Requisites** 85 | 86 | - A working local Docker client (`brew install docker`) 87 | - A VM to run Docker, recommend [minikube](https://github.com/kubernetes/minikube/releases) 88 | 89 | ``` 90 | git clone https://github.com/philips/2016-LinuxCon-NA-CoreOS-A-Tutorial-on-Hyperscale-Infrastructure 91 | cd guestbook/v1 92 | eval $(minikube docker-env) 93 | VERSION=v1 REGISTRY=quay.io/philips make build 94 | VERSION=v1 REGISTRY=quay.io/philips make push 95 | ``` 96 | 97 | ## Running a Single Container 98 | 99 | **Pre-Requisites** 100 | - Any CoreOS virtual machine and SSH session 101 | 102 | Now that we have built the container it is easy to run on a virtual machine with rkt: 103 | 104 | ``` 105 | rkt fetch docker://quay.io/philips/guestbook:v1 --insecure-options=image 106 | ``` 107 | 108 | Now, one thing to note is that rkt does not have a daemon. So, we really on your system init system to monitor the process. To do that quickly under systemd do something like this: 109 | 110 | ``` 111 | systemd-run rkt fetch docker://quay.io/philips/guestbook:v1 --insecure-options=image 112 | ``` 113 | 114 | Or with docker: 115 | 116 | ``` 117 | docker run quay.io/philips/guestbook:v1 118 | ``` 119 | 120 | ## Debugging with Toolbox 121 | 122 | **Pre-Requisites** 123 | - Any CoreOS virtual machine and SSH session 124 | 125 | With fewer pieces of software on the host what happens to debugging tools? CoreOS provides a quick solution with `toolbox`: 126 | 127 | ``` 128 | toolbox 129 | ``` 130 | 131 | The environment can be customized to run any container whether that is Debian, Ubuntu, Fedora, Arch, etc. 132 | 133 | 134 | ## Kubernetes Basics 135 | 136 | **Pre-Requisites** 137 | 138 | - An [AWS account](http://aws.amazon.com/) and [AWS cli](https://aws.amazon.com/cli/) 139 | - An [AWS keypair for us-west-2](https://us-west-2.console.aws.amazon.com/ec2/v2/home?region=us-west-2#KeyPairs:sort=keyName) 140 | - [kube-aws](https://coreos.com/kubernetes/docs/latest/kubernetes-on-aws.html) installed and in your path 141 | - [kubectl 1.3.4+](https://coreos.com/kubernetes/docs/latest/configure-kubectl.html) installed and in your path 142 | - Follow the [Kubernetes + CoreOS + AWS docs](https://coreos.com/kubernetes/docs/latest/kubernetes-on-aws.html). 143 | 144 | With a working Kubernetes cluster it is possible to proxy through to localhost for development without having to worry about auth: 145 | 146 | ``` 147 | kubectl proxy 148 | ``` 149 | 150 | From there the API becomes very accessible using well-known tools like curl: 151 | 152 | ``` 153 | curl 127.0.0.1:8001/api/v1/services 154 | ``` 155 | 156 | The equivalent of this rest API call is: 157 | 158 | ``` 159 | kubectl describes services 160 | ``` 161 | 162 | ## Kubernetes App Deployments 163 | 164 | First, lets run the app that was built earlier and pushed to quay.io. 165 | 166 | ``` 167 | kubectl run guestbook --image quay.io/philips/guestbook:v1 -l app=guestbook 168 | ``` 169 | 170 | Confirm that the application is running by selecting all things that have `app=guestbook`: 171 | 172 | ``` 173 | kubectl get pods -l app=guestbook 174 | NAME READY STATUS RESTARTS AGE 175 | guestbook-2893398214-x04rm 1/1 Running 0 4m 176 | ``` 177 | 178 | Neat! Now, connect to our application by selecting that pod process and forwarding the port. 179 | 180 | ``` 181 | kubectl port-forward $(kubectl get pods -l app=guestbook -o template --template="{{range.items}}{{.metadata.name}}{{end}}") 3000:3000 182 | ``` 183 | 184 | Visit: http://localhost:3000 185 | 186 | Success! Kubernetes is now able to manage running a container! Cleanup: 187 | 188 | ``` 189 | kubectl delete deployment guestbook 190 | ``` 191 | 192 | ## Kubernetes App Failures 193 | 194 | Setup the application to run again using the kubectl run subcommand. 195 | 196 | ``` 197 | kubectl run guestbook --image quay.io/philips/guestbook:v1 -l app=guestbook 198 | kubectl get pods -l app=guestbook 199 | ``` 200 | 201 | Kubernetes will allow the app instance to be killed. 202 | 203 | ``` 204 | kubectl delete guestbook-2893398214-ikc58 205 | ``` 206 | 207 | But, it will drive the cluster state torwards a single running instance. Within a few seconds a replacement is launched: 208 | 209 | ``` 210 | kubectl get pods -l app=guestbook 211 | ``` 212 | 213 | The reason that the single pod was replaced is because the deployment, which we will discuss later, that is driving the guestbook application 214 | 215 | ``` 216 | kubectl describe deployment guestbook 217 | ``` 218 | 219 | ## Kubernetes App Scaling 220 | 221 | ``` 222 | kubectl scale deployment guestbook --replicas=3 223 | ``` 224 | 225 | - Third party controllers for complex applications 226 | 227 | ## Kubernetes Services 228 | 229 | Earlier we used port forwarding to confirm the application was running. This is fun, but this isn't terribly useful as no one outside of the cluster can reach our application. Delete the deployment and lets try exposing a port: 230 | 231 | ``` 232 | kubectl delete deployment guestbook 233 | kubectl run guestbook --image quay.io/philips/guestbook:v1 -l app=guestbook --expose --port 3000 234 | ``` 235 | 236 | Now the service will have a cluster IP that is routable to other nodes on the cluster. 237 | 238 | ``` 239 | kubectl describe service guestbook 240 | ``` 241 | 242 | Often, this isn't terribly useful as users workstation's rarely are on the same network/VPC/overlay/etc that the cluster is on. The 10.0.0.0/24 address isn't routable. But, the IPs of the nodes are. And by using a type of service called a "NodePort" a port on the nodes will forward to the service. 243 | 244 | Assuming we setup our cluster at http://toronto.example.com:31512/ 245 | 246 | ``` 247 | kubectl edit service guestbook 248 | ``` 249 | 250 | It would be much more convienent however if the cluster setup a real load balancer. This can be done by editing the type once more from "NodePort" to "LoadBalancer" 251 | 252 | ``` 253 | kubectl edit service guestbook 254 | kubectl describe service guestbook 255 | ``` 256 | 257 | This time the service description will include a "LoadBalancer Ingress" that is a real LoadBalancer depending on the environment of the cluster. 258 | 259 | Now, cleanup everything: 260 | 261 | ``` 262 | kubectl delete deployment guestbook 263 | kubectl delete service guestbook 264 | ``` 265 | 266 | ## More on Services 267 | 268 | Port-forward cluster local DNS to your workstation. 269 | 270 | ``` 271 | kubectl port-forward --namespace=kube-system $( kubectl get pods --namespace=kube-system -l k8s-app=kube-dns -o template --template="{{range.items}}{{.metadata.name}}{{end}}") 5300:53 272 | ``` 273 | 274 | Try and grab the redis-master service powering our website: 275 | 276 | ``` 277 | dig +vc -p 5300 @127.0.0.1 redis-master.default.svc.cluster.local 278 | redis-master.default.svc.cluster.local. 30 IN A 10.3.0.25 279 | ``` 280 | 281 | For more [network debugging tips see this page](https://github.com/coreos/docs/blob/master/kubernetes/network-troubleshooting.md). 282 | 283 | ## Using Kubernetes in a Development Workflow 284 | 285 | **Pre-Requisites** 286 | 287 | - A working [Go environment](https://golang.org/doc/install) 288 | - A working redis cluster from above 289 | - goreman or another Procfile runner (`go get github.com/mattn/goreman`) 290 | 291 | It is very useful to be able to hack on code locally while using services running the cluster. Let's walk through the development workflow for the Guestbook service. 292 | 293 | First, this setup is really naive so it only works when there is only one slave. Scale the slave replica set down to 1. 294 | 295 | ``` 296 | kubectl scale rs redis-slave --replicas=1 297 | ``` 298 | 299 | Now, inside of the guestbook subdirectory of this repo there is a Procfile. When ran with `goreman start` it will forward the redis master and slave. 300 | 301 | ``` 302 | goreman start 303 | ``` 304 | 305 | At this point you now have the live cluster database forwarding locally. This can be confirmed by querying the redis database using the CLI tooling: 306 | 307 | ``` 308 | redis-cli -h 127.0.0.1 -p 6380 keys '*' 309 | ``` 310 | 311 | Now hacking on the application is easy, simply go into the v2 directory and run: 312 | 313 | ``` 314 | REDIS_SLAVE=localhost:6379 REDIS_MASTER=localhost:6380 go run main.go 315 | ``` 316 | 317 | Its the best of both worlds! 318 | -------------------------------------------------------------------------------- /guestbook/Procfile: -------------------------------------------------------------------------------- 1 | master: kubectl port-forward $(kubectl get pods -l app=redis,role=master -o template --template="{{range.items}}{{.metadata.name}}{{end}}") 6380:6379 2 | slave: kubectl port-forward $(kubectl get pods -l app=redis,role=slave -o template --template="{{range.items}}{{.metadata.name}}{{end}}") 6379:6379 3 | -------------------------------------------------------------------------------- /guestbook/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This is a live tutorial given at LinuxCon 2016 North America. It is designed to introduce all of the basic concepts of CoreOS and Kubernetes as a platform for application management and distributed systems. 4 | 5 | Slides: https://speakerdeck.com/philips/coreos-a-tutorial-on-hyperscale-infrastructure 6 | 7 | ## Building Guestbook 8 | 9 | Releasing the image requires that you have access to the registry user account which will host the image. You can specify the registry including the user account by setting the environment variable `REGISTRY`. 10 | 11 | To build and release the guestbook image: 12 | 13 | cd examples/guestbook-go/_src 14 | make release 15 | 16 | To build and release the guestbook image with a different registry and version: 17 | 18 | VERSION=v4 REGISTRY="quay.io/philips" make build 19 | 20 | If you want to, you can build and push the image step by step: 21 | 22 | make clean 23 | make build 24 | make push 25 | 26 | 27 | ## Developing 28 | 29 | Port forward redis to localhost 30 | 31 | ``` 32 | kubectl port-forward $(kubectl get pods -l app=redis,role=master -o template --template="{{range.items}}{{.metadata.name}}{{end}}") 6380:6379 33 | kubectl port-forward $(kubectl get pods -l app=redis,role=slave -o template --template="{{range.items}}{{.metadata.name}}{{end}}") 6379:6379 34 | ``` 35 | 36 | Tell the app to use those ports 37 | 38 | ``` 39 | REDIS_SLAVE=localhost:6379 REDIS_MASTER=localhost:6380 go run main.go 40 | ``` 41 | -------------------------------------------------------------------------------- /guestbook/resources-with-secrets/guestbook-controller.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: guestbook 5 | spec: 6 | replicas: 3 7 | selector: 8 | matchLabels: 9 | app: guestbook 10 | template: 11 | metadata: 12 | labels: 13 | app: guestbook 14 | spec: 15 | volumes: 16 | - name: ssl 17 | hostPath: 18 | path: /etc/ssl 19 | - name: ca 20 | hostPath: 21 | path: /usr/share/ca-certificates 22 | containers: 23 | - image: quay.io/philips/guestbook:v2 24 | imagePullPolicy: Always 25 | name: guestbook 26 | volumeMounts: 27 | - mountPath: /etc/ssl 28 | name: ssl 29 | - mountPath: /usr/share/ca-certificates 30 | name: ca 31 | ports: 32 | - containerPort: 3000 33 | name: http-server 34 | env: 35 | - name: TWILIO_ACCOUNT_SID 36 | valueFrom: 37 | secretKeyRef: 38 | name: twilio 39 | key: sid 40 | - name: TWILIO_ACCOUNT_TOKEN 41 | valueFrom: 42 | secretKeyRef: 43 | name: twilio 44 | key: token 45 | 46 | -------------------------------------------------------------------------------- /guestbook/resources/guestbook-controller.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: guestbook 5 | spec: 6 | replicas: 3 7 | selector: 8 | matchLabels: 9 | app: guestbook 10 | template: 11 | metadata: 12 | labels: 13 | app: guestbook 14 | spec: 15 | containers: 16 | - image: quay.io/philips/guestbook:v1 17 | name: guestbook 18 | ports: 19 | - containerPort: 3000 20 | name: http-server 21 | 22 | -------------------------------------------------------------------------------- /guestbook/resources/guestbook-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: guestbook 6 | name: guestbook 7 | spec: 8 | ports: 9 | - port: 80 10 | targetPort: http-server 11 | selector: 12 | app: guestbook 13 | type: LoadBalancer 14 | 15 | -------------------------------------------------------------------------------- /guestbook/resources/redis-master-controller.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: ReplicaSet 3 | metadata: 4 | name: redis-master 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: redis 10 | role: master 11 | template: 12 | metadata: 13 | labels: 14 | app: redis 15 | role: master 16 | spec: 17 | containers: 18 | - image: redis:2.8.23 19 | name: redis-master 20 | ports: 21 | - containerPort: 6379 22 | name: redis-server 23 | 24 | -------------------------------------------------------------------------------- /guestbook/resources/redis-master-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: redis 6 | role: master 7 | name: redis-master 8 | spec: 9 | ports: 10 | - port: 6379 11 | targetPort: redis-server 12 | selector: 13 | app: redis 14 | role: master 15 | 16 | -------------------------------------------------------------------------------- /guestbook/resources/redis-slave-controller.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: ReplicaSet 3 | metadata: 4 | name: redis-slave 5 | spec: 6 | replicas: 2 7 | selector: 8 | matchLabels: 9 | app: redis 10 | role: slave 11 | template: 12 | metadata: 13 | labels: 14 | app: redis 15 | role: slave 16 | spec: 17 | containers: 18 | - image: kubernetes/redis-slave:v2 19 | name: redis-slave 20 | ports: 21 | - containerPort: 6379 22 | name: redis-server 23 | -------------------------------------------------------------------------------- /guestbook/resources/redis-slave-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: redis 6 | role: slave 7 | name: redis-slave 8 | spec: 9 | ports: 10 | - port: 6379 11 | targetPort: redis-server 12 | selector: 13 | app: redis 14 | role: slave 15 | 16 | -------------------------------------------------------------------------------- /guestbook/v1/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2016 The Kubernetes Authors. 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 | FROM golang:1.6.0 16 | 17 | ADD . /go/src/github.com/GoogleCloudPlatform/kubernetes/examples/guestbook-go/_src 18 | 19 | WORKDIR /go/src/github.com/GoogleCloudPlatform/kubernetes/examples/guestbook-go/ 20 | RUN cd _src/ && go get && go build -o ../bin/guestbook 21 | RUN cp _src/guestbook/Dockerfile . 22 | 23 | CMD tar cvzf - . 24 | -------------------------------------------------------------------------------- /guestbook/v1/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2016 The Kubernetes Authors. 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 | # Build the guestbook-go example 16 | 17 | # Usage: 18 | # [VERSION=v3] [REGISTRY="gcr.io/google_containers"] make build 19 | VERSION?=v3 20 | REGISTRY?=gcr.io/google_containers 21 | 22 | release: clean build push clean 23 | 24 | # builds a docker image that builds the app and packages it into a minimal docker image 25 | build: 26 | docker build --rm --force-rm -t ${REGISTRY}/guestbook-builder . 27 | docker run --rm ${REGISTRY}/guestbook-builder | docker build -t "${REGISTRY}/guestbook:${VERSION}" - 28 | 29 | # push the image to an registry 30 | push: 31 | gcloud docker push ${REGISTRY}/guestbook:${VERSION} 32 | 33 | # remove previous images and containers 34 | clean: 35 | docker rm -f ${REGISTRY}/guestbook-builder 2> /dev/null || true 36 | docker rmi -f ${REGISTRY}/guestbook-builder || true 37 | docker rmi -f "${REGISTRY}/guestbook:${VERSION}" || true 38 | 39 | .PHONY: release clean build push 40 | -------------------------------------------------------------------------------- /guestbook/v1/guestbook/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2016 The Kubernetes Authors. 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 | FROM busybox:ubuntu-14.04 16 | 17 | ADD ./bin/guestbook /app/guestbook 18 | ADD ./_src/public/index.html /app/public/index.html 19 | ADD ./_src/public/script.js /app/public/script.js 20 | ADD ./_src/public/style.css /app/public/style.css 21 | 22 | WORKDIR /app 23 | CMD ["./guestbook"] 24 | EXPOSE 3000 25 | -------------------------------------------------------------------------------- /guestbook/v1/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "encoding/json" 21 | "net/http" 22 | "os" 23 | "strings" 24 | 25 | "github.com/codegangsta/negroni" 26 | "github.com/gorilla/mux" 27 | "github.com/xyproto/simpleredis" 28 | ) 29 | 30 | var ( 31 | masterPool *simpleredis.ConnectionPool 32 | slavePool *simpleredis.ConnectionPool 33 | ) 34 | 35 | func ListRangeHandler(rw http.ResponseWriter, req *http.Request) { 36 | key := mux.Vars(req)["key"] 37 | list := simpleredis.NewList(slavePool, key) 38 | members := HandleError(list.GetAll()).([]string) 39 | membersJSON := HandleError(json.MarshalIndent(members, "", " ")).([]byte) 40 | rw.Write(membersJSON) 41 | } 42 | 43 | func ListPushHandler(rw http.ResponseWriter, req *http.Request) { 44 | key := mux.Vars(req)["key"] 45 | value := mux.Vars(req)["value"] 46 | list := simpleredis.NewList(masterPool, key) 47 | HandleError(nil, list.Add(value)) 48 | ListRangeHandler(rw, req) 49 | } 50 | 51 | func InfoHandler(rw http.ResponseWriter, req *http.Request) { 52 | info := HandleError(masterPool.Get(0).Do("INFO")).([]byte) 53 | rw.Write(info) 54 | } 55 | 56 | func EnvHandler(rw http.ResponseWriter, req *http.Request) { 57 | environment := make(map[string]string) 58 | for _, item := range os.Environ() { 59 | splits := strings.Split(item, "=") 60 | key := splits[0] 61 | val := strings.Join(splits[1:], "=") 62 | environment[key] = val 63 | } 64 | 65 | envJSON := HandleError(json.MarshalIndent(environment, "", " ")).([]byte) 66 | rw.Write(envJSON) 67 | } 68 | 69 | func HandleError(result interface{}, err error) (r interface{}) { 70 | if err != nil { 71 | panic(err) 72 | } 73 | return result 74 | } 75 | 76 | func main() { 77 | masterPool = simpleredis.NewConnectionPoolHost("redis-master:6379") 78 | defer masterPool.Close() 79 | slavePool = simpleredis.NewConnectionPoolHost("redis-slave:6379") 80 | defer slavePool.Close() 81 | 82 | r := mux.NewRouter() 83 | r.Path("/lrange/{key}").Methods("GET").HandlerFunc(ListRangeHandler) 84 | r.Path("/rpush/{key}/{value}").Methods("GET").HandlerFunc(ListPushHandler) 85 | r.Path("/info").Methods("GET").HandlerFunc(InfoHandler) 86 | r.Path("/env").Methods("GET").HandlerFunc(EnvHandler) 87 | 88 | n := negroni.Classic() 89 | n.UseHandler(r) 90 | n.Run(":3000") 91 | } 92 | -------------------------------------------------------------------------------- /guestbook/v1/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Guestbook 9 | 10 | 11 | 14 | 15 |
16 |

Waiting for database connection...

17 |
18 | 19 |
20 |
21 | 22 | Submit 23 |
24 |
25 | 26 |
27 |

28 |

/env 29 | /info

30 |
31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /guestbook/v1/public/script.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | var headerTitleElement = $("#header h1"); 3 | var entriesElement = $("#guestbook-entries"); 4 | var formElement = $("#guestbook-form"); 5 | var submitElement = $("#guestbook-submit"); 6 | var entryContentElement = $("#guestbook-entry-content"); 7 | var hostAddressElement = $("#guestbook-host-address"); 8 | 9 | var appendGuestbookEntries = function(data) { 10 | entriesElement.empty(); 11 | $.each(data, function(key, val) { 12 | entriesElement.append("

" + val + "

"); 13 | }); 14 | } 15 | 16 | var handleSubmission = function(e) { 17 | e.preventDefault(); 18 | var entryValue = entryContentElement.val() 19 | if (entryValue.length > 0) { 20 | entriesElement.append("

...

"); 21 | $.getJSON("rpush/guestbook/" + entryValue, appendGuestbookEntries); 22 | } 23 | return false; 24 | } 25 | 26 | // colors = purple, blue, red, green, yellow 27 | var colors = ["#549", "#18d", "#d31", "#2a4", "#db1"]; 28 | var randomColor = colors[Math.floor(5 * Math.random())]; 29 | (function setElementsColor(color) { 30 | headerTitleElement.css("color", color); 31 | entryContentElement.css("box-shadow", "inset 0 0 0 2px " + color); 32 | submitElement.css("background-color", color); 33 | })(randomColor); 34 | 35 | submitElement.click(handleSubmission); 36 | formElement.submit(handleSubmission); 37 | hostAddressElement.append(document.URL); 38 | 39 | // Poll every second. 40 | (function fetchGuestbook() { 41 | $.getJSON("lrange/guestbook").done(appendGuestbookEntries).always( 42 | function() { 43 | setTimeout(fetchGuestbook, 1000); 44 | }); 45 | })(); 46 | }); 47 | -------------------------------------------------------------------------------- /guestbook/v1/public/style.css: -------------------------------------------------------------------------------- 1 | body, input { 2 | color: #123; 3 | font-family: "Gill Sans", sans-serif; 4 | } 5 | 6 | div { 7 | overflow: hidden; 8 | padding: 1em 0; 9 | position: relative; 10 | text-align: center; 11 | } 12 | 13 | h1, h2, p, input, a { 14 | font-weight: 300; 15 | margin: 0; 16 | } 17 | 18 | h1 { 19 | color: #BDB76B; 20 | font-size: 3.5em; 21 | } 22 | 23 | h2 { 24 | color: #999; 25 | } 26 | 27 | form { 28 | margin: 0 auto; 29 | max-width: 50em; 30 | text-align: center; 31 | } 32 | 33 | input { 34 | border: 0; 35 | border-radius: 1000px; 36 | box-shadow: inset 0 0 0 2px #BDB76B; 37 | display: inline; 38 | font-size: 1.5em; 39 | margin-bottom: 1em; 40 | outline: none; 41 | padding: .5em 5%; 42 | width: 55%; 43 | } 44 | 45 | form a { 46 | background: #BDB76B; 47 | border: 0; 48 | border-radius: 1000px; 49 | color: #FFF; 50 | font-size: 1.25em; 51 | font-weight: 400; 52 | padding: .75em 2em; 53 | text-decoration: none; 54 | text-transform: uppercase; 55 | white-space: normal; 56 | } 57 | 58 | p { 59 | font-size: 1.5em; 60 | line-height: 1.5; 61 | } 62 | -------------------------------------------------------------------------------- /guestbook/v2/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2016 The Kubernetes Authors. 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 | FROM golang:1.6.0 16 | 17 | ADD . /go/src/github.com/GoogleCloudPlatform/kubernetes/examples/guestbook-go/_src 18 | 19 | WORKDIR /go/src/github.com/GoogleCloudPlatform/kubernetes/examples/guestbook-go/ 20 | RUN cd _src/ && go get && go build -o ../bin/guestbook 21 | RUN cp _src/guestbook/Dockerfile . 22 | 23 | CMD tar cvzf - . 24 | -------------------------------------------------------------------------------- /guestbook/v2/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2016 The Kubernetes Authors. 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 | # Build the guestbook-go example 16 | 17 | # Usage: 18 | # [VERSION=v3] [REGISTRY="gcr.io/google_containers"] make build 19 | VERSION?=v3 20 | REGISTRY?=gcr.io/google_containers 21 | 22 | release: clean build push clean 23 | 24 | # builds a docker image that builds the app and packages it into a minimal docker image 25 | build: 26 | docker build --rm --force-rm -t ${REGISTRY}/guestbook-builder . 27 | docker run --rm ${REGISTRY}/guestbook-builder | docker build -t "${REGISTRY}/guestbook:${VERSION}" - 28 | 29 | # push the image to an registry 30 | push: 31 | gcloud docker push ${REGISTRY}/guestbook:${VERSION} 32 | 33 | # remove previous images and containers 34 | clean: 35 | docker rm -f ${REGISTRY}/guestbook-builder 2> /dev/null || true 36 | docker rmi -f ${REGISTRY}/guestbook-builder || true 37 | docker rmi -f "${REGISTRY}/guestbook:${VERSION}" || true 38 | 39 | .PHONY: release clean build push 40 | -------------------------------------------------------------------------------- /guestbook/v2/guestbook/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2016 The Kubernetes Authors. 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 | FROM busybox:ubuntu-14.04 16 | 17 | ADD ./bin/guestbook /app/guestbook 18 | ADD ./_src/public/index.html /app/public/index.html 19 | ADD ./_src/public/script.js /app/public/script.js 20 | ADD ./_src/public/style.css /app/public/style.css 21 | 22 | WORKDIR /app 23 | CMD ["./guestbook"] 24 | EXPOSE 3000 25 | -------------------------------------------------------------------------------- /guestbook/v2/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "net/http" 23 | "net/url" 24 | "os" 25 | "strconv" 26 | "strings" 27 | "time" 28 | 29 | "github.com/codegangsta/negroni" 30 | "github.com/gorilla/mux" 31 | "github.com/xyproto/simpleredis" 32 | ) 33 | 34 | var ( 35 | masterPool *simpleredis.ConnectionPool 36 | slavePool *simpleredis.ConnectionPool 37 | ) 38 | 39 | func ListRangeHandler(rw http.ResponseWriter, req *http.Request) { 40 | key := mux.Vars(req)["key"] 41 | list := simpleredis.NewList(slavePool, key) 42 | members := HandleError(list.GetAll()).([]string) 43 | membersJSON := HandleError(json.MarshalIndent(members, "", " ")).([]byte) 44 | rw.Write(membersJSON) 45 | } 46 | 47 | func ListPushHandler(rw http.ResponseWriter, req *http.Request) { 48 | key := mux.Vars(req)["key"] 49 | value := mux.Vars(req)["value"] 50 | list := simpleredis.NewList(masterPool, key) 51 | HandleError(nil, list.Add(value)) 52 | ListRangeHandler(rw, req) 53 | } 54 | 55 | func InfoHandler(rw http.ResponseWriter, req *http.Request) { 56 | info := HandleError(masterPool.Get(0).Do("INFO")).([]byte) 57 | rw.Write(info) 58 | } 59 | 60 | func EnvHandler(rw http.ResponseWriter, req *http.Request) { 61 | environment := make(map[string]string) 62 | for _, item := range os.Environ() { 63 | splits := strings.Split(item, "=") 64 | key := splits[0] 65 | val := strings.Join(splits[1:], "=") 66 | environment[key] = val 67 | } 68 | 69 | envJSON := HandleError(json.MarshalIndent(environment, "", " ")).([]byte) 70 | rw.Write(envJSON) 71 | } 72 | 73 | func HandleError(result interface{}, err error) (r interface{}) { 74 | if err != nil { 75 | panic(err) 76 | } 77 | return result 78 | } 79 | 80 | func HandleTwilio() { 81 | c := time.Tick(100 * time.Millisecond) 82 | for range c { 83 | findMessages() 84 | } 85 | } 86 | 87 | type sentMessages struct { 88 | Number string 89 | Last int 90 | } 91 | 92 | func findMessages() { 93 | outbox := simpleredis.NewKeyValue(masterPool, "outbox") 94 | phoneNumbers, err := simpleredis.NewList(slavePool, "phoneNumbers").GetAll() 95 | if err != nil { 96 | fmt.Println(err) 97 | } 98 | entries, err := simpleredis.NewList(slavePool, "guestbook").GetAll() 99 | if err != nil { 100 | fmt.Println(err) 101 | } 102 | 103 | for _, n := range phoneNumbers { 104 | last, err := outbox.Get(n) 105 | if last == "" { 106 | last = "0" 107 | } 108 | l, err := strconv.Atoi(last) 109 | if err != nil { 110 | fmt.Println(err) 111 | continue 112 | } 113 | 114 | if len(entries) < l { 115 | continue 116 | } 117 | 118 | last, err = outbox.Inc(n) 119 | l, err = strconv.Atoi(last) 120 | if err != nil { 121 | fmt.Println(err) 122 | continue 123 | } 124 | 125 | for _, e := range entries[(l - 2):(l - 1)] { 126 | sendTwilio(n, e) 127 | } 128 | } 129 | } 130 | 131 | func sendTwilio(number string, msg string) { 132 | // Set initial variables 133 | accountSid := os.Getenv("TWILIO_ACCOUNT_SID") 134 | authToken := os.Getenv("TWILIO_ACCOUNT_TOKEN") 135 | if authToken == "" || accountSid == "" { 136 | fmt.Printf("empty accountSid or authToken, not using Twilio number=%v msg=%v\n", number, msg) 137 | return 138 | } 139 | urlStr := "https://api.twilio.com/2010-04-01/Accounts/" + accountSid + "/Messages.json" 140 | 141 | // Build out the data for our message 142 | v := url.Values{} 143 | v.Set("To", number) 144 | v.Set("From", "+14157874263") 145 | 146 | end := len(msg) 147 | if end > 110 { 148 | end = 110 149 | } 150 | m := msg[:end] 151 | v.Set("Body", m+" To stop reply STOP") 152 | rb := *strings.NewReader(v.Encode()) 153 | 154 | // Create client 155 | client := &http.Client{} 156 | 157 | req, _ := http.NewRequest("POST", urlStr, &rb) 158 | req.SetBasicAuth(accountSid, authToken) 159 | req.Header.Add("Accept", "application/json") 160 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 161 | 162 | // Make request 163 | resp, err := client.Do(req) 164 | if err != nil { 165 | fmt.Println(err) 166 | return 167 | } 168 | fmt.Println(resp.Status) 169 | //fmt.Printf("%v\n", req) 170 | } 171 | 172 | func main() { 173 | master := os.Getenv("REDIS_MASTER") 174 | if master == "" { 175 | master = "redis-master:6379" 176 | } 177 | masterPool = simpleredis.NewConnectionPoolHost(master) 178 | defer masterPool.Close() 179 | 180 | slave := os.Getenv("REDIS_SLAVE") 181 | if slave == "" { 182 | slave = "redis-slave:6379" 183 | } 184 | slavePool = simpleredis.NewConnectionPoolHost(slave) 185 | defer slavePool.Close() 186 | 187 | r := mux.NewRouter() 188 | r.Path("/lrange/{key}").Methods("GET").HandlerFunc(ListRangeHandler) 189 | r.Path("/rpush/{key}/{value}").Methods("GET").HandlerFunc(ListPushHandler) 190 | r.Path("/info").Methods("GET").HandlerFunc(InfoHandler) 191 | r.Path("/env").Methods("GET").HandlerFunc(EnvHandler) 192 | 193 | go HandleTwilio() 194 | 195 | n := negroni.Classic() 196 | n.UseHandler(r) 197 | n.Run(":3000") 198 | } 199 | -------------------------------------------------------------------------------- /guestbook/v2/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Guestbook 9 | 10 | 11 | 14 | 15 |
16 |

Waiting for database connection...

17 |
18 | 19 |
20 |
21 | 22 | 23 | 24 | 25 | Submit 26 |
27 |
28 | 29 |
30 |

31 |

/env 32 | /info

33 |
34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /guestbook/v2/public/script.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | var headerTitleElement = $("#header h1"); 3 | var entriesElement = $("#guestbook-entries"); 4 | var formElement = $("#guestbook-form"); 5 | var submitElement = $("#guestbook-submit"); 6 | var entryContentElement = $("#guestbook-entry-content"); 7 | var entryPhoneElement = $("#guestbook-phone-number"); 8 | var hostAddressElement = $("#guestbook-host-address"); 9 | 10 | var appendGuestbookEntries = function(data) { 11 | entriesElement.empty(); 12 | $.each(data, function(key, val) { 13 | entriesElement.append("

" + val + "

"); 14 | }); 15 | } 16 | 17 | var handleSubmission = function(e) { 18 | e.preventDefault(); 19 | var entryValue = entryContentElement.val() 20 | if (entryValue.length > 0) { 21 | entriesElement.append("

...

"); 22 | $.getJSON("rpush/guestbook/" + entryValue, appendGuestbookEntries); 23 | } 24 | var phoneNumber = entryPhoneElement.val() 25 | if (phoneNumber.length > 0) { 26 | $.getJSON("rpush/phoneNumbers/" + phoneNumber); 27 | } 28 | return false; 29 | } 30 | 31 | // colors = purple, blue, red, green, yellow 32 | var colors = ["#549", "#18d", "#d31", "#2a4", "#db1"]; 33 | var randomColor = colors[Math.floor(5 * Math.random())]; 34 | (function setElementsColor(color) { 35 | headerTitleElement.css("color", color); 36 | entryContentElement.css("box-shadow", "inset 0 0 0 2px " + color); 37 | submitElement.css("background-color", color); 38 | })(randomColor); 39 | 40 | submitElement.click(handleSubmission); 41 | formElement.submit(handleSubmission); 42 | hostAddressElement.append(document.URL); 43 | 44 | // Poll every second. 45 | (function fetchGuestbook() { 46 | $.getJSON("lrange/guestbook").done(appendGuestbookEntries).always( 47 | function() { 48 | setTimeout(fetchGuestbook, 1000); 49 | }); 50 | })(); 51 | }); 52 | -------------------------------------------------------------------------------- /guestbook/v2/public/style.css: -------------------------------------------------------------------------------- 1 | body, input { 2 | color: #123; 3 | font-family: "Gill Sans", sans-serif; 4 | } 5 | 6 | div { 7 | overflow: hidden; 8 | padding: 1em 0; 9 | position: relative; 10 | text-align: center; 11 | } 12 | 13 | h1, h2, p, input, a { 14 | font-weight: 300; 15 | margin: 0; 16 | } 17 | 18 | h1 { 19 | color: #BDB76B; 20 | font-size: 3.5em; 21 | } 22 | 23 | h2 { 24 | color: #999; 25 | } 26 | 27 | form { 28 | margin: 0 auto; 29 | max-width: 50em; 30 | text-align: center; 31 | } 32 | 33 | input { 34 | border: 0; 35 | box-shadow: inset 0 0 0 2px #BDB76B; 36 | display: float; 37 | font-size: 1.5em; 38 | margin-bottom: 1em; 39 | outline: none; 40 | padding: .5em 5%; 41 | } 42 | 43 | #guestbook-entry-content { 44 | width: 55%; 45 | } 46 | 47 | 48 | #guestbook-phone-number { 49 | width: 30%; 50 | } 51 | 52 | 53 | #guestbook-submit { 54 | display: block; 55 | } 56 | 57 | label { 58 | font-weight: bold; 59 | display: block; 60 | width: 100%; 61 | } 62 | label:after { content: ": " } 63 | 64 | form a { 65 | background: #BDB76B; 66 | border: 0; 67 | border-radius: 1000px; 68 | color: #FFF; 69 | font-size: 1.25em; 70 | font-weight: 400; 71 | padding: .75em 2em; 72 | text-decoration: none; 73 | text-transform: uppercase; 74 | white-space: normal; 75 | } 76 | 77 | p { 78 | font-size: 1.5em; 79 | line-height: 1.5; 80 | } 81 | --------------------------------------------------------------------------------