Merge pull request #2 from lukaszbudnik/go-modules-gorilla-tests-json

go modules, gorilla web toolkit, changed responses to json
This commit is contained in:
Łukasz Budnik
2021-02-04 01:02:08 +01:00
committed by GitHub
10 changed files with 283 additions and 163 deletions

25
.github/workflows/go.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Go
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.15
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
debug.test
yosoy

View File

@@ -1,13 +1,13 @@
FROM golang:1.13.5-alpine3.10 as builder FROM golang:1.15.7-alpine3.13 as builder
LABEL maintainer="Łukasz Budnik lukasz.budnik@gmail.com" LABEL maintainer="Łukasz Budnik lukasz.budnik@gmail.com"
# build yosoy # build yosoy
RUN apk add git RUN apk --update add git
RUN git clone https://github.com/lukaszbudnik/yosoy.git RUN git clone https://github.com/lukaszbudnik/yosoy.git
RUN cd /go/yosoy && go build RUN cd /go/yosoy && go build
FROM alpine:3.10 FROM alpine:3.13
COPY --from=builder /go/yosoy/yosoy /bin COPY --from=builder /go/yosoy/yosoy /bin
# register entrypoint # register entrypoint

177
README.md
View File

@@ -11,19 +11,24 @@ Typical use cases include:
* testing HTTP caching * testing HTTP caching
* stubbing and prototyping distributed applications * stubbing and prototyping distributed applications
yosoy will provide information like: ## API
* Request URI yosoy responds to all requests with a JSON containing the information about:
* Hostname
* Remote IP * HTTP request:
* How many times it was called * Host
* HTTP headers * Request URI
* Env variables if `YOSOY_SHOW_ENVS` is set to `true`, `yes`, `on`, or `1` * Remote IP
* Files' contents if `YOSOY_SHOW_FILES` is set to a comma-separated list of (valid) files * HTTP headers
* HTTP proxy headers
* host:
* Hostname
* How many times it was called
* Env variables if `YOSOY_SHOW_ENVS` is set to `true`, `yes`, `on`, or `1`
* Files' contents if `YOSOY_SHOW_FILES` is set to a comma-separated list of (valid) files
See [Kubernetes example](#kubernetes-example) below. See [Kubernetes example](#kubernetes-example) below.
## Docker image ## Docker image
The docker image is available on docker hub: The docker image is available on docker hub:
@@ -36,116 +41,58 @@ It exposes HTTP service on port 80.
## Kubernetes example ## Kubernetes example
Let's take a look at a sample Kubernetes deployment file. It uses both `YOSOY_SHOW_ENVS` and `YOSOY_SHOW_FILES`. There is a sample Kubernetes deployment file in the `test` folder. It uses both `YOSOY_SHOW_ENVS` and `YOSOY_SHOW_FILES`. The deployment uses Kubernetes Downward API to expose labels and annotations as volume files which are then returned by yosoy.
> To illustrate `YOSOY_SHOW_FILES` functionality Kubernetes Downward API is used to expose labels and annotations as volume files which are then returned by yosoy. Deploy it to minikube and execute curl to the service a couple of times:
``` ```bash
apiVersion: apps/v1 # start minikube
kind: Deployment minikube start
metadata: # deploy test service
name: camarero kubectl apply -f test/deployment.yaml
labels: # tunnel to it and copy the URL as $URL variable
app.kubernetes.io/name: camarero minikube service --url camarero
spec: # call it a few times
replicas: 2 curl $URL
selector: curl $URL
matchLabels: curl $URL
app.kubernetes.io/name: camarero curl $URL
template:
metadata:
labels:
app.kubernetes.io/name: camarero
spec:
containers:
- name: yosoy
image: lukasz/yosoy
env:
- name: YOSOY_SHOW_ENVS
value: "true"
- name: YOSOY_SHOW_FILES
value: "/etc/podinfo/labels,/etc/podinfo/annotations"
ports:
- containerPort: 80
volumeMounts:
- name: podinfo
mountPath: /etc/podinfo
volumes:
- name: podinfo
downwardAPI:
items:
- path: "labels"
fieldRef:
fieldPath: metadata.labels
- path: "annotations"
fieldRef:
fieldPath: metadata.annotations
---
apiVersion: v1
kind: Service
metadata:
name: camarero
labels:
app.kubernetes.io/name: camarero
spec:
type: NodePort
selector:
app.kubernetes.io/name: camarero
ports:
- protocol: TCP
port: 80
```
Deploy above service (with 2 replicas) and execute curl to the service a couple of times:
```
kubectl apply -f test-deployment.yaml
export NODE_PORT=$(kubectl get services/camarero -o go-template='{{(index .spec.ports 0).nodePort}}')
curl $(minikube ip):$NODE_PORT
curl $(minikube ip):$NODE_PORT
curl $(minikube ip):$NODE_PORT
curl $(minikube ip):$NODE_PORT
``` ```
A sample response looks like this: A sample response looks like this:
``` ```json
Request URI: / {
Hostname: camarero-859d7c6d6b-kb5s5 "host": "127.0.0.1:53366",
Remote IP: 172.18.0.1 "requestUri": "/",
Called: 2 "remoteAddr": "172.17.0.1",
"counter": 4,
HTTP headers: "headers": {
Accept: */* "Accept": [
User-Agent: curl/7.64.1 "*/*"
],
Env variables: "User-Agent": [
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin "curl/7.64.1"
HOSTNAME=camarero-859d7c6d6b-kb5s5 ]
YOSOY_SHOW_ENVS=true },
YOSOY_SHOW_FILES=/etc/podinfo/labels,/etc/podinfo/annotations "hostname": "camarero-77787464ff-hjdkq",
CAMARERO_PORT_80_TCP_PORT=80 "envVariables": [
CAMARERO_PORT_80_TCP_ADDR=10.105.203.131 "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
KUBERNETES_PORT=tcp://10.96.0.1:443 "HOSTNAME=camarero-77787464ff-hjdkq",
KUBERNETES_PORT_443_TCP_PORT=443 "YOSOY_SHOW_ENVS=true",
CAMARERO_SERVICE_HOST=10.105.203.131 "YOSOY_SHOW_FILES=/etc/podinfo/labels,/etc/podinfo/annotations",
KUBERNETES_PORT_443_TCP_PROTO=tcp "CAMARERO_SERVICE_HOST=10.97.113.33",
KUBERNETES_SERVICE_HOST=10.96.0.1 "CAMARERO_PORT=tcp://10.97.113.33:80",
KUBERNETES_SERVICE_PORT=443 "CAMARERO_PORT_80_TCP=tcp://10.97.113.33:80",
KUBERNETES_SERVICE_PORT_HTTPS=443 "CAMARERO_PORT_80_TCP_ADDR=10.97.113.33",
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443 "CAMARERO_SERVICE_PORT=80",
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1 "CAMARERO_PORT_80_TCP_PROTO=tcp",
CAMARERO_PORT=tcp://10.105.203.131:80 "CAMARERO_PORT_80_TCP_PORT=80",
CAMARERO_SERVICE_PORT=80 "HOME=/root"
CAMARERO_PORT_80_TCP=tcp://10.105.203.131:80 ],
CAMARERO_PORT_80_TCP_PROTO=tcp "files": {
HOME=/root "/etc/podinfo/annotations": "kubernetes.io/config.seen=\"2021-02-03T13:18:34.563751030Z\"\nkubernetes.io/config.source=\"api\"",
"/etc/podinfo/labels": "app.kubernetes.io/name=\"camarero\"\npod-template-hash=\"77787464ff\""
File /etc/podinfo/labels: }
app.kubernetes.io/name="camarero" }
pod-template-hash="859d7c6d6b"
File /etc/podinfo/annotations:
kubernetes.io/config.seen="2020-11-17T07:38:15.374049163Z"
kubernetes.io/config.source="api"
``` ```

15
TestDockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM golang:1.15.7-alpine3.13 as builder
LABEL maintainer="Łukasz Budnik lukasz.budnik@gmail.com"
# build yosoy
ADD . /go/yosoy
RUN cd /go/yosoy && go build
FROM alpine:3.13
COPY --from=builder /go/yosoy/yosoy /bin
# register entrypoint
ENTRYPOINT ["yosoy"]
EXPOSE 80

9
go.mod Normal file
View File

@@ -0,0 +1,9 @@
module github.com/lukaszbudnik/yosoy
go 1.15
require (
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/stretchr/testify v1.7.0
)

16
go.sum Normal file
View File

@@ -0,0 +1,16 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

104
server.go
View File

@@ -1,76 +1,90 @@
package main package main
import ( import (
"fmt" "encoding/json"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"os" "os"
"sort"
"strings" "strings"
"time"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
) )
type response struct {
Host string `json:"host"`
RequestURI string `json:"requestUri"`
RemoteAddr string `json:"remoteAddr"`
Counter int `json:"counter"`
Headers map[string][]string `json:"headers"`
Hostname string `json:"hostname"`
EnvVariables []string `json:"envVariables,omitempty"`
Files map[string]string `json:"files,omitempty"`
}
var counter = 0 var counter = 0
var hostname = os.Getenv("HOSTNAME") var hostname = os.Getenv("HOSTNAME")
var showEnvs = os.Getenv("YOSOY_SHOW_ENVS")
var showFiles = os.Getenv("YOSOY_SHOW_FILES")
func handler(w http.ResponseWriter, req *http.Request) { func handler(w http.ResponseWriter, req *http.Request) {
if req.RequestURI == "/favicon.ico" { showEnvs := os.Getenv("YOSOY_SHOW_ENVS")
w.WriteHeader(http.StatusNotFound) showFiles := os.Getenv("YOSOY_SHOW_FILES")
return
} response := &response{}
remoteAddr := req.RemoteAddr
if index := strings.LastIndex(remoteAddr, ":"); index > 0 {
remoteAddr = remoteAddr[0:index]
}
fmt.Printf("[%v] - %v - %v - \"%v %v\"\n", hostname, time.Now().Format(time.RFC3339), remoteAddr, req.Method, req.RequestURI)
w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "text/plain")
fmt.Fprintf(w, "Request URI: %v\n", req.RequestURI)
fmt.Fprintf(w, "Hostname: %v\n", hostname)
fmt.Fprintf(w, "Remote IP: %v\n", remoteAddr)
counter++ counter++
fmt.Fprintf(w, "Called: %v\n", counter) response.Counter = counter
fmt.Fprintln(w)
fmt.Fprintf(w, "HTTP headers:\n") remoteAddr := remoteAddrWithoutPort(req)
headers := make([]string, 0, len(req.Header)) response.RemoteAddr = remoteAddr
for k := range req.Header {
headers = append(headers, k) response.RequestURI = req.RequestURI
} response.Host = req.Host
sort.Strings(headers) response.Headers = req.Header
for _, header := range headers {
headers := req.Header[header] response.Hostname = hostname
for _, h := range headers {
fmt.Fprintf(w, "%v: %v\n", header, h)
}
}
if strings.ToLower(showEnvs) == "true" || strings.ToLower(showEnvs) == "yes" || strings.ToLower(showEnvs) == "on" || showEnvs == "1" { if strings.ToLower(showEnvs) == "true" || strings.ToLower(showEnvs) == "yes" || strings.ToLower(showEnvs) == "on" || showEnvs == "1" {
fmt.Fprintln(w) response.EnvVariables = os.Environ()
fmt.Fprintf(w, "Env variables:\n")
for _, e := range os.Environ() {
fmt.Fprintln(w, e)
}
} }
if len(showFiles) > 0 { if len(showFiles) > 0 {
response.Files = make(map[string]string)
files := strings.Split(showFiles, ",") files := strings.Split(showFiles, ",")
for _, file := range files { for _, file := range files {
bytes, err := ioutil.ReadFile(file) bytes, err := ioutil.ReadFile(file)
if err != nil { if err != nil {
fmt.Printf("[%v] - %v - could not read file %v: %v\n", hostname, time.Now().Format(time.RFC3339), file, err) log.Printf("Could not read file %v: %v\n", file, err)
continue continue
} }
fmt.Fprintln(w)
fmt.Fprintf(w, "File %v:\n", file)
contents := string(bytes) contents := string(bytes)
fmt.Fprintln(w, contents) response.Files[file] = contents
} }
} }
w.Header().Add("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
}
func remoteAddrWithoutPort(req *http.Request) string {
remoteAddr := req.RemoteAddr
if index := strings.LastIndex(remoteAddr, ":"); index > 0 {
remoteAddr = remoteAddr[0:index]
}
return remoteAddr
} }
func main() { func main() {
fmt.Printf("[%v] - %v - yosoy is up!\n", hostname, time.Now().Format(time.RFC3339)) log.Printf("yosoy is up %v\n", hostname)
http.HandleFunc("/", handler)
http.ListenAndServe(":80", nil) r := mux.NewRouter()
r.Handle("/favicon.ico", r.NotFoundHandler)
r.PathPrefix("/").HandlerFunc(handler)
loggingRouter := handlers.CombinedLoggingHandler(os.Stdout, r)
proxyRouter := handlers.ProxyHeaders(loggingRouter)
recoveryRouter := handlers.RecoveryHandler()(proxyRouter)
http.ListenAndServe(":80", recoveryRouter)
} }

39
server_test.go Normal file
View File

@@ -0,0 +1,39 @@
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestHandler(t *testing.T) {
os.Setenv("YOSOY_SHOW_ENVS", "true")
os.Setenv("YOSOY_SHOW_FILES", ".gitignore")
req, err := http.NewRequest("GET", "https://example.org/sample/path", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Accept", "*/*")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(handler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
var response response
json.Unmarshal(rr.Body.Bytes(), &response)
assert.Equal(t, 1, response.Counter)
assert.Equal(t, "example.org", response.Host)
assert.NotEmpty(t, response.EnvVariables)
assert.NotEmpty(t, response.Files[".gitignore"])
}

53
test/deployment.yaml Normal file
View File

@@ -0,0 +1,53 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: camarero
labels:
app.kubernetes.io/name: camarero
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: camarero
template:
metadata:
labels:
app.kubernetes.io/name: camarero
spec:
containers:
- name: yosoy
image: lukasz/yosoy
env:
- name: YOSOY_SHOW_ENVS
value: "true"
- name: YOSOY_SHOW_FILES
value: "/etc/podinfo/labels,/etc/podinfo/annotations"
ports:
- containerPort: 80
volumeMounts:
- name: podinfo
mountPath: /etc/podinfo
volumes:
- name: podinfo
downwardAPI:
items:
- path: "labels"
fieldRef:
fieldPath: metadata.labels
- path: "annotations"
fieldRef:
fieldPath: metadata.annotations
---
apiVersion: v1
kind: Service
metadata:
name: camarero
labels:
app.kubernetes.io/name: camarero
spec:
type: NodePort
selector:
app.kubernetes.io/name: camarero
ports:
- protocol: TCP
port: 80