web server, kube only
This commit is contained in:
Коммит
f2caf25305
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
FROM golang:onbuild
|
||||
|
||||
MAINTAINER Andy Grunwald <andygrunwald@gmail.com>
|
||||
|
||||
EXPOSE 8082
|
||||
ENTRYPOINT ["app"]
|
|
@ -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
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
Загрузка…
Ссылка в новой задаче