Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9bb065696 | |||
|
|
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 | ||
|
|
27d54a67c5 | ||
|
|
394b6c6b19 | ||
|
|
572a661570 | ||
|
|
ae2c5d1f80 | ||
|
|
0b8223504c | ||
|
|
d10a176e53 | ||
|
|
e8636e09ac | ||
|
|
109e665451 | ||
|
|
33c6293042 | ||
|
|
6c93e2987b | ||
|
|
cfdeef0e2a | ||
|
|
1842453288 | ||
|
|
9fdfd5cb17 | ||
|
|
62d92811ce | ||
|
|
5e0380377f | ||
|
|
669c88abce | ||
|
|
46ce22d8ec | ||
|
|
6e8531123f | ||
|
|
f651a27580 | ||
|
|
900118758f | ||
|
|
2da157319c | ||
|
|
1f0946a0b1 | ||
|
|
1b5234d37c | ||
|
|
b334dcfcea | ||
|
|
f09931b33a | ||
|
|
afd2708b14 | ||
|
|
f35f940d90 | ||
|
|
54942ada28 | ||
|
|
cc1fbdc2b9 | ||
|
|
865440a0b0 | ||
|
|
300d243657 | ||
|
|
5be69cf38d | ||
|
|
1701c6cbe5 | ||
|
|
f1ca7d069c | ||
|
|
0ae522a43e | ||
|
|
3af4030d87 | ||
|
|
cc7ae5835f | ||
|
|
1307c185e0 | ||
|
|
dc016ceccc | ||
|
|
2cd59bd065 | ||
|
|
664b9f425d | ||
|
|
7dc37b1b74 | ||
|
|
6786f6753c | ||
|
|
3744500c5e | ||
|
|
866b1a165a | ||
|
|
f8fab8d501 | ||
|
|
bb07fc7e6e | ||
|
|
7073bea751 | ||
|
|
b3fa7f0507 | ||
|
|
50a90591fc | ||
|
|
204cd377f5 | ||
|
|
988ee495ba | ||
|
|
67a63bc610 | ||
|
|
9c90cf07e9 | ||
|
|
1844d70a58 | ||
|
|
43e698f307 | ||
|
|
eeecbb33bb | ||
|
|
715a703b0a | ||
|
|
a2a3c4f948 | ||
|
|
defd5bdb32 | ||
|
|
908f01df4b | ||
|
|
1a1f4cf14d | ||
|
|
cd90e887fe |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [lukaszbudnik]
|
||||
17
.github/dependabot.yml
vendored
Normal file
17
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
# Maintain dependencies for go lang
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
# Maintain dependencies for docker
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
26
.github/workflows/docker-image.yml
vendored
Normal file
26
.github/workflows/docker-image.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Docker Image CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- name: Build the Docker image
|
||||
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
|
||||
52
.github/workflows/docker-publish.yml
vendored
Normal file
52
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
push_to_registries:
|
||||
name: Push Docker image to Docker Hub and ghcr.io registries
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
lukasz/yosoy
|
||||
ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=edge
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
20
.github/workflows/go.yml
vendored
Normal file
20
.github/workflows/go.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Go
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.19
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
debug.test
|
||||
cover.out
|
||||
yosoy
|
||||
13
Dockerfile
13
Dockerfile
@@ -1,16 +1,19 @@
|
||||
FROM golang:1.13.5-alpine3.10 as builder
|
||||
FROM golang:1.21-alpine as builder
|
||||
|
||||
LABEL maintainer="Łukasz Budnik lukasz.budnik@gmail.com"
|
||||
|
||||
# install prerequisites
|
||||
RUN apk update && apk add git
|
||||
|
||||
# build yosoy
|
||||
RUN apk add git
|
||||
RUN git clone https://github.com/lukaszbudnik/yosoy.git
|
||||
ADD . /go/yosoy
|
||||
RUN go env -w GOPROXY=direct
|
||||
RUN cd /go/yosoy && go build
|
||||
|
||||
FROM alpine:3.10
|
||||
FROM alpine:3.18
|
||||
COPY --from=builder /go/yosoy/yosoy /bin
|
||||
|
||||
# register entrypoint
|
||||
ENTRYPOINT ["yosoy"]
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 8080
|
||||
|
||||
286
README.md
286
README.md
@@ -1,151 +1,197 @@
|
||||
# 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 a 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:
|
||||
|
||||
* testing HTTP routing & ingress
|
||||
* testing HTTP load balancing
|
||||
* testing HTTP caching
|
||||
* stubbing and prototyping distributed applications
|
||||
- Testing HTTP routing and ingress
|
||||
- Testing HTTP load balancing
|
||||
- Testing HTTP caching
|
||||
- Executing reachability analysis
|
||||
- Stubbing and prototyping distributed applications
|
||||
|
||||
yosoy will provide information like:
|
||||
"Yo soy" means "I am" in Spanish.
|
||||
|
||||
* Request URI
|
||||
* Hostname
|
||||
* Remote IP
|
||||
* How many times it was called
|
||||
* HTTP headers
|
||||
* 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
|
||||
## API
|
||||
|
||||
See [Kubernetes example](#kubernetes-example) below.
|
||||
yosoy responds to all requests with a JSON containing the information about:
|
||||
|
||||
- HTTP request:
|
||||
- Host
|
||||
- Request URI
|
||||
- Method
|
||||
- Scheme
|
||||
- Proto
|
||||
- URL
|
||||
- Remote IP
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
The docker image is available on docker hub:
|
||||
The docker image is available on docker hub and ghcr.io:
|
||||
|
||||
```
|
||||
lukasz/yosoy
|
||||
```sh
|
||||
docker pull lukasz/yosoy
|
||||
docker pull ghcr.io/lukaszbudnik/yosoy
|
||||
```
|
||||
|
||||
It exposes HTTP service on port 80.
|
||||
|
||||
## 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` features. 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:
|
||||
|
||||
```
|
||||
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
|
||||
```bash
|
||||
# start minikube
|
||||
minikube start
|
||||
# deploy test service
|
||||
kubectl apply -f test/deployment.yaml
|
||||
# tunnel to it and copy the URL as $URL variable
|
||||
minikube service --url camarero
|
||||
# simulate some HTTP requests
|
||||
curl -H "Host: gateway.myapp.com" $URL/camarero/abc
|
||||
curl -H "Host: gateway.myapp.com" $URL/camarero/abc
|
||||
curl -H "Host: gateway.myapp.com" $URL/camarero/abc
|
||||
curl -H "Host: gateway.myapp.com" $URL/camarero/abc
|
||||
```
|
||||
|
||||
Deploy above service (with 2 replicas) and execute curl to the service a couple of times:
|
||||
## Sample JSON response
|
||||
|
||||
```
|
||||
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 yosoy JSON response to a request made from a single page application (SPA) to a backend API deployed in Kubernetes behind nginx ingress and [haproxy-auth-gateway](https://github.com/lukaszbudnik/haproxy-auth-gateway) looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"host": "api.localtest.me",
|
||||
"proto": "HTTP/1.1",
|
||||
"method": "GET",
|
||||
"scheme": "https",
|
||||
"requestUri": "/camarero",
|
||||
"url": "https:///camarero",
|
||||
"remoteAddr": "192.168.65.3",
|
||||
"counter": 1,
|
||||
"headers": {
|
||||
"Accept": ["*/*"],
|
||||
"Accept-Encoding": ["gzip, deflate, br"],
|
||||
"Accept-Language": ["en-US,en;q=0.9,pl-PL;q=0.8,pl;q=0.7,es;q=0.6"],
|
||||
"Authorization": [
|
||||
"Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJXejFuaDNCWDI4UHMxVEMzSDRoOW52Q1VWRXpjVVBzQms4Z1NmeEp4ZS1JIn0.eyJleHAiOjE2Mjk4MjM3OTMsImlhdCI6MTYyOTgyMjg5MywiYXV0aF90aW1lIjoxNjI5ODIyODkyLCJqdGkiOiI3ZmQzMjkwZi05NjMyLTQ0NzEtYjRjOS1lNTFjZDYwMjllYjgiLCJpc3MiOiJodHRwczovL2F1dGgubG9jYWx0ZXN0Lm1lL2F1dGgvcmVhbG1zL2hvdGVsIiwic3ViIjoiMDdmYzM3YmYtMmJjNy00ZTRmLWE3MDUtYzRjNjgzNTIwYmU1IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoicmVhY3QiLCJub25jZSI6IjQzNDhmMjU5LTliYTYtNDk2ZC04N2I5LWZmZGYzNDMwN2UzOSIsInNlc3Npb25fc3RhdGUiOiJmNTM5OGI3Ny01OTNhLTQ3OWYtOTc5NS00NGIyNGJjMjhkYjQiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHBzOi8vbHVrYXN6YnVkbmlrLmdpdGh1Yi5pbyJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiY2FtYXJlcm8iXX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJzaWQiOiJmNTM5OGI3Ny01OTNhLTQ3OWYtOTc5NS00NGIyNGJjMjhkYjQiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJKdWxpbyIsInByZWZlcnJlZF91c2VybmFtZSI6Imp1bGlvIiwiZ2l2ZW5fbmFtZSI6Ikp1bGlvIn0.t5y3L4FzGxM0zwI3fskDI8Kemxz_izcvPPKciSEvNHnZWGQK-9AclGNFz_A9cLFSkpc6l6lBmt7WaC0i04c4h1a9G9AOFImmVXPMPDdTXOQ4aah4CvlN6Fy8ShvSHrQA-wMHEELBpIFsKFx2WP3QHiy27ycr3kqQzW4QymyU7J8tM4-qKR_H1_8aiNOrm5fIED-nEP096V2zvWXiGZX7ts6XE2-annhKphCABLdmIiwgDUnhlAz0hdiDrDbIjzr0ldW4AnUkSQxIZY0PnoEnGVuUvkOYvJpFx10gjORMnRgHSEj9Mk5dtyVGHcihZ5TntCL40WoymNxae6K4-FH3Lw"
|
||||
],
|
||||
"Origin": ["https://lukaszbudnik.github.io"],
|
||||
"Referer": ["https://lukaszbudnik.github.io/"],
|
||||
"Sec-Ch-Ua": [
|
||||
"\" Not;A Brand\";v=\"99\", \"Google Chrome\";v=\"91\", \"Chromium\";v=\"91\""
|
||||
],
|
||||
"Sec-Ch-Ua-Mobile": ["?0"],
|
||||
"Sec-Fetch-Dest": ["empty"],
|
||||
"Sec-Fetch-Mode": ["cors"],
|
||||
"Sec-Fetch-Site": ["cross-site"],
|
||||
"User-Agent": [
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
|
||||
],
|
||||
"X-Forwarded-For": ["192.168.65.3", "10.1.3.9"],
|
||||
"X-Forwarded-Host": ["api.localtest.me"],
|
||||
"X-Forwarded-Port": ["443"],
|
||||
"X-Forwarded-Proto": ["https"],
|
||||
"X-Real-Ip": ["192.168.65.3"],
|
||||
"X-Request-Id": ["48a77564d88ca8a893610b9458bfd885"],
|
||||
"X-Scheme": ["https"]
|
||||
},
|
||||
"hostname": "camarero-cf7c95ccd-cz5lh",
|
||||
"envVariables": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"HOSTNAME=camarero-cf7c95ccd-cz5lh",
|
||||
"YOSOY_SHOW_FILES=/etc/podinfo/labels,/etc/podinfo/annotations",
|
||||
"YOSOY_SHOW_ENVS=true",
|
||||
"KUBERNETES_SERVICE_PORT=443",
|
||||
"KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443",
|
||||
"KUBERNETES_PORT=tcp://10.96.0.1:443",
|
||||
"KUBERNETES_PORT_443_TCP_PORT=443",
|
||||
"KUBERNETES_SERVICE_HOST=10.96.0.1",
|
||||
"KUBERNETES_PORT_443_TCP_PROTO=tcp",
|
||||
"KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1",
|
||||
"HOME=/root"
|
||||
],
|
||||
"files": {
|
||||
"/etc/podinfo/annotations": "kubernetes.io/config.seen=\"2021-08-24T15:12:19.555374430Z\"\nkubernetes.io/config.source=\"api\"",
|
||||
"/etc/podinfo/labels": "app.kubernetes.io/component=\"api\"\napp.kubernetes.io/name=\"camarero\"\napp.kubernetes.io/part-of=\"hotel\"\napp.kubernetes.io/version=\"0.0.1\"\npod-template-hash=\"cf7c95ccd\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A sample response looks like this:
|
||||
## 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"}
|
||||
```
|
||||
Request URI: /
|
||||
Hostname: camarero-859d7c6d6b-kb5s5
|
||||
Remote IP: 172.18.0.1
|
||||
Called: 2
|
||||
|
||||
HTTP headers:
|
||||
Accept: */*
|
||||
User-Agent: curl/7.64.1
|
||||
To see an unsuccessful response you may use localhost with some random port number:
|
||||
|
||||
Env variables:
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
HOSTNAME=camarero-859d7c6d6b-kb5s5
|
||||
YOSOY_SHOW_ENVS=true
|
||||
YOSOY_SHOW_FILES=/etc/podinfo/labels,/etc/podinfo/annotations
|
||||
CAMARERO_PORT_80_TCP_PORT=80
|
||||
CAMARERO_PORT_80_TCP_ADDR=10.105.203.131
|
||||
KUBERNETES_PORT=tcp://10.96.0.1:443
|
||||
KUBERNETES_PORT_443_TCP_PORT=443
|
||||
CAMARERO_SERVICE_HOST=10.105.203.131
|
||||
KUBERNETES_PORT_443_TCP_PROTO=tcp
|
||||
KUBERNETES_SERVICE_HOST=10.96.0.1
|
||||
KUBERNETES_SERVICE_PORT=443
|
||||
KUBERNETES_SERVICE_PORT_HTTPS=443
|
||||
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
|
||||
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
|
||||
CAMARERO_PORT=tcp://10.105.203.131:80
|
||||
CAMARERO_SERVICE_PORT=80
|
||||
CAMARERO_PORT_80_TCP=tcp://10.105.203.131:80
|
||||
CAMARERO_PORT_80_TCP_PROTO=tcp
|
||||
HOME=/root
|
||||
|
||||
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"
|
||||
```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
|
||||
```
|
||||
16
go.mod
Normal file
16
go.mod
Normal file
@@ -0,0 +1,16 @@
|
||||
module github.com/lukaszbudnik/yosoy
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gorilla/handlers v1.5.2
|
||||
github.com/gorilla/mux v1.8.1
|
||||
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
|
||||
)
|
||||
16
go.sum
Normal file
16
go.sum
Normal file
@@ -0,0 +1,16 @@
|
||||
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/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
222
server.go
222
server.go
@@ -1,76 +1,192 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
const PING_DEFAULT_TIMEOUT = 10
|
||||
const PING_DEFAULT_NETWORK = "tcp"
|
||||
|
||||
type response struct {
|
||||
Host string `json:"host"`
|
||||
Proto string `json:"proto"`
|
||||
Method string `json:"method"`
|
||||
Scheme string `json:"scheme"`
|
||||
RequestURI string `json:"requestUri"`
|
||||
URL string `json:"url"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type errorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type successResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
var counter = 0
|
||||
var hostname = os.Getenv("HOSTNAME")
|
||||
var showEnvs = os.Getenv("YOSOY_SHOW_ENVS")
|
||||
var showFiles = os.Getenv("YOSOY_SHOW_FILES")
|
||||
|
||||
func preflight(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "*")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
w.Header().Set("Access-Control-Expose-Headers", "*")
|
||||
w.Header().Set("Access-Control-Max-Age", "600")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func handler(w http.ResponseWriter, req *http.Request) {
|
||||
if req.RequestURI == "/favicon.ico" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
showEnvs := os.Getenv("YOSOY_SHOW_ENVS")
|
||||
showFiles := os.Getenv("YOSOY_SHOW_FILES")
|
||||
|
||||
response := &response{}
|
||||
|
||||
counter++
|
||||
response.Counter = counter
|
||||
|
||||
remoteAddr := remoteAddrWithoutPort(req)
|
||||
response.RemoteAddr = remoteAddr
|
||||
|
||||
response.RequestURI = req.RequestURI
|
||||
response.Host = req.Host
|
||||
response.Proto = req.Proto
|
||||
response.Method = req.Method
|
||||
response.Scheme = req.URL.Scheme
|
||||
response.Headers = req.Header
|
||||
response.URL = req.URL.String()
|
||||
|
||||
response.Hostname = hostname
|
||||
|
||||
if strings.ToLower(showEnvs) == "true" || strings.ToLower(showEnvs) == "yes" || strings.ToLower(showEnvs) == "on" || showEnvs == "1" {
|
||||
response.EnvVariables = os.Environ()
|
||||
}
|
||||
if len(showFiles) > 0 {
|
||||
response.Files = make(map[string]string)
|
||||
files := strings.Split(showFiles, ",")
|
||||
for _, file := range files {
|
||||
bytes, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
log.Printf("Could not read file %v: %v\n", file, err)
|
||||
continue
|
||||
}
|
||||
contents := string(bytes)
|
||||
response.Files[file] = contents
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
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 {
|
||||
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++
|
||||
fmt.Fprintf(w, "Called: %v\n", counter)
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintf(w, "HTTP headers:\n")
|
||||
headers := make([]string, 0, len(req.Header))
|
||||
for k := range req.Header {
|
||||
headers = append(headers, k)
|
||||
}
|
||||
sort.Strings(headers)
|
||||
for _, header := range headers {
|
||||
headers := req.Header[header]
|
||||
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" {
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintf(w, "Env variables:\n")
|
||||
for _, e := range os.Environ() {
|
||||
fmt.Fprintln(w, e)
|
||||
}
|
||||
}
|
||||
if len(showFiles) > 0 {
|
||||
files := strings.Split(showFiles, ",")
|
||||
for _, file := range files {
|
||||
bytes, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
fmt.Printf("[%v] - %v - could not read file %v: %v\n", hostname, time.Now().Format(time.RFC3339), file, err)
|
||||
continue
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintf(w, "File %v:\n", file)
|
||||
contents := string(bytes)
|
||||
fmt.Fprintln(w, contents)
|
||||
}
|
||||
}
|
||||
return remoteAddr
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Printf("[%v] - %v - yosoy is up!\n", hostname, time.Now().Format(time.RFC3339))
|
||||
http.HandleFunc("/", handler)
|
||||
http.ListenAndServe(":80", nil)
|
||||
log.Printf("yosoy is up %v\n", hostname)
|
||||
|
||||
r := mux.NewRouter()
|
||||
|
||||
r.Handle("/favicon.ico", r.NotFoundHandler)
|
||||
r.HandleFunc("/_/yosoy/ping", ping).Methods(http.MethodGet)
|
||||
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)
|
||||
|
||||
loggingRouter := handlers.CombinedLoggingHandler(os.Stdout, r)
|
||||
proxyRouter := handlers.ProxyHeaders(loggingRouter)
|
||||
recoveryRouter := handlers.RecoveryHandler()(proxyRouter)
|
||||
|
||||
http.ListenAndServe(":8080", recoveryRouter)
|
||||
}
|
||||
|
||||
305
server_test.go
Normal file
305
server_test.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"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,/file/does/not/exist")
|
||||
|
||||
req, err := http.NewRequest("GET", "https://example.org/sample/path?one=jeden&two=dwa", 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)
|
||||
}
|
||||
|
||||
result := rr.Result()
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(result.Body)
|
||||
|
||||
var response response
|
||||
json.Unmarshal(buf.Bytes(), &response)
|
||||
|
||||
// test response
|
||||
assert.Equal(t, 1, response.Counter)
|
||||
assert.Equal(t, "example.org", response.Host)
|
||||
assert.Equal(t, "GET", response.Method)
|
||||
assert.Equal(t, "https", response.Scheme)
|
||||
assert.Equal(t, "HTTP/1.1", response.Proto)
|
||||
assert.Equal(t, "https://example.org/sample/path?one=jeden&two=dwa", response.URL)
|
||||
assert.NotEmpty(t, response.EnvVariables)
|
||||
assert.NotEmpty(t, response.Files[".gitignore"])
|
||||
|
||||
// test cors
|
||||
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)
|
||||
}
|
||||
53
test/deployment.yaml
Normal file
53
test/deployment.yaml
Normal 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:edge
|
||||
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
|
||||
Reference in New Issue
Block a user