This commit is contained in:
Divyansh Manchanda 2019-06-09 22:52:48 +05:30
Коммит f2caf25305
9 изменённых файлов: 546 добавлений и 0 удалений

28
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,28 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
# Application
simple-webserver
output.html

6
Dockerfile Normal file
Просмотреть файл

@ -0,0 +1,6 @@
FROM golang:onbuild
MAINTAINER Andy Grunwald <andygrunwald@gmail.com>
EXPOSE 8082
ENTRYPOINT ["app"]

28
doc.go Normal file
Просмотреть файл

@ -0,0 +1,28 @@
// A small webserver for testing various technologies, techniques and concepts.
//
// This project enables you to learn new technologies by try them in
// a small and simple environment to get a first idea how things are working.
//
// This part is responsible to show the standard way of documentation
// of golang. With the idea to inline the documentation, code and docs are one.
// This minimize the risk that documentation get out of date quickly.
//
// Notice that this is only a a small and not production ready demo.
// If you want to deep dive into the topic of golang + documentation
// i suggest to checkout:
//
// https://blog.golang.org/godoc-documenting-go-code
//
// https://godoc.org/golang.org/x/tools/cmd/godoc
//
// https://github.com/fluhus/godoc-tricks
//
// https://github.com/golang/gddo
//
// If you have any suggestion or comment, please feel free to open an issue on
// this the GitHub page of this project!
//
// More information and details can be found there as well.
// Checkout https://github.com/andygrunwald/simple-webserver
//
package main

14
docker-compose.yml Normal file
Просмотреть файл

@ -0,0 +1,14 @@
version: '2'
services:
redis:
image: redis:3-alpine
simple-webserver:
image: andygrunwald/simple-webserver:v1.1.1
command: -redis "redis:6379"
depends_on:
- redis
links:
- redis
ports:
- "8082:8082"

79
kubernetes.yaml Normal file
Просмотреть файл

@ -0,0 +1,79 @@
apiVersion: v1
kind: Service
metadata:
name: simple-webserver
labels:
app: simple-webserver
namespace: default
spec:
type: NodePort
ports:
- port: 8082
selector:
app: simple-webserver
tier: frontend
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: simple-webserver
labels:
app: simple-webserver
spec:
template:
metadata:
labels:
app: simple-webserver
tier: frontend
spec:
containers:
- image: andygrunwald/simple-webserver:v1.1.1
name: simple-webserver
command: ["app", "-redis", "simple-webserver-redis:6379"]
ports:
- containerPort: 8082
---
apiVersion: v1
kind: Service
metadata:
name: simple-webserver-redis
labels:
app: simple-webserver
spec:
ports:
- port: 6379
selector:
app: simple-webserver
tier: redis
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: simple-webserver-redis
labels:
app: simple-webserver
spec:
template:
metadata:
labels:
app: simple-webserver
tier: redis
spec:
containers:
- image: redis:3-alpine
name: redis
ports:
- containerPort: 6379
name: redis
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
labels:
app: simple-webserver
name: simple-webserver
namespace: default
spec:
backend:
serviceName: simple-webserver
servicePort: 8082

143
main.go Normal file
Просмотреть файл

@ -0,0 +1,143 @@
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"io/ioutil"
"github.com/gorilla/handlers"
)
const (
// Name of the application
Name = "simple webserver"
// Version of the application
Version = "1.1.1"
)
func main() {
var (
listenFlag = flag.String("listen", EnvOrDefault("SIMPLE_WEBSERVER_LISTEN", ":8082"), "Address + Port to listen on. Format ip:port. Environment variable: SIMPLE_WEBSERVER_LISTEN")
redisFlag = flag.String("redis", EnvOrDefault("SIMPLE_WEBSERVER_REDIS", ":6379"), "Address + Port where a redis server is listening. Environment variable: SIMPLE_WEBSERVER_REDIS")
)
flag.Parse()
// Create Redis storage
r := NewRedisStorage(*redisFlag)
// Define HTTP endpoints
s := http.NewServeMux()
s.HandleFunc("/", RootHandler)
s.HandleFunc("/ping", PingHandler(r))
s.HandleFunc("/kill", KillHandler)
s.HandleFunc("/version", VersionHandler)
s.HandleFunc("/payload", PayloadHandler)
// Bootstrap logger
logger := log.New(os.Stdout, "", log.LstdFlags)
logger.Printf("Starting webserver and listen on %s", *listenFlag)
// Start HTTP Server with request logging
loggingHandler := handlers.LoggingHandler(os.Stdout, s)
log.Fatal(http.ListenAndServe(*listenFlag, loggingHandler))
}
// RootHandler handles requests to the "/" path.
// It will redirect the request to /ping with a 303 HTTP header
func RootHandler(resp http.ResponseWriter, req *http.Request) {
http.Redirect(resp, req, "/ping", http.StatusSeeOther)
}
// PingHandler handles request to the "/ping" endpoint.
// It will send a PING request to Redis and return the response
// of the NoSQL database.
// The response is obvious: "pong" :)
func PingHandler(s Storage) http.HandlerFunc {
return func(resp http.ResponseWriter, req *http.Request) {
res, err := s.Ping()
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(resp, err.Error())
return
}
resp.WriteHeader(http.StatusOK)
fmt.Fprintln(resp, res)
}
}
// KillHandler handles request to the "/kill" endpoint.
// Will shut down the webserver immediately (via exit code 1).
// Only DELETE requests are accepted.
// Other request methods will throw a HTTP Status Code 405 (Method Not Allowed)
func KillHandler(resp http.ResponseWriter, req *http.Request) {
if req.Method != "DELETE" {
resp.WriteHeader(http.StatusMethodNotAllowed)
return
}
// We need to send a HTTP Status Code 200 (OK)
// to respond that we have accepted the request.
// Here we send a chunked response to the requester.
flusher, ok := resp.(http.Flusher)
if !ok {
resp.WriteHeader(http.StatusInternalServerError)
return
}
resp.WriteHeader(http.StatusOK)
flusher.Flush()
// And we kill the server (like requested)
os.Exit(1)
}
// VersionHandler handles request to the "/version" endpoint.
// It prints the Name and Version of this app.
func VersionHandler(resp http.ResponseWriter, req *http.Request) {
resp.WriteHeader(http.StatusOK)
fmt.Fprintf(resp, "%s v%s\n", Name, Version)
}
// PayloadHandler handles request to the "/payload" endpoint.
// It is a debug route to dump the complete request incl. method, header and body.
func PayloadHandler(resp http.ResponseWriter, req *http.Request) {
body, err := ioutil.ReadAll(req.Body)
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return
}
resp.WriteHeader(http.StatusOK)
log.Printf("Method: %s\n", req.Method)
fmt.Fprintf(resp, "Method: %s\n", req.Method)
if len(req.Header) > 0 {
log.Println("Headers:")
fmt.Fprint(resp, "Headers:\n")
for key, values := range req.Header {
for _, val := range values {
log.Printf("%s: %s\n", key, val)
fmt.Fprintf(resp, "%s: %s\n", key, val)
}
}
}
log.Printf("Payload: %s", string(body))
fmt.Fprintf(resp, "Payload: %s", string(body))
}
// EnvOrDefault will read env from the environment.
// If the environment variable is not set in the environment
// fallback will be returned.
// This function can be used as a value for flag.String to enable
// env var support for your binary flags.
func EnvOrDefault(env, fallback string) string {
value := fallback
if v := os.Getenv(env); v != "" {
value = v
}
return value
}

141
main_test.go Normal file
Просмотреть файл

@ -0,0 +1,141 @@
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"testing"
)
type dummyStorage struct{}
func (s *dummyStorage) Ping() (string, error) {
return "PONG", nil
}
func TestPingHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/ping", nil)
if err != nil {
t.Fatal(err)
}
backend := &dummyStorage{}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(PingHandler(backend))
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Wrong status code: got %v, expected %v", status, http.StatusOK)
}
expected := "PONG\n"
if rr.Body.String() != expected {
t.Errorf("Unexpected body: got %q, want %q", rr.Body.String(), expected)
}
}
type dummyErrorStorage struct{}
func (s *dummyErrorStorage) Ping() (string, error) {
return "", fmt.Errorf("Connection timeout to storage backend")
}
func TestPingHandler_Error(t *testing.T) {
req, err := http.NewRequest("GET", "/ping", nil)
if err != nil {
t.Fatal(err)
}
backend := &dummyErrorStorage{}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(PingHandler(backend))
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusInternalServerError {
t.Errorf("Wrong status code: got %v, expected %v", status, http.StatusInternalServerError)
}
expected := "Connection timeout to storage backend"
if rr.Body.String() != expected {
t.Errorf("Unexpected body: got %q, want %q", rr.Body.String(), expected)
}
}
func TestVersionHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/version", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(VersionHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Wrong status code: got %v, expected %v", status, http.StatusOK)
}
expected := fmt.Sprintf("%s v%s\n", Name, Version)
if rr.Body.String() != expected {
t.Errorf("Unexpected body: got %q, want %q", rr.Body.String(), expected)
}
}
func TestKillHandler(t *testing.T) {
// Here we only test if the end point kills itself.
if os.Getenv("KILL_ENDPOINT") == "1" {
req, err := http.NewRequest("DELETE", "/kill", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(KillHandler)
handler.ServeHTTP(rr, req)
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestKillHandler")
cmd.Env = append(os.Environ(), "KILL_ENDPOINT=1")
err := cmd.Run()
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
return
}
t.Errorf("Process ran with err %v, want exit status 1", err)
}
func TestKillHandler_WrongMethod(t *testing.T) {
req, err := http.NewRequest("POST", "/kill", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(KillHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusMethodNotAllowed {
t.Errorf("Wrong status code: got %v, expected %v", status, http.StatusMethodNotAllowed)
}
}
func TestEnvOrDefault_Fallback(t *testing.T) {
expected := "dummy"
got := EnvOrDefault("TEST_DEFAULT_ENV", expected)
if expected != got {
t.Errorf("Wrong fallback value: got %v, expected %v", got, expected)
}
}
func TestEnvOrDefault_WithEnv(t *testing.T) {
expected := "dummy-from-env"
os.Setenv("TEST_SET_ENV", expected)
got := EnvOrDefault("TEST_SET_ENV", "dummy-fallback")
if expected != got {
t.Errorf("Wrong env value: got %v, expected %v", got, expected)
}
}

56
redis.go Normal file
Просмотреть файл

@ -0,0 +1,56 @@
package main
import (
"time"
"github.com/garyburd/redigo/redis"
)
// Storage is the basic interface to communicate
// with different storage endpoints.
type Storage interface {
// Ping will "ping" the storage backend.
// The function can use to check the connection from
// the app to the storage backend.
Ping() (string, error)
}
// RedisStorage reflects the Storage with
// a Redis NoSQL Server as backend
// See http://redis.io/
type RedisStorage struct {
connectionPool *redis.Pool
}
// NewRedisStorage will return a new Storage type
// with Redis as backend
func NewRedisStorage(server string) Storage {
return &RedisStorage{
connectionPool: &redis.Pool{
MaxIdle: 3,
IdleTimeout: 240 * time.Second,
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", server)
if err != nil {
return nil, err
}
return c, err
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
_, err := c.Do("PING")
return err
},
},
}
}
// Ping will "ping" the storage backend.
// The function can use to check the connection from
// the app to the storage backend.
func (r *RedisStorage) Ping() (string, error) {
conn := r.connectionPool.Get()
defer conn.Close()
res, err := redis.String(conn.Do("PING"))
return res, err
}

51
scripts/build-release Normal file
Просмотреть файл

@ -0,0 +1,51 @@
#!/bin/sh -e
#
# This script is inspired by coreos/etcd.
# The credits goes to coreos.
# This version is only a (slightly) modified version.
#
# Thanks to open source and thanks to CoreOS / etcd.
#
# @link https://github.com/coreos/etcd/blob/master/scripts/build-release
#
VER=$1
PROJ="simple-webserver"
if [ -z "$1" ]; then
echo "Usage: ${0} VERSION" >> /dev/stderr
exit 255
fi
set -u
function main {
mkdir release
for os in darwin windows linux freebsd; do
export GOOS=${os}
export GOARCH="amd64"
TARGET="simple-webserver-${VER}-${GOOS}-${GOARCH}"
mkdir ${TARGET}
cp README.md ${TARGET}/README.md
local ext=""
if [ ${GOOS} == "windows" ]; then
ext=".exe"
fi
go build -o ${TARGET}/simple-webserver${ext}
if [ ${GOOS} == "linux" ]; then
tar cfz release/${TARGET}.tar.gz ${TARGET}
echo "Wrote release/${TARGET}.tar.gz"
else
zip -qr release/${TARGET}.zip ${TARGET}
echo "Wrote release/${TARGET}.zip"
fi
done
}
main