Compare commits
49 Commits
v2.1.0
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5444af8bf1 | ||
|
|
906be51244 | ||
|
|
5651f7227f | ||
|
|
bfb0a19737 | ||
|
|
f6cd6893fd | ||
|
|
1d44df4ba9 | ||
|
|
3ccac4010f | ||
|
|
35bf0249ad | ||
|
|
046b2040c3 | ||
|
|
6d914f6e51 | ||
|
|
bf6a62c004 | ||
|
|
4a5cc84585 | ||
|
|
5cc4cc573f | ||
|
|
be35c96a78 | ||
|
|
206a37ccfb | ||
|
|
e9a1cb3340 | ||
|
|
270c92f038 | ||
|
|
d8e6d8a770 | ||
|
|
dbe56d6684 | ||
|
|
fe4ccaac6d | ||
|
|
593c42fba7 | ||
|
|
d8a01b4f77 | ||
|
|
0245cda959 | ||
|
|
20a8b270e8 | ||
|
|
2a3c6806b5 | ||
|
|
b207586411 | ||
|
|
ac599cd5d3 | ||
|
|
f35c750b60 | ||
|
|
1f120f6f61 | ||
|
|
0697b7feca | ||
|
|
35f72dbaad | ||
|
|
6c09411f4e | ||
|
|
26a1d3c829 | ||
|
|
3a3b2df8b7 | ||
|
|
04824c18f0 | ||
|
|
2f942c3ee7 | ||
|
|
40780e79ee | ||
|
|
2268784739 | ||
|
|
53bbc21af9 | ||
|
|
490e0e43f0 | ||
|
|
68691a718a | ||
|
|
605de812c3 | ||
|
|
48cb758883 | ||
|
|
196306d131 | ||
|
|
c5415206e6 | ||
|
|
66b4078631 | ||
|
|
d14e46e839 | ||
|
|
21ffd8ef65 | ||
|
|
75cd57556b |
18
.github/workflows/docker-image.yml
vendored
18
.github/workflows/docker-image.yml
vendored
@@ -7,6 +7,20 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3.1.0
|
- uses: actions/checkout@v4.1.1
|
||||||
- name: Build the Docker image
|
- name: Build the Docker image
|
||||||
run: docker build . --file Dockerfile --tag yosoy-local:$(date +%s)
|
run: docker build . --file Dockerfile --tag yosoy-local:latest
|
||||||
|
- name: Run simple integration test
|
||||||
|
run: |
|
||||||
|
docker run -p 3333:80 yosoy-local:latest > yosoy.log &
|
||||||
|
sleep 5
|
||||||
|
RESULT=$(curl -s -X DELETE -H 'x-api-key: abc123' 'http://0.0.0.0:3333/sample/path?with=params')
|
||||||
|
echo "$RESULT"
|
||||||
|
if [[ $RESULT =~ '"method":"DELETE"' ]]
|
||||||
|
then
|
||||||
|
echo 'Test successful'
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo 'Test failure'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|||||||
10
.github/workflows/docker-publish.yml
vendored
10
.github/workflows/docker-publish.yml
vendored
@@ -15,16 +15,16 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v3.1.0
|
uses: actions/checkout@v4.1.1
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
lukasz/yosoy
|
lukasz/yosoy
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
|
|
||||||
- name: Build and push Docker images
|
- name: Build and push Docker images
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
|||||||
6
.github/workflows/go.yml
vendored
6
.github/workflows/go.yml
vendored
@@ -6,12 +6,12 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3.1.0
|
- uses: actions/checkout@v4.1.1
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.16
|
go-version: 1.19
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build -v ./...
|
run: go build -v ./...
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
debug.test
|
debug.test
|
||||||
|
cover.out
|
||||||
yosoy
|
yosoy
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
FROM golang:1.17.3-alpine3.13 as builder
|
FROM golang:1.21-alpine as builder
|
||||||
|
|
||||||
LABEL maintainer="Łukasz Budnik lukasz.budnik@gmail.com"
|
LABEL maintainer="Łukasz Budnik lukasz.budnik@gmail.com"
|
||||||
|
|
||||||
|
# install prerequisites
|
||||||
|
RUN apk update && apk add git
|
||||||
|
|
||||||
# build yosoy
|
# build yosoy
|
||||||
ADD . /go/yosoy
|
ADD . /go/yosoy
|
||||||
|
RUN go env -w GOPROXY=direct
|
||||||
RUN cd /go/yosoy && go build
|
RUN cd /go/yosoy && go build
|
||||||
|
|
||||||
FROM alpine:3.16.2
|
FROM alpine:3.18
|
||||||
COPY --from=builder /go/yosoy/yosoy /bin
|
COPY --from=builder /go/yosoy/yosoy /bin
|
||||||
|
|
||||||
# register entrypoint
|
# register entrypoint
|
||||||
|
|||||||
92
README.md
92
README.md
@@ -1,15 +1,18 @@
|
|||||||
# yosoy  
|
# yosoy  
|
||||||
|
|
||||||
yosoy is a HTTP service for stubbing and prototyping distributed applications. It is a service which will introduce itself to the caller and print some useful information about its environment. "Yo soy" in español means "I am".
|
yosoy is an HTTP service for stubbing and prototyping distributed applications. It is a service that introduces itself to the caller and prints useful information about its runtime environment.
|
||||||
|
|
||||||
yosoy is extremely useful when creating a distributed application stub and you need to see more meaningful responses than a default nginx welcome page.
|
yosoy is extremely useful when creating a stub for a distributed application, as it provides more meaningful responses than, for example, a default nginx welcome page. Further, yosoy incorporates a built-in reachability analyzer to facilitate troubleshooting connectivity issues in distributed systems. A dedicated reachability analyzer endpoint validates network connectivity between yosoy and remote endpoints.
|
||||||
|
|
||||||
Typical use cases include:
|
Typical use cases include:
|
||||||
|
|
||||||
- testing HTTP routing & ingress
|
- Testing HTTP routing and ingress
|
||||||
- testing HTTP load balancing
|
- Testing HTTP load balancing
|
||||||
- testing HTTP caching
|
- Testing HTTP caching
|
||||||
- stubbing and prototyping distributed applications
|
- Executing reachability analysis
|
||||||
|
- Stubbing and prototyping distributed applications
|
||||||
|
|
||||||
|
"Yo soy" means "I am" in Spanish.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
@@ -31,19 +34,25 @@ yosoy responds to all requests with a JSON containing the information about:
|
|||||||
- Env variables if `YOSOY_SHOW_ENVS` is set to `true`, `yes`, `on`, or `1`
|
- 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
|
- Files' contents if `YOSOY_SHOW_FILES` is set to a comma-separated list of (valid) files
|
||||||
|
|
||||||
Checkout out [Sample JSON response](#sample-json-response) below to see how useful yosoy is when troubleshooting/stubbing/prototyping distributed applications.
|
Check [Sample JSON response](#sample-json-response) to see how you can use yosoy for stubbing/prototyping/troubleshooting distributed applications.
|
||||||
|
|
||||||
|
## ping/reachability analyzer
|
||||||
|
|
||||||
|
yosoy includes a simple ping/reachability analyzer. You can use this functionality when prototyping distributed systems to validate whether a given component can reach a specific endpoint. yosoy exposes a dedicated `/_/yosoy/ping` endpoint which accepts the following 4 query parameters:
|
||||||
|
|
||||||
|
* `h` - required - hostname of the endpoint
|
||||||
|
* `p` - required - port of the endpoint
|
||||||
|
* `n` - optional - network, all valid Go networks are supported (including the most popular ones like `tcp`, `udp`, IPv4, IPV6, etc.). If `n` parameter is not provided, it defaults to `tcp`. If `n` parameter is set to unknown network, an error will be returned.
|
||||||
|
* `t` - optional - timeout in seconds. If `t` parameter is not provided, it defaults to `10`. If `t` contains invalid integer literal, an error will be returned.
|
||||||
|
|
||||||
|
Check [Sample ping/reachability analyzer responses](#sample-pingreachability-analyzer-responses) to see how you can use yosoy for troubleshooting network connectivity.
|
||||||
|
|
||||||
## Docker image
|
## Docker image
|
||||||
|
|
||||||
The docker image is available on docker hub:
|
The docker image is available on docker hub and ghcr.io:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker pull lukasz/yosoy
|
docker pull lukasz/yosoy
|
||||||
```
|
|
||||||
|
|
||||||
and ghcr.io:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker pull ghcr.io/lukaszbudnik/yosoy
|
docker pull ghcr.io/lukaszbudnik/yosoy
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -51,7 +60,7 @@ It exposes HTTP service on port 80.
|
|||||||
|
|
||||||
## Kubernetes example
|
## Kubernetes example
|
||||||
|
|
||||||
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.
|
There is a sample Kubernetes deployment file in the `test` folder. It uses both `YOSOY_SHOW_ENVS` and `YOSOY_SHOW_FILES` features. The deployment uses Kubernetes Downward API 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:
|
Deploy it to minikube and execute curl to the service a couple of times:
|
||||||
|
|
||||||
@@ -131,3 +140,58 @@ A sample yosoy JSON response to a request made from a single page application (S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Sample ping/reachability analyzer responses
|
||||||
|
|
||||||
|
To test if yosoy can connect to `google.com` on port `443` using default `tcp` network use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -v "http://localhost/_/yosoy/ping?h=google.com&p=443"
|
||||||
|
> GET /_/yosoy/ping?h=google.com&p=443 HTTP/1.1
|
||||||
|
> Host: localhost
|
||||||
|
> User-Agent: curl/7.86.0
|
||||||
|
> Accept: */*
|
||||||
|
>
|
||||||
|
< HTTP/1.1 200 OK
|
||||||
|
< Date: Fri, 17 Nov 2023 05:54:36 GMT
|
||||||
|
< Content-Length: 29
|
||||||
|
< Content-Type: text/plain; charset=utf-8
|
||||||
|
<
|
||||||
|
{"message":"ping succeeded"}
|
||||||
|
```
|
||||||
|
|
||||||
|
To see an unsuccessful response you may use localhost with some random port number:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -v "http://localhost/_/yosoy/ping?h=127.0.0.1&p=12345"
|
||||||
|
> GET /_/yosoy/ping?h=127.0.0.1&p=12345 HTTP/1.1
|
||||||
|
> Host: localhost
|
||||||
|
> User-Agent: curl/7.86.0
|
||||||
|
> Accept: */*
|
||||||
|
>
|
||||||
|
< HTTP/1.1 500 Internal Server Error
|
||||||
|
< Date: Fri, 17 Nov 2023 05:53:48 GMT
|
||||||
|
< Content-Length: 66
|
||||||
|
< Content-Type: text/plain; charset=utf-8
|
||||||
|
<
|
||||||
|
{"error":"dial tcp 127.0.0.1:12345: connect: connection refused"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building and testing locally
|
||||||
|
|
||||||
|
Here are some commands to get you started.
|
||||||
|
|
||||||
|
Run yosoy directly on port 80.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test -coverprofile cover.out
|
||||||
|
go tool cover -html=cover.out
|
||||||
|
go run server.go
|
||||||
|
```
|
||||||
|
|
||||||
|
Building local Docker container and run it on port 8080:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t yosoy-local:latest .
|
||||||
|
docker run --rm --name yosoy-local -p 8080:80 yosoy-local:latest
|
||||||
|
```
|
||||||
15
go.mod
15
go.mod
@@ -1,9 +1,16 @@
|
|||||||
module github.com/lukaszbudnik/yosoy
|
module github.com/lukaszbudnik/yosoy
|
||||||
|
|
||||||
go 1.16
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gorilla/handlers v1.5.1
|
github.com/gorilla/handlers v1.5.2
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/stretchr/testify v1.8.0
|
github.com/stretchr/testify v1.8.4
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
21
go.sum
21
go.sum
@@ -1,21 +1,16 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
85
server.go
85
server.go
@@ -2,16 +2,21 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const PING_DEFAULT_TIMEOUT = 10
|
||||||
|
const PING_DEFAULT_NETWORK = "tcp"
|
||||||
|
|
||||||
type response struct {
|
type response struct {
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Proto string `json:"proto"`
|
Proto string `json:"proto"`
|
||||||
@@ -27,6 +32,14 @@ type response struct {
|
|||||||
Files map[string]string `json:"files,omitempty"`
|
Files map[string]string `json:"files,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type errorResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type successResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
var counter = 0
|
var counter = 0
|
||||||
var hostname = os.Getenv("HOSTNAME")
|
var hostname = os.Getenv("HOSTNAME")
|
||||||
|
|
||||||
@@ -71,7 +84,7 @@ func handler(w http.ResponseWriter, req *http.Request) {
|
|||||||
response.Files = make(map[string]string)
|
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 := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Could not read file %v: %v\n", file, err)
|
log.Printf("Could not read file %v: %v\n", file, err)
|
||||||
continue
|
continue
|
||||||
@@ -86,6 +99,73 @@ func handler(w http.ResponseWriter, req *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(response)
|
json.NewEncoder(w).Encode(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ping(w http.ResponseWriter, req *http.Request) {
|
||||||
|
// get h, p, n, t parameters from query string
|
||||||
|
hostname := req.URL.Query().Get("h")
|
||||||
|
port := req.URL.Query().Get("p")
|
||||||
|
network := req.URL.Query().Get("n")
|
||||||
|
timeoutString := req.URL.Query().Get("t")
|
||||||
|
var timeout int64 = PING_DEFAULT_TIMEOUT
|
||||||
|
|
||||||
|
// return HTTP BadRequest when hostname is empty
|
||||||
|
if hostname == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Add("Content-Type", "application/json; charset=utf-8")
|
||||||
|
json.NewEncoder(w).Encode(&errorResponse{"hostname is empty"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// return HTTP BadRequest when port is empty
|
||||||
|
if port == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Add("Content-Type", "application/json; charset=utf-8")
|
||||||
|
json.NewEncoder(w).Encode(&errorResponse{"port is empty"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// check if timeoutString is a valid int64, return HTTP BadRequest when invalid, otherwise set timeout value
|
||||||
|
if timeoutString != "" {
|
||||||
|
timeoutInt, err := strconv.ParseInt(timeoutString, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Add("Content-Type", "application/json; charset=utf-8")
|
||||||
|
json.NewEncoder(w).Encode(&errorResponse{"timeout is invalid"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timeout = timeoutInt
|
||||||
|
}
|
||||||
|
|
||||||
|
// if network is empty set default to tcp
|
||||||
|
if network == "" {
|
||||||
|
network = PING_DEFAULT_NETWORK
|
||||||
|
}
|
||||||
|
|
||||||
|
// ping the hostname and port
|
||||||
|
err := pingHost(hostname, port, network, timeout)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Header().Add("Content-Type", "application/json; charset=utf-8")
|
||||||
|
json.NewEncoder(w).Encode(&errorResponse{err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// return HTTP OK
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Header().Add("Content-Type", "application/json; charset=utf-8")
|
||||||
|
json.NewEncoder(w).Encode(&successResponse{"ping succeeded"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func pingHost(hostname, port, network string, timeout int64) error {
|
||||||
|
// create timeoutDuration variable of Duration type using timeout as the value in seconds
|
||||||
|
timeoutDuration := time.Duration(timeout) * time.Second
|
||||||
|
|
||||||
|
// open a socket to hostname and port
|
||||||
|
conn, err := net.DialTimeout(network, hostname+":"+port, timeoutDuration)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// close the socket
|
||||||
|
conn.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func remoteAddrWithoutPort(req *http.Request) string {
|
func remoteAddrWithoutPort(req *http.Request) string {
|
||||||
remoteAddr := req.RemoteAddr
|
remoteAddr := req.RemoteAddr
|
||||||
if index := strings.LastIndex(remoteAddr, ":"); index > 0 {
|
if index := strings.LastIndex(remoteAddr, ":"); index > 0 {
|
||||||
@@ -100,6 +180,7 @@ func main() {
|
|||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
|
|
||||||
r.Handle("/favicon.ico", r.NotFoundHandler)
|
r.Handle("/favicon.ico", r.NotFoundHandler)
|
||||||
|
r.HandleFunc("/_/yosoy/ping", ping).Methods(http.MethodGet)
|
||||||
r.PathPrefix("/").HandlerFunc(preflight).Methods(http.MethodOptions)
|
r.PathPrefix("/").HandlerFunc(preflight).Methods(http.MethodOptions)
|
||||||
r.PathPrefix("/").HandlerFunc(handler).Methods(http.MethodGet, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete, http.MethodConnect, http.MethodHead, http.MethodTrace)
|
r.PathPrefix("/").HandlerFunc(handler).Methods(http.MethodGet, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete, http.MethodConnect, http.MethodHead, http.MethodTrace)
|
||||||
|
|
||||||
|
|||||||
264
server_test.go
264
server_test.go
@@ -1,7 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
@@ -12,7 +14,7 @@ import (
|
|||||||
|
|
||||||
func TestHandler(t *testing.T) {
|
func TestHandler(t *testing.T) {
|
||||||
os.Setenv("YOSOY_SHOW_ENVS", "true")
|
os.Setenv("YOSOY_SHOW_ENVS", "true")
|
||||||
os.Setenv("YOSOY_SHOW_FILES", ".gitignore")
|
os.Setenv("YOSOY_SHOW_FILES", ".gitignore,/file/does/not/exist")
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", "https://example.org/sample/path?one=jeden&two=dwa", nil)
|
req, err := http.NewRequest("GET", "https://example.org/sample/path?one=jeden&two=dwa", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -29,8 +31,12 @@ func TestHandler(t *testing.T) {
|
|||||||
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
|
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result := rr.Result()
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(result.Body)
|
||||||
|
|
||||||
var response response
|
var response response
|
||||||
json.Unmarshal(rr.Body.Bytes(), &response)
|
json.Unmarshal(buf.Bytes(), &response)
|
||||||
|
|
||||||
// test response
|
// test response
|
||||||
assert.Equal(t, 1, response.Counter)
|
assert.Equal(t, 1, response.Counter)
|
||||||
@@ -43,5 +49,257 @@ func TestHandler(t *testing.T) {
|
|||||||
assert.NotEmpty(t, response.Files[".gitignore"])
|
assert.NotEmpty(t, response.Files[".gitignore"])
|
||||||
|
|
||||||
// test cors
|
// test cors
|
||||||
assert.Contains(t, rr.HeaderMap["Access-Control-Allow-Origin"], "*")
|
assert.Contains(t, result.Header["Access-Control-Allow-Origin"], "*")
|
||||||
|
}
|
||||||
|
|
||||||
|
// write test for request /_/yosoy/ping without any query parameters, the request should return bad request 400 error and return JSON error about missing hostname parameter
|
||||||
|
func TestHandlerPingNoParameters(t *testing.T) {
|
||||||
|
req, err := http.NewRequest("GET", "https://example.org/_/yosoy/ping", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler := http.HandlerFunc(ping)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusBadRequest {
|
||||||
|
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := rr.Result()
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(result.Body)
|
||||||
|
|
||||||
|
var response errorResponse
|
||||||
|
json.Unmarshal(buf.Bytes(), &response)
|
||||||
|
|
||||||
|
// test response
|
||||||
|
assert.Equal(t, "hostname is empty", response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write test for request /_/yosoy/ping with h parameter, the request should return bad request 400 error and return JSON error about port is empty
|
||||||
|
func TestHandlerPingWithHostname(t *testing.T) {
|
||||||
|
req, err := http.NewRequest("GET", "https://example.org/_/yosoy/ping?h=example.org", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler := http.HandlerFunc(ping)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusBadRequest {
|
||||||
|
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := rr.Result()
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(result.Body)
|
||||||
|
|
||||||
|
var response errorResponse
|
||||||
|
json.Unmarshal(buf.Bytes(), &response)
|
||||||
|
|
||||||
|
// test response
|
||||||
|
assert.Equal(t, "port is empty", response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write test for request /_/yosoy/ping with h=127.0.0.1 parameter and p=8123 parameter, the request should return bad request 400 error and return JSON error about tcp connection issue
|
||||||
|
func TestHandlerPingWithHostnameAndPort(t *testing.T) {
|
||||||
|
req, err := http.NewRequest("GET", "https://example.org/_/yosoy/ping?h=127.0.0.1&p=8123", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler := http.HandlerFunc(ping)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusInternalServerError {
|
||||||
|
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := rr.Result()
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(result.Body)
|
||||||
|
|
||||||
|
var response errorResponse
|
||||||
|
json.Unmarshal(buf.Bytes(), &response)
|
||||||
|
|
||||||
|
// test response
|
||||||
|
assert.Equal(t, "dial tcp 127.0.0.1:8123: connect: connection refused", response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write test for request /_/yosoy/ping with h=127.0.0.1 parameter and p=8123 parameter, the request should return 200 ok and return JSON with message ping succeeded
|
||||||
|
func TestHandlerPingWithHostnameAndPortSuccess(t *testing.T) {
|
||||||
|
|
||||||
|
// create tcp process to listen on port 8123
|
||||||
|
listener, err := net.Listen("tcp", "localhost:8123")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "https://example.org/_/yosoy/ping?h=127.0.0.1&p=8123", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler := http.HandlerFunc(ping)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := rr.Result()
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(result.Body)
|
||||||
|
|
||||||
|
var response successResponse
|
||||||
|
json.Unmarshal(buf.Bytes(), &response)
|
||||||
|
|
||||||
|
// test response
|
||||||
|
assert.Equal(t, "ping succeeded", response.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write test for request /_/yosoy/ping?h=127.0.0.1&p=8123&n=qwq, the request should return 500 internal server error and return JSON with error "dial qwq: unknown network qwq"
|
||||||
|
func TestHandlerPingWithHostnameAndPortAndNetwork(t *testing.T) {
|
||||||
|
|
||||||
|
// create tcp process to listen on port 8123
|
||||||
|
listener, err := net.Listen("tcp", "localhost:8123")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "https://example.org/_/yosoy/ping?h=127.0.0.1&p=8123&n=qwq", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler := http.HandlerFunc(ping)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusInternalServerError {
|
||||||
|
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := rr.Result()
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(result.Body)
|
||||||
|
var response errorResponse
|
||||||
|
json.Unmarshal(buf.Bytes(), &response)
|
||||||
|
// test response
|
||||||
|
assert.Equal(t, "dial qwq: unknown network qwq", response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write test for preflight HTTP Options request, verify that all headers are set
|
||||||
|
func TestHandlerPreflight(t *testing.T) {
|
||||||
|
req, err := http.NewRequest("OPTIONS", "https://example.org/test", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler := http.HandlerFunc(preflight)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := rr.Result()
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(result.Body)
|
||||||
|
|
||||||
|
// test response
|
||||||
|
assert.Equal(t, "*", result.Header.Get("Access-Control-Allow-Origin"))
|
||||||
|
assert.Equal(t, "*", result.Header.Get("Access-Control-Allow-Methods"))
|
||||||
|
assert.Equal(t, "*", result.Header.Get("Access-Control-Allow-Headers"))
|
||||||
|
assert.Equal(t, "true", result.Header.Get("Access-Control-Allow-Credentials"))
|
||||||
|
assert.Equal(t, "600", result.Header.Get("Access-Control-Max-Age"))
|
||||||
|
assert.Equal(t, "*", result.Header.Get("Access-Control-Expose-Headers"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// write test for request /_/yosoy/ping?h=127.0.0.1&p=8123&n=tcp&t=5, the request should return 200 ok and return JSON with message "ping succeeded"
|
||||||
|
func TestHandlerPingWithHostnameAndPortAndNetworkAndTimeout(t *testing.T) {
|
||||||
|
|
||||||
|
// create tcp process to listen on port 8123
|
||||||
|
listener, err := net.Listen("tcp", "localhost:8123")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "https://example.org/_/yosoy/ping?h=127.0.0.1&p=8123&n=tcp&t=5", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler := http.HandlerFunc(ping)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := rr.Result()
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(result.Body)
|
||||||
|
var response successResponse
|
||||||
|
json.Unmarshal(buf.Bytes(), &response)
|
||||||
|
// test response
|
||||||
|
assert.Equal(t, "ping succeeded", response.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write test for request /_/yosoy/ping?h=127.0.0.1&p=8123&n=tcp&t=invalid, the request should return 400 bad request and return JSON with error "timeout is invalid"
|
||||||
|
func TestHandlerPingWithHostnameAndPortAndNetworkAndTimeoutInvalid(t *testing.T) {
|
||||||
|
|
||||||
|
// create tcp process to listen on port 8123
|
||||||
|
listener, err := net.Listen("tcp", "localhost:8123")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "https://example.org/_/yosoy/ping?h=127.0.0.1&p=8123&n=tcp&t=invalid", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler := http.HandlerFunc(ping)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusBadRequest {
|
||||||
|
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := rr.Result()
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(result.Body)
|
||||||
|
var response errorResponse
|
||||||
|
json.Unmarshal(buf.Bytes(), &response)
|
||||||
|
// test response
|
||||||
|
assert.Equal(t, "timeout is invalid", response.Error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: yosoy
|
- name: yosoy
|
||||||
image: lukasz/yosoy:2.0.3
|
image: lukasz/yosoy:edge
|
||||||
env:
|
env:
|
||||||
- name: YOSOY_SHOW_ENVS
|
- name: YOSOY_SHOW_ENVS
|
||||||
value: "true"
|
value: "true"
|
||||||
|
|||||||
Reference in New Issue
Block a user