Compare commits

...

93 Commits
v0.x ... v1.4.0

Author SHA1 Message Date
Stefan Prodan
ea93f3ed9f Bump version to 1.4.0 2018-11-28 11:45:41 +02:00
Stefan Prodan
2fc253a7c7 Merge pull request #8 from guyfedwards/blue-green-flagger
Set background colour based on version
2018-11-28 11:37:03 +02:00
guyfedwards
c83e19a217 set color based on primary/canary workload
sets the bg color as blue for primary and green for canary workloads
2018-11-27 17:41:41 +00:00
Stefan Prodan
a9a1252a22 Add brew install cmd for podcli 2018-11-07 14:58:37 +02:00
Stefan Prodan
046a9a4852 Add version to code init commit message 2018-10-27 12:19:54 +03:00
Stefan Prodan
4d78abdad8 Prep for v1.3.2 release 2018-10-27 12:19:15 +03:00
Stefan Prodan
f8b32fa130 Release ngrok helm chart v0.2.0 2018-10-27 12:02:46 +03:00
Stefan Prodan
a30fb535de Merge pull request #7 from tdickman/subdomain-support
Add ngrok subdomain support
2018-10-27 12:00:12 +03:00
Stefan Prodan
8d662334a2 Update GitHub actions docs 2018-10-27 11:59:24 +03:00
Stefan Prodan
4ed9271783 Release v1.3.1 2018-10-27 11:50:14 +03:00
Stefan Prodan
97157694be Add logs to Docker build GitHub action 2018-10-27 11:48:32 +03:00
Stefan Prodan
bf92728234 Generate Github actions CI pipeline
- add custom Dockerfile
- use GitHub actions env vars as docker build args
- remove .gh from Makefile and Dockerfile in destination project
2018-10-27 11:41:52 +03:00
Tom Dickman
bd31f8b23e Add ngrok subdomain support 2018-10-26 14:06:37 -05:00
Stefan Prodan
f7c8061ac0 Bump version in GitHub Actions docs 2018-10-26 19:07:00 +03:00
Stefan Prodan
943f4e26ab Release v1.3.1 2018-10-26 19:05:17 +03:00
Stefan Prodan
f44909ef77 Add golang tools GitHub Action 2018-10-26 18:23:02 +03:00
Stefan Prodan
1af24bd3cd Run gofmt 2018-10-26 18:20:54 +03:00
Stefan Prodan
14ef95dac6 Release v1.3.0 2018-10-26 15:32:22 +03:00
Stefan Prodan
08a26cef24 Add GitHub Actions docs 2018-10-26 15:30:36 +03:00
Stefan Prodan
8013c0bed0 Merge pull request #6 from stefanprodan/test
Add code init command and GitHub actions
2018-10-26 15:08:05 +03:00
Stefan Prodan
6aa4303e08 Bump version to 1.3.0 2018-10-26 15:02:39 +03:00
Stefan Prodan
f34fbacf13 Add git push to code init command 2018-10-26 15:01:48 +03:00
Stefan Prodan
b7701f6ae7 Don't reload page if version changed 2018-10-26 15:01:24 +03:00
Stefan Prodan
d3208cd8ac Add custom GitHub action for docker tag and push
- if the push refers a branch the docker tag will be branch-sha
- if the push refers a git tag the docker tag will be th git tag
2018-10-26 14:29:16 +03:00
Stefan Prodan
7d4c89d965 Add GitHub release workflow 2018-10-26 13:06:49 +03:00
Stefan Prodan
3b5ac61680 Remove chart from code init 2018-10-26 13:06:21 +03:00
Stefan Prodan
e8e2ac2b34 Run unit tests in docker multi-stage build 2018-10-26 12:37:13 +03:00
Stefan Prodan
ef571a9b1b Add initialize podinfo code repo command 2018-10-26 12:09:21 +03:00
Stefan Prodan
3d9cabcea4 Add GitHub workflow for branch test, build and push 2018-10-26 12:08:59 +03:00
Stefan Prodan
ae4120a24e Add go-getter pkg 2018-10-26 12:08:01 +03:00
Stefan Prodan
97d36bd8bb Release v1.2.1
- wait for the readiness probe to remove the endpoint from the LB before entering HTTP server graceful shutdown
2018-09-25 12:10:46 +03:00
Stefan Prodan
18a22d1b94 Add shutdown delay (wait for the readiness probe) 2018-09-25 12:01:01 +03:00
Stefan Prodan
083de34465 Fix podinfo-istio chart health checks 2018-09-19 13:16:25 +03:00
Stefan Prodan
64b85dc30d Move Istio docs to the istio-gke repo 2018-09-18 14:54:30 +03:00
Stefan Prodan
fed964e223 Expose Istio Grafana 2018-09-17 18:34:34 +03:00
Stefan Prodan
efb6a76242 Mention Istio Gateway reload cert issue 2018-09-17 13:58:38 +03:00
Stefan Prodan
fb199b72a1 Split GKE and Could DNS setup 2018-09-17 12:51:01 +03:00
Stefan Prodan
ce117e1706 Add Could DNS verify commands 2018-09-16 20:47:34 +03:00
Stefan Prodan
23e67f9923 Remove istio sub domain 2018-09-16 14:29:41 +03:00
Stefan Prodan
30b030a685 Add CloudDNS CNAME record 2018-09-16 14:07:04 +03:00
Stefan Prodan
0fe4a7a3a9 Add GKE, CloudDNS and Helm setup steps 2018-09-16 13:48:58 +03:00
Stefan Prodan
982063ab9b Resize Istio cert-manager diagram 2018-09-16 12:49:48 +03:00
Stefan Prodan
c3256bd18f Add Istio cert-manager diagram 2018-09-16 12:43:08 +03:00
Stefan Prodan
d947fc5b2c Add OpenFaaS Istio port-forward commands 2018-09-14 16:49:41 +03:00
Stefan Prodan
dc6d64137d Add OpenFaaS Istio intro 2018-09-14 13:25:20 +03:00
Stefan Prodan
f3c1ee7dbc Add OpenFaaS Istio canary diagram 2018-09-14 12:04:58 +03:00
Stefan Prodan
6b6dd86fea Add OpenFaaS Istio diagram 2018-09-14 11:13:09 +03:00
Stefan Prodan
02e5f233d0 Release v1.2.0 2018-09-11 22:18:58 +03:00
Stefan Prodan
b89f46ac04 Add websocket client command to CLI 2018-09-11 22:14:59 +03:00
Stefan Prodan
59cd692141 Add websocket echo handler 2018-09-11 22:13:54 +03:00
Stefan Prodan
bcd61428d1 Import gorilla/websocket 2018-09-11 22:12:37 +03:00
Stefan Prodan
f8ec9c0947 Fix multi-arch docker push 2018-09-11 18:04:13 +03:00
Stefan Prodan
6c98fbf1f4 Add JWT token issue and validate handlers 2018-09-10 11:36:11 +03:00
Stefan Prodan
54f6d9f74d Add env handler 2018-09-10 01:29:49 +03:00
Stefan Prodan
1d35304d9d OpenfaaS canary deployments 2018-09-09 16:38:37 +03:00
Stefan Prodan
457a56f71a Publish podinfo CLI to GitHub with goreleaser 2018-09-09 12:38:23 +03:00
Stefan Prodan
fbcab6cf56 Upgrade to go 1.11 and alpine 3.8 2018-09-09 12:37:58 +03:00
Stefan Prodan
0126282669 add goreleaser 2018-09-08 19:05:37 +03:00
Stefan Prodan
ff1fb39f43 Release v1.1.0
- add podinfo CLI to Quay docker image
- use podinfo CLI for health checks (Istio mTLS support)
2018-09-08 11:38:48 +03:00
Stefan Prodan
84f0e1c9e2 Add CLI check certificate 2018-09-07 15:26:07 +03:00
Stefan Prodan
3eb4cc90f9 Add CLI to Quay docker image 2018-09-07 14:55:24 +03:00
Stefan Prodan
b6c3d36bde Add CLI check tcp command 2018-09-07 14:54:55 +03:00
Stefan Prodan
a8a85e6aae Add CLI version cmd 2018-09-07 14:54:35 +03:00
Stefan Prodan
79b2d784bf Add OpenFaaS Istio guide WIP 2018-09-07 13:38:39 +03:00
Stefan Prodan
bfd35f6cc0 Make health checks compatible with Istio mTLS 2018-09-07 13:38:18 +03:00
Stefan Prodan
f1775ba090 Add podinfo CLI WIP 2018-09-07 13:37:40 +03:00
Stefan Prodan
7a2d59de8e Add OpenFaaS Istio mTLS and policies 2018-09-05 15:38:53 +03:00
Stefan Prodan
8191871761 Add Istio A/B test dashboard 2018-09-05 15:38:24 +03:00
Stefan Prodan
36bb719b1c Bump version to 1.0.1 2018-08-27 16:26:30 +01:00
Stefan Prodan
ecd204b15e Release v1.0.0 2018-08-22 00:57:53 +03:00
Stefan Prodan
979fd669df Use gorilla mux route name as Prometheus path label 2018-08-21 15:19:21 +03:00
Stefan Prodan
feac686e60 Release v1.0.0-beta.1 2018-08-21 12:03:34 +03:00
Stefan Prodan
d362dc5f81 Set env var prefix to PODINFO 2018-08-21 11:58:37 +03:00
Stefan Prodan
593ccaa0cd Add random delay and errors middleware 2018-08-21 03:12:20 +03:00
Stefan Prodan
0f098cf0f1 Add config file support 2018-08-21 02:02:47 +03:00
Stefan Prodan
2ddbc03371 Replace zerolog with zap 2018-08-21 02:01:26 +03:00
Stefan Prodan
f2d95bbf80 Add logging middleware and log level option 2018-08-20 17:03:07 +03:00
Stefan Prodan
7d18ec68b3 Use plag, viper and zap 2018-08-20 11:30:18 +03:00
Stefan Prodan
774d34c1dd Rewrite HTTP server with gorilla mux 2018-08-20 11:29:11 +03:00
Stefan Prodan
f13d006993 Add Kubernetes probes handlers 2018-08-20 11:28:06 +03:00
Stefan Prodan
aeeb146c2a Add UI handler 2018-08-20 11:27:40 +03:00
Stefan Prodan
11bd74eff2 Add local storage read/write handler 2018-08-20 11:27:08 +03:00
Stefan Prodan
af6d11fd33 Add panic handler 2018-08-20 11:26:24 +03:00
Stefan Prodan
49746fe2fb Add fscache reader handler 2018-08-20 11:26:08 +03:00
Stefan Prodan
da24d729bb Add runtime info handler 2018-08-20 11:25:36 +03:00
Stefan Prodan
449fcca3a9 Add HTTP status code handler 2018-08-20 11:25:15 +03:00
Stefan Prodan
2b0a742974 Add echo headers handler 2018-08-20 11:24:49 +03:00
Stefan Prodan
153f4dce45 Add echo handler with backend propagation 2018-08-20 11:24:23 +03:00
Stefan Prodan
4c8d11cc3e Add delay handler 2018-08-20 11:23:45 +03:00
Stefan Prodan
08415ce2ce Add version handler 2018-08-20 11:23:13 +03:00
Stefan Prodan
d26b7a96d9 Add UI index handler 2018-08-20 11:22:48 +03:00
Stefan Prodan
3c897b8bd7 Rename git commit to revision 2018-08-20 11:21:51 +03:00
Stefan Prodan
511ab87a18 Update deps for v1.0 2018-08-20 11:20:56 +03:00
925 changed files with 158490 additions and 11752 deletions

17
.github/actions/docker/Dockerfile vendored Normal file
View File

@@ -0,0 +1,17 @@
FROM docker:stable
LABEL "name"="Docker tag and push action"
LABEL "maintainer"="Stefan Prodan <support@weave.works>"
LABEL "version"="1.0.0"
LABEL "com.github.actions.icon"="package"
LABEL "com.github.actions.color"="green"
LABEL "com.github.actions.name"="Docker Publish"
LABEL "com.github.actions.description"="This is an Action to run docker tag and push commands."
RUN apk add --no-cache ca-certificates bash git curl \
&& rm -rf /var/cache/apk/*
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

25
.github/actions/docker/entrypoint.sh vendored Executable file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
DOCKER_TAG="latest"
if [[ "${GITHUB_REF}" == "refs/tags"* ]]; then
DOCKER_TAG=$(echo ${GITHUB_REF} | rev | cut -d/ -f1 | rev)
else
DOCKER_TAG=$(echo ${GITHUB_REF} | rev | cut -d/ -f1 | rev)-$(echo ${GITHUB_SHA} | head -c7)
fi
if [[ "$1" == "build" ]]; then
docker build -t ${DOCKER_IMAGE}:${DOCKER_TAG} \
--build-arg REPOSITORY=${GITHUB_REPOSITORY} \
--build-arg SHA=${GITHUB_SHA} -f $2 .
echo "Docker image tagged as ${DOCKER_IMAGE}:${DOCKER_TAG}"
fi
if [[ "$1" == "push" ]]; then
docker push ${DOCKER_IMAGE}:${DOCKER_TAG}
echo "Docker image pushed to ${DOCKER_IMAGE}:${DOCKER_TAG}"
fi

20
.github/actions/golang/Dockerfile vendored Normal file
View File

@@ -0,0 +1,20 @@
FROM golang:1.10
LABEL "name"="golang tools action"
LABEL "maintainer"="Stefan Prodan <support@weave.works>"
LABEL "version"="1.0.0"
LABEL "com.github.actions.icon"="code"
LABEL "com.github.actions.color"="green-dark"
LABEL "com.github.actions.name"="Go Tools"
LABEL "com.github.actions.description"="This is an Action to run go and dep commands."
ENV DEP_VERSION="v0.5.0"
RUN curl -fL -o /usr/local/bin/dep https://github.com/golang/dep/releases/download/${DEP_VERSION}/dep-linux-amd64 \
&& chmod +x /usr/local/bin/dep
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

26
.github/actions/golang/entrypoint.sh vendored Executable file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
set -e
APP_DIR="/go/src/github.com/${GITHUB_REPOSITORY}/"
mkdir -p ${APP_DIR} && cp -r ./ ${APP_DIR} && cd ${APP_DIR}
if [[ "$1" == "fmt" ]]; then
echo "Running go fmt"
files=$(gofmt -l $(find . -type f -name '*.go' -not -path "./vendor/*") 2>&1)
if [ "$files" ]; then
echo "These files did not pass the gofmt check:"
echo ${files}
exit 1
fi
fi
if [[ "$1" == "test" ]]; then
echo "Running go test"
go test $(go list ./... | grep -v /vendor/) -race -coverprofile=coverage.txt -covermode=atomic
cat coverage.txt
fi

35
.github/main.workflow vendored Normal file
View File

@@ -0,0 +1,35 @@
workflow "Publish container" {
on = "push"
resolves = ["Push"]
}
action "Lint" {
uses = "./.github/actions/golang"
args = "fmt"
}
action "Test" {
needs = ["Lint"]
uses = "./.github/actions/golang"
args = "test"
}
action "Build" {
needs = ["Test"]
uses = "./.github/actions/docker"
secrets = ["DOCKER_IMAGE"]
args = ["build", "Dockerfile.gh"]
}
action "Login" {
needs = ["Build"]
uses = "actions/docker/login@master"
secrets = ["DOCKER_USERNAME", "DOCKER_PASSWORD"]
}
action "Push" {
needs = ["Login"]
uses = "./.github/actions/docker"
secrets = ["DOCKER_IMAGE"]
args = "push"
}

1
.gitignore vendored
View File

@@ -18,3 +18,4 @@
release/
build/
gcloud/
dist/

21
.goreleaser.yml Normal file
View File

@@ -0,0 +1,21 @@
builds:
- main: ./cmd/podcli
binary: podcli
ldflags: -s -w -X github.com/stefanprodan/k8s-podinfo/pkg/version.REVISION={{.Commit}}
goos:
- windows
- darwin
- linux
goarch:
- amd64
env:
- CGO_ENABLED=0
ignore:
- goos: darwin
goarch: 386
- goos: windows
goarch: 386
archive:
name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
files:
- none*

View File

@@ -2,7 +2,7 @@ sudo: required
language: go
go:
- 1.9.x
- 1.11.x
services:
- docker
@@ -14,19 +14,10 @@ addons:
before_install:
- make dep
# - curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl"
# - mkdir -p .bin; mv ./kubectl .bin/kubectl && chmod +x .bin/kubectl
# - export PATH="$TRAVIS_BUILD_DIR/.bin:$PATH"
# - wget https://cdn.rawgit.com/Mirantis/kubeadm-dind-cluster/master/fixed/dind-cluster-v1.8.sh && chmod +x dind-cluster-v1.8.sh && ./dind-cluster-v1.8.sh up
# - export PATH="$HOME/.kubeadm-dind-cluster:$PATH"
script:
- make test
- make build docker-build
# - kubectl get nodes
# - kubectl run podinfo --image=podinfo:latest --port=9898
# - sleep 5
# - kubectl get pods
after_success:
- if [ -z "$DOCKER_USER" ]; then
@@ -41,3 +32,10 @@ after_success:
echo $QUAY_PASS | docker login -u $QUAY_USER --password-stdin quay.io;
make quay-push;
fi
deploy:
- provider: script
skip_cleanup: true
script: curl -sL http://git.io/goreleaser | bash
on:
tags: true

View File

@@ -1,4 +1,4 @@
FROM golang:1.9 as builder
FROM golang:1.11 as builder
RUN mkdir -p /go/src/github.com/stefanprodan/k8s-podinfo/
@@ -11,10 +11,14 @@ RUN go test $(go list ./... | grep -v integration | grep -v /vendor/ | grep -v /
RUN gofmt -l -d $(find . -type f -name '*.go' -not -path "./vendor/*") && \
GIT_COMMIT=$(git rev-list -1 HEAD) && \
CGO_ENABLED=0 GOOS=linux go build -ldflags "-s -w \
-X github.com/stefanprodan/k8s-podinfo/pkg/version.GITCOMMIT=${GIT_COMMIT}" \
-X github.com/stefanprodan/k8s-podinfo/pkg/version.REVISION=${GIT_COMMIT}" \
-a -installsuffix cgo -o podinfo ./cmd/podinfo
FROM alpine:3.7
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags "-s -w \
-X github.com/stefanprodan/k8s-podinfo/pkg/version.REVISION=${GIT_COMMIT}" \
-a -installsuffix cgo -o podcli ./cmd/podcli
FROM alpine:3.8
RUN addgroup -S app \
&& adduser -S -g app app \
@@ -24,6 +28,7 @@ RUN addgroup -S app \
WORKDIR /home/app
COPY --from=builder /go/src/github.com/stefanprodan/k8s-podinfo/podinfo .
COPY --from=builder /go/src/github.com/stefanprodan/k8s-podinfo/podcli /usr/local/bin/podcli
COPY ./ui ./ui
RUN chown -R app:app ./

39
Dockerfile.gh Normal file
View File

@@ -0,0 +1,39 @@
FROM golang:1.11 as builder
ARG REPOSITORY
ARG SHA
RUN mkdir -p /go/src/github.com/${REPOSITORY}/
WORKDIR /go/src/github.com/${REPOSITORY}
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags "-s -w -X github.com/${REPOSITORY}/pkg/version.REVISION=${SHA}" \
-a -installsuffix cgo -o podinfo ./cmd/podinfo \
&& mv podinfo /usr/local/bin/podinfo
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags "-s -w -X github.com/${REPOSITORY}/pkg/version.REVISION=${SHA}" \
-a -installsuffix cgo -o podcli ./cmd/podcli \
&& mv podcli /usr/local/bin/podcli
FROM alpine:3.8
RUN addgroup -S app \
&& adduser -S -g app app \
&& apk --no-cache add \
curl openssl netcat-openbsd
WORKDIR /home/app
COPY --from=builder /usr/local/bin/podinfo .
COPY --from=builder /usr/local/bin/podcli /usr/local/bin/podcli
COPY ./ui ./ui
RUN chown -R app:app ./
USER app
CMD ["./podinfo"]

405
Gopkg.lock generated
View File

@@ -2,97 +2,448 @@
[[projects]]
branch = "master"
name = "github.com/beorn7/perks"
packages = ["quantile"]
revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9"
digest = "1:a281ff052e060998bc715e444e448f6762a6da3e20e68c689dd8696e2894a5fd"
name = "github.com/aws/aws-sdk-go"
packages = [
"aws",
"aws/awserr",
"aws/awsutil",
"aws/client",
"aws/client/metadata",
"aws/corehandlers",
"aws/credentials",
"aws/credentials/ec2rolecreds",
"aws/credentials/endpointcreds",
"aws/credentials/stscreds",
"aws/csm",
"aws/defaults",
"aws/ec2metadata",
"aws/endpoints",
"aws/request",
"aws/session",
"aws/signer/v4",
"internal/ini",
"internal/s3err",
"internal/sdkio",
"internal/sdkrand",
"internal/sdkuri",
"internal/shareddefaults",
"private/protocol",
"private/protocol/eventstream",
"private/protocol/eventstream/eventstreamapi",
"private/protocol/query",
"private/protocol/query/queryutil",
"private/protocol/rest",
"private/protocol/restxml",
"private/protocol/xml/xmlutil",
"service/s3",
"service/sts",
]
pruneopts = "UT"
revision = "f76d9803fd695eebf74cb23c460287fa496efc21"
version = "v1.15.63"
[[projects]]
branch = "master"
digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d"
name = "github.com/beorn7/perks"
packages = ["quantile"]
pruneopts = "UT"
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
[[projects]]
branch = "master"
digest = "1:37011b20a70e205b93ebea5287e1afa5618db54bf3998c36ff5a8e4b146a170a"
name = "github.com/bgentry/go-netrc"
packages = ["netrc"]
pruneopts = "UT"
revision = "9fd32a8b3d3d3f9d43c341bfe098430e07609480"
[[projects]]
digest = "1:b95738a1e6ace058b5b8544303c0871fc01d224ef0d672f778f696265d0f2917"
name = "github.com/chzyer/readline"
packages = ["."]
pruneopts = "UT"
revision = "62c6fe6193755f722b8b8788aa7357be55a50ff1"
version = "v1.4"
[[projects]]
digest = "1:76dc72490af7174349349838f2fe118996381b31ea83243812a97e5a0fd5ed55"
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
pruneopts = "UT"
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[[projects]]
digest = "1:865079840386857c809b72ce300be7580cb50d3d3129ce11bf9aa6ca2bc1934a"
name = "github.com/fatih/color"
packages = ["."]
pruneopts = "UT"
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
version = "v1.7.0"
[[projects]]
digest = "1:abeb38ade3f32a92943e5be54f55ed6d6e3b6602761d74b4aab4c9dd45c18abd"
name = "github.com/fsnotify/fsnotify"
packages = ["."]
pruneopts = "UT"
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
version = "v1.4.7"
[[projects]]
digest = "1:97df918963298c287643883209a2c3f642e6593379f97ab400c2a2e219ab647d"
name = "github.com/golang/protobuf"
packages = ["proto"]
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
pruneopts = "UT"
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
version = "v1.2.0"
[[projects]]
digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1"
name = "github.com/gorilla/context"
packages = ["."]
pruneopts = "UT"
revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42"
version = "v1.1.1"
[[projects]]
digest = "1:e73f5b0152105f18bc131fba127d9949305c8693f8a762588a82a48f61756f5f"
name = "github.com/gorilla/mux"
packages = ["."]
pruneopts = "UT"
revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf"
version = "v1.6.2"
[[projects]]
digest = "1:7b5c6e2eeaa9ae5907c391a91c132abfd5c9e8a784a341b5625e750c67e6825d"
name = "github.com/gorilla/websocket"
packages = ["."]
pruneopts = "UT"
revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d"
version = "v1.4.0"
[[projects]]
digest = "1:f47d6109c2034cb16bd62b220e18afd5aa9d5a1630fe5d937ad96a4fb7cbb277"
name = "github.com/hashicorp/go-cleanhttp"
packages = ["."]
pruneopts = "UT"
revision = "e8ab9daed8d1ddd2d3c4efba338fe2eeae2e4f18"
version = "v0.5.0"
[[projects]]
branch = "master"
digest = "1:724d270a4cac0945dedb8de6357625a9d976cb640d381d3dab3144cec295db26"
name = "github.com/hashicorp/go-getter"
packages = [
".",
"helper/url",
]
pruneopts = "UT"
revision = "4bda8fa99001c61db3cad96b421d4c12a81f256d"
[[projects]]
digest = "1:605c47454db9040e30b20dc1b29e3e9d42d6ee742545729cdef74afb1b898ad0"
name = "github.com/hashicorp/go-safetemp"
packages = ["."]
pruneopts = "UT"
revision = "c9a55de4fe06c920a71964b53cfe3dd293a3c743"
version = "v1.0.0"
[[projects]]
digest = "1:77395dd3847dac9c45118c668f5dab85aedf0163dc3b38aea6578c5cf0d502f9"
name = "github.com/hashicorp/go-version"
packages = ["."]
pruneopts = "UT"
revision = "b5a281d3160aa11950a6182bd9a9dc2cb1e02d50"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:a361611b8c8c75a1091f00027767f7779b29cb37c456a71b8f2604c88057ab40"
name = "github.com/hashicorp/hcl"
packages = [
".",
"hcl/ast",
"hcl/parser",
"hcl/printer",
"hcl/scanner",
"hcl/strconv",
"hcl/token",
"json/parser",
"json/scanner",
"json/token",
]
pruneopts = "UT"
revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168"
[[projects]]
digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
name = "github.com/inconshreveable/mousetrap"
packages = ["."]
pruneopts = "UT"
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
version = "v1.0"
[[projects]]
digest = "1:e22af8c7518e1eab6f2eab2b7d7558927f816262586cd6ed9f349c97a6c285c4"
name = "github.com/jmespath/go-jmespath"
packages = ["."]
pruneopts = "UT"
revision = "0b12d6b5"
[[projects]]
digest = "1:c568d7727aa262c32bdf8a3f7db83614f7af0ed661474b24588de635c20024c7"
name = "github.com/magiconair/properties"
packages = ["."]
pruneopts = "UT"
revision = "c2353362d570a7bfa228149c62842019201cfb71"
version = "v1.8.0"
[[projects]]
digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67"
name = "github.com/mattn/go-colorable"
packages = ["."]
pruneopts = "UT"
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
digest = "1:0981502f9816113c9c8c4ac301583841855c8cf4da8c72f696b3ebedf6d0e4e5"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = "UT"
revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c"
version = "v0.0.4"
[[projects]]
digest = "1:ff5ebae34cfbf047d505ee150de27e60570e8c394b3b8fdbb720ff6ac71985fc"
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
pruneopts = "UT"
revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c"
version = "v1.0.1"
[[projects]]
digest = "1:78bbb1ba5b7c3f2ed0ea1eab57bdd3859aec7e177811563edc41198a760b06af"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
pruneopts = "UT"
revision = "ae18d6b8b3205b561c79e8e5f69bff09736185f4"
version = "v1.0.0"
[[projects]]
name = "github.com/pkg/errors"
digest = "1:42eb1f52b84a06820cedc9baec2e710bfbda3ee6dac6cdb97f8b9a5066134ec6"
name = "github.com/mitchellh/go-testing-interface"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
pruneopts = "UT"
revision = "6d0b8010fcc857872e42fc6c931227569016843c"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:5ab79470a1d0fb19b041a624415612f8236b3c06070161a910562f2b2d064355"
name = "github.com/mitchellh/mapstructure"
packages = ["."]
pruneopts = "UT"
revision = "f15292f7a699fcc1a38a80977f80a046874ba8ac"
[[projects]]
digest = "1:95741de3af260a92cc5c7f3f3061e85273f5a81b5db20d4bd68da74bd521675e"
name = "github.com/pelletier/go-toml"
packages = ["."]
pruneopts = "UT"
revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194"
version = "v1.2.0"
[[projects]]
digest = "1:d14a5f4bfecf017cb780bdde1b6483e5deb87e12c332544d2c430eda58734bcb"
name = "github.com/prometheus/client_golang"
packages = [
"prometheus",
"prometheus/promhttp"
"prometheus/promhttp",
]
pruneopts = "UT"
revision = "c5b7fccd204277076155f10851dad72b76a49317"
version = "v0.8.0"
[[projects]]
branch = "master"
digest = "1:2d5cd61daa5565187e1d96bae64dbbc6080dacf741448e9629c64fd93203b0d4"
name = "github.com/prometheus/client_model"
packages = ["go"]
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
pruneopts = "UT"
revision = "5c3871d89910bfb32f5fcab2aa4b9ec68e65a99f"
[[projects]]
branch = "master"
digest = "1:63b68062b8968092eb86bedc4e68894bd096ea6b24920faca8b9dcf451f54bb5"
name = "github.com/prometheus/common"
packages = [
"expfmt",
"internal/bitbucket.org/ww/goautoneg",
"model"
"model",
]
revision = "e4aa40a9169a88835b849a6efb71e05dc04b88f0"
pruneopts = "UT"
revision = "c7de2306084e37d54b8be01f3541a8464345e9a5"
[[projects]]
branch = "master"
digest = "1:8c49953a1414305f2ff5465147ee576dd705487c35b15918fcd4efdc0cb7a290"
name = "github.com/prometheus/procfs"
packages = [
".",
"internal/util",
"nfs",
"xfs"
"xfs",
]
revision = "54d17b57dd7d4a3aa092476596b3f8a933bde349"
pruneopts = "UT"
revision = "05ee40e3a273f7245e8777337fc7b46e533a9a92"
[[projects]]
name = "github.com/rs/zerolog"
digest = "1:bd1ae00087d17c5a748660b8e89e1043e1e5479d0fea743352cda2f8dd8c4f84"
name = "github.com/spf13/afero"
packages = [
".",
"internal/cbor",
"internal/json",
"log"
"mem",
]
revision = "77db4b4f350e31be66a57c332acb7721cf9ff9bb"
version = "v1.8.0"
pruneopts = "UT"
revision = "787d034dfe70e44075ccc060d346146ef53270ad"
version = "v1.1.1"
[[projects]]
digest = "1:516e71bed754268937f57d4ecb190e01958452336fa73dbac880894164e91c1f"
name = "github.com/spf13/cast"
packages = ["."]
pruneopts = "UT"
revision = "8965335b8c7107321228e3e3702cab9832751bac"
version = "v1.2.0"
[[projects]]
digest = "1:645cabccbb4fa8aab25a956cbcbdf6a6845ca736b2c64e197ca7cbb9d210b939"
name = "github.com/spf13/cobra"
packages = ["."]
pruneopts = "UT"
revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385"
version = "v0.0.3"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix"]
revision = "bd9dbc187b6e1dacfdd2722a87e83093c2d7bd6e"
digest = "1:8a020f916b23ff574845789daee6818daf8d25a4852419aae3f0b12378ba432a"
name = "github.com/spf13/jwalterweatherman"
packages = ["."]
pruneopts = "UT"
revision = "14d3d4c518341bea657dd8a226f5121c0ff8c9f2"
[[projects]]
digest = "1:dab83a1bbc7ad3d7a6ba1a1cc1760f25ac38cdf7d96a5cdd55cd915a4f5ceaf9"
name = "github.com/spf13/pflag"
packages = ["."]
pruneopts = "UT"
revision = "9a97c102cda95a86cec2345a6f09f55a939babf5"
version = "v1.0.2"
[[projects]]
digest = "1:4fc8a61287ccfb4286e1ca5ad2ce3b0b301d746053bf44ac38cf34e40ae10372"
name = "github.com/spf13/viper"
packages = ["."]
pruneopts = "UT"
revision = "907c19d40d9a6c9bb55f040ff4ae45271a4754b9"
version = "v1.1.0"
[[projects]]
digest = "1:4aeb3860275fa1fd60cccfb5a6ef85da438bf17402e1e84412ade4d4b55066a0"
name = "github.com/ulikunitz/xz"
packages = [
".",
"internal/hash",
"internal/xlog",
"lzma",
]
pruneopts = "UT"
revision = "0c6b41e72360850ca4f98dc341fd999726ea007f"
version = "v0.5.4"
[[projects]]
digest = "1:3c1a69cdae3501bf75e76d0d86dc6f2b0a7421bc205c0cb7b96b19eed464a34d"
name = "go.uber.org/atomic"
packages = ["."]
pruneopts = "UT"
revision = "1ea20fb1cbb1cc08cbd0d913a96dead89aa18289"
version = "v1.3.2"
[[projects]]
digest = "1:60bf2a5e347af463c42ed31a493d817f8a72f102543060ed992754e689805d1a"
name = "go.uber.org/multierr"
packages = ["."]
pruneopts = "UT"
revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a"
version = "v1.1.0"
[[projects]]
digest = "1:c52caf7bd44f92e54627a31b85baf06a68333a196b3d8d241480a774733dcf8b"
name = "go.uber.org/zap"
packages = [
".",
"buffer",
"internal/bufferpool",
"internal/color",
"internal/exit",
"zapcore",
]
pruneopts = "UT"
revision = "ff33455a0e382e8a81d14dd7c922020b6b5e7982"
version = "v1.9.1"
[[projects]]
branch = "master"
digest = "1:e3e7f51633f98fce9404940f74dfd65cf306ee173ddf5a8b247c9c830e42b38a"
name = "golang.org/x/sys"
packages = ["unix"]
pruneopts = "UT"
revision = "1a700e749ce29638d0bbcb531cce1094ea096bd3"
[[projects]]
digest = "1:8029e9743749d4be5bc9f7d42ea1659471767860f0cdc34d37c3111bd308a295"
name = "golang.org/x/text"
packages = [
"internal/gen",
"internal/triegen",
"internal/ucd",
"transform",
"unicode/cldr",
"unicode/norm",
]
pruneopts = "UT"
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202"
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "7f97868eec74b32b0982dd158a51a446d1da7eb5"
version = "v2.1.1"
pruneopts = "UT"
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "1b88c0a618973c53f6b715dd51ad99f2952baf09c4d752bc8a985d26439a739c"
input-imports = [
"github.com/chzyer/readline",
"github.com/dgrijalva/jwt-go",
"github.com/fatih/color",
"github.com/fsnotify/fsnotify",
"github.com/gorilla/mux",
"github.com/gorilla/websocket",
"github.com/hashicorp/go-getter",
"github.com/prometheus/client_golang/prometheus",
"github.com/prometheus/client_golang/prometheus/promhttp",
"github.com/spf13/cobra",
"github.com/spf13/pflag",
"github.com/spf13/viper",
"go.uber.org/zap",
"go.uber.org/zap/zapcore",
]
solver-name = "gps-cdcl"
solver-version = 1

View File

@@ -1,24 +1,39 @@
[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.0"
[[constraint]]
name = "github.com/prometheus/client_golang"
version = "0.8.0"
[[constraint]]
name = "github.com/rs/zerolog"
version = "1.8.0"
name = "github.com/gorilla/mux"
version = "v1.6.2"
[[constraint]]
name = "gopkg.in/yaml.v2"
version = "2.1.1"
name = "github.com/gorilla/websocket"
version = "v1.4.0"
[[constraint]]
name = "go.uber.org/zap"
version = "v1.9.1"
[[override]]
name = "github.com/fsnotify/fsnotify"
version = "1.2.9"
[[constraint]]
name = "github.com/spf13/pflag"
version = "v1.0.2"
[[constraint]]
name = "github.com/spf13/viper"
version = "v1.1.0"
[[constraint]]
name = "github.com/spf13/cobra"
version = "v0.0.3"
[[constraint]]
name = "github.com/dgrijalva/jwt-go"
version = "v3.2.0"
[prune]
go-tests = true
unused-packages = true

View File

@@ -20,7 +20,7 @@ build:
@rm -rf build && mkdir build
@echo Building: linux/$(LINUX_ARCH) $(VERSION) ;\
for arch in $(LINUX_ARCH); do \
mkdir -p build/linux/$$arch && CGO_ENABLED=0 GOOS=linux GOARCH=$$arch go build -ldflags="-s -w -X $(GITREPO)/pkg/version.GITCOMMIT=$(GITCOMMIT)" -o build/linux/$$arch/$(NAME) ./cmd/$(NAME) ;\
mkdir -p build/linux/$$arch && CGO_ENABLED=0 GOOS=linux GOARCH=$$arch go build -ldflags="-s -w -X $(GITREPO)/pkg/version.REVISION=$(GITCOMMIT)" -o build/linux/$$arch/$(NAME) ./cmd/$(NAME) ;\
cp -r ui/ build/linux/$$arch/ui;\
done
@@ -73,9 +73,9 @@ docker-build: tar
.PHONY: docker-push
docker-push:
@echo Pushing: $(VERSION) to $(DOCKER_IMAGE_NAME)
for arch in $(LINUX_ARCH); do \
docker push $(DOCKER_IMAGE_NAME):$(NAME)-$$arch ;\
done
for arch in $(LINUX_ARCH); do \
docker push $(DOCKER_IMAGE_NAME):$(NAME)-$$arch ;\
done
manifest-tool push from-args --platforms $(PLATFORMS) --template $(DOCKER_IMAGE_NAME):podinfo-ARCH --target $(DOCKER_IMAGE_NAME):$(VERSION)
manifest-tool push from-args --platforms $(PLATFORMS) --template $(DOCKER_IMAGE_NAME):podinfo-ARCH --target $(DOCKER_IMAGE_NAME):latest
@@ -95,7 +95,7 @@ gcr-build:
.PHONY: test
test:
cd pkg/server ; go test -v -race ./...
go test -v -race ./...
.PHONY: dep
dep:

17
Makefile.gh Normal file
View File

@@ -0,0 +1,17 @@
DOCKER_IMAGE?=stefanprodan/k8s-podinfo
DOCKER_TAG?=$(shell git symbolic-ref --short HEAD)
GIT_REPOSITORY?=stefanprodan/k8s-podinfo
GIT_SHA:=$(shell git describe --dirty --always)
.PHONY: all
all: test build
.PHONY: test
test:
go test ./...
.PHONY: build
build:
docker build -t $(DOCKER_IMAGE):$(DOCKER_TAG) \
--build-arg REPOSITORY=${GIT_REPOSITORY} \
--build-arg SHA=${GIT_SHA} -f Dockerfile.gh .

View File

@@ -9,31 +9,35 @@ Specifications:
* Multi-platform Docker image (amd64/arm/arm64/ppc64le/s390x)
* Health checks (readiness and liveness)
* Graceful shutdown on interrupt signals
* Watches for secrets and configmaps changes and updates the in-memory cache
* Prometheus instrumentation (RED metrics)
* Dependency management with golang/dep
* Structured logging with zerolog
* Error handling with pkg/errors
* File watcher for secrets and configmaps
* Instrumented with Prometheus
* Tracing with Istio and Jaeger
* Structured logging with zap
* 12-factor app with viper
* Fault injection (random errors and latency)
* Helm chart
Web API:
* `GET /` prints runtime information, environment variables, labels and annotations
* `GET /` prints runtime information
* `GET /version` prints podinfo version and git commit hash
* `GET /metrics` http requests duration and Go runtime metrics
* `GET /metrics` return HTTP requests duration and Go runtime metrics
* `GET /healthz` used by Kubernetes liveness probe
* `GET /readyz` used by Kubernetes readiness probe
* `POST /readyz/enable` signals the Kubernetes LB that this instance is ready to receive traffic
* `POST /readyz/disable` signals the Kubernetes LB to stop sending requests to this instance
* `GET /error` returns code 500 and logs the error
* `GET /status/{code}` returns the status code
* `GET /panic` crashes the process with exit code 255
* `POST /echo` echos the posted content, logs the SHA1 hash of the content
* `GET /echoheaders` prints the request HTTP headers
* `POST /job` long running job, json body: `{"wait":2}`
* `GET /configs` prints the configmaps and/or secrets mounted in the `config` volume
* `POST /echo` forwards the call to the backend service and echos the posted content
* `GET /env` returns the environment variables as a JSON array
* `GET /headers` returns a JSON with the request HTTP headers
* `GET /delay/{seconds}` waits for the specified period
* `POST /token` issues a JWT token valid for one minute `JWT=$(curl -sd 'anon' podinfo:9898/token | jq -r .token)`
* `GET /token/validate` validates the JWT token `curl -H "Authorization: Bearer $JWT" podinfo:9898/token/validate`
* `GET /configs` returns a JSON with configmaps and/or secrets mounted in the `config` volume
* `POST /write` writes the posted content to disk at /data/hash and returns the SHA1 hash of the content
* `POST /read` receives a SHA1 hash and returns the content of the file /data/hash if exists
* `POST /backend` forwards the call to the backend service on `http://backend-podinfo:9898/echo`
* `GET /read/{hash}` returns the content of the file /data/hash if exists
* `GET /ws/echo` echos content via websockets `podcli ws ws://localhost:9898/ws/echo`
### Guides
@@ -44,3 +48,4 @@ Web API:
* [Expose Kubernetes services over HTTPS with Ngrok](docs/6-ngrok.md)
* [A/B Testing with Ambassador API Gateway](docs/5-canary.md)
* [Canary Deployments with Istio](docs/7-istio.md)
* [GitHub Actions CI demo](docs/8-gh-actions.md)

File diff suppressed because it is too large Load Diff

View File

@@ -2,4 +2,4 @@ apiVersion: v1
appVersion: "1.0"
description: A Ngrok Helm chart for Kubernetes
name: ngrok
version: 0.1.0
version: 0.2.0

View File

@@ -41,6 +41,7 @@ Parameter | Description | Default
`service.type` | type of service | `ClusterIP`
`token` | Ngrok auth token | `none`
`expose.service` | Service address to be exposed as in `service-name:port` | `none`
`subdomain` | Ngrok subdomain | `none`
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example,

View File

@@ -28,6 +28,9 @@ spec:
command:
- ./ngrok
- http
{{- if .Values.subdomain }}
- --subdomain={{ .Values.subdomain }}
{{- end }}
- {{ .Values.expose.service }}
volumeMounts:
- name: config

View File

@@ -23,3 +23,5 @@ nodeSelector: {}
tolerations: []
affinity: {}
subdomain:

View File

@@ -1,12 +1,12 @@
apiVersion: v1
appVersion: "0.6.0"
description: Podinfo Helm chart for Istio
version: 1.2.1
appVersion: 1.2.1
engine: gotpl
name: podinfo-istio
version: 0.1.0
description: Podinfo Helm chart for Istio
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
- email: stefanprodan@users.noreply.github.com
name: stefanprodan
sources:
- https://github.com/stefanprodan/k8s-podinfo
maintainers:
- name: stefanprodan
email: stefanprodan@users.noreply.github.com
engine: gotpl

View File

@@ -6,13 +6,13 @@ host: backend
# stable release
blue:
replicas: 2
tag: "0.6.0"
tag: "1.1.1"
backend: http://store:9898/api/echo
# canary release
green:
replicas: 2
tag: "0.6.1"
tag: "1.2.0"
routing:
# target green callers
- match:

View File

@@ -9,21 +9,21 @@ exposeHost: true
# if you have a Gateway running you can set the name to your own gateway and turn off create
gateway:
name: public-gateway
create: true
create: false
tls: true
httpsRedirect: true
# stable release
blue:
replicas: 2
tag: "0.6.0"
tag: "1.1.1"
message: "Greetings from the blue frontend"
backend: http://backend:9898/api/echo
# canary release
green:
replicas: 2
tag: "0.6.1"
tag: "1.2.0"
routing:
# target Safari
- match:

View File

@@ -6,13 +6,13 @@ host: store
# load balance 80/20 between blue and green
blue:
replicas: 2
tag: "0.6.0"
tag: "1.1.1"
backend: https://httpbin.org/anything
weight: 80
green:
replicas: 2
tag: "0.6.1"
tag: "1.2.0"
backend: https://httpbin.org/anything
externalServices:

View File

@@ -35,37 +35,40 @@ spec:
imagePullPolicy: {{ .Values.imagePullPolicy }}
command:
- ./podinfo
- -port={{ .Values.containerPort }}
- -logLevel={{ .Values.logLevel }}
- --port={{ .Values.containerPort }}
- --level={{ .Values.logLevel }}
- --random-delay={{ .Values.blue.faults.delay }}
- --random-error={{ .Values.blue.faults.error }}
env:
- name: color
- name: PODINFO_UI_COLOR
value: blue
{{- if .Values.blue.backend }}
- name: backendURL
- name: PODINFO_BACKEND_URL
value: {{ .Values.blue.backend }}
{{- end }}
{{- if .Values.blue.message }}
- name: message
- name: PODINFO_UI_MESSAGE
value: {{ .Values.blue.message }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.containerPort }}
protocol: TCP
readinessProbe:
httpGet:
path: /readyz
port: 9898
initialDelaySeconds: 1
periodSeconds: 2
failureThreshold: 1
livenessProbe:
httpGet:
path: /healthz
port: 9898
initialDelaySeconds: 1
periodSeconds: 10
failureThreshold: 2
exec:
command:
- podcli
- check
- http
- localhost:{{ .Values.containerPort }}/healthz
readinessProbe:
exec:
command:
- podcli
- check
- http
- localhost:{{ .Values.containerPort }}/readyz
periodSeconds: 3
volumeMounts:
- name: data
mountPath: /data

View File

@@ -36,37 +36,40 @@ spec:
imagePullPolicy: {{ .Values.imagePullPolicy }}
command:
- ./podinfo
- -port={{ .Values.containerPort }}
- -logLevel={{ .Values.logLevel }}
- --port={{ .Values.containerPort }}
- --level={{ .Values.logLevel }}
- --random-delay={{ .Values.green.faults.delay }}
- --random-error={{ .Values.green.faults.error }}
env:
- name: color
- name: PODINFO_UI_COLOR
value: green
{{- if .Values.green.backend }}
- name: backendURL
- name: PODINFO_BACKEND_URL
value: {{ .Values.green.backend }}
{{- end }}
{{- if .Values.green.message }}
- name: message
- name: PODINFO_UI_MESSAGE
value: {{ .Values.green.message }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.containerPort }}
protocol: TCP
readinessProbe:
httpGet:
path: /readyz
port: 9898
initialDelaySeconds: 1
periodSeconds: 2
failureThreshold: 1
livenessProbe:
httpGet:
path: /healthz
port: 9898
initialDelaySeconds: 1
periodSeconds: 10
failureThreshold: 2
exec:
command:
- podcli
- check
- http
- localhost:{{ .Values.containerPort }}/healthz
readinessProbe:
exec:
command:
- podcli
- check
- http
- localhost:{{ .Values.containerPort }}/readyz
periodSeconds: 3
volumeMounts:
- name: data
mountPath: /data
@@ -75,4 +78,4 @@ spec:
volumes:
- name: data
emptyDir: {}
{{- end }}
{{- end }}

View File

@@ -1,6 +1,6 @@
# Default values for podinfo-istio.
# host can be an extarnal domain or a local one
# host can be an external domain or a local one
host: podinfo
# if the host is an external domain must be exposed via the Gateway
exposeHost: false
@@ -27,22 +27,27 @@ gateway:
blue:
replicas: 2
repository: quay.io/stefanprodan/podinfo
tag: "0.6.0"
tag: "1.2.1"
# green must have at at least one replica to set weight under 100
weight: 100
message:
backend:
faults:
delay: false
error: false
# canary release
# disabled with 0 replicas
green:
replicas: 0
repository: quay.io/stefanprodan/podinfo
tag: "0.6.1"
tag: "1.2.1"
message:
backend:
routing:
faults:
delay: false
error: false
# blue/green common settings
logLevel: info

View File

@@ -1,12 +1,12 @@
apiVersion: v1
appVersion: "0.6.0"
description: Podinfo Helm chart for Kubernetes
version: 1.3.1
appVersion: 1.3.1
name: podinfo
version: 0.2.2
engine: gotpl
description: Podinfo Helm chart for Kubernetes
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
- email: stefanprodan@users.noreply.github.com
name: stefanprodan
sources:
- https://github.com/stefanprodan/k8s-podinfo
maintainers:
- name: stefanprodan
email: stefanprodan@users.noreply.github.com
engine: gotpl

View File

@@ -8,7 +8,8 @@ that showcases best practices of running microservices in Kubernetes.
To install the chart with the release name `my-release`:
```console
$ helm install stable/podinfo --name my-release
$ helm repo add sp https://stefanprodan.github.io/k8s-podinfo
$ helm upgrade my-release --install sp/podinfo
```
The command deploys podinfo on the Kubernetes cluster in the default namespace.
@@ -31,23 +32,27 @@ The following tables lists the configurable parameters of the podinfo chart and
Parameter | Description | Default
--- | --- | ---
`affinity` | node/pod affinities | None
`hpa.enabled` | Enables HPA | `false`
`hpa.cpu` | Target CPU usage per pod | None
`hpa.memory` | Target memory usage per pod | None
`hpa.requests` | Target requests per second per pod | None
`hpa.maxReplicas` | Maximum pod replicas | `10`
`ingress.hosts` | Ingress accepted hostnames | None
`ingress.tls` | Ingress TLS configuration | None:
`image.pullPolicy` | Image pull policy | `IfNotPresent`
`image.repository` | Image repository | `stefanprodan/podinfo`
`image.tag` | Image tag | `0.0.1`
`ingress.enabled` | Enables Ingress | `false`
`ingress.annotations` | Ingress annotations | None
`ingress.hosts` | Ingress accepted hostnames | None
`ingress.tls` | Ingress TLS configuration | None
`color` | UI color | blue
`backend` | echo backend URL | None
`faults.delay` | random HTTP response delays between 0 and 5 seconds | `false`
`faults.error` | 1/3 chances of a random HTTP response error | `false`
`hpa.enabled` | enables HPA | `false`
`hpa.cpu` | target CPU usage per pod | None
`hpa.memory` | target memory usage per pod | None
`hpa.requests` | target requests per second per pod | None
`hpa.maxReplicas` | maximum pod replicas | `10`
`ingress.hosts` | ingress accepted hostnames | None
`ingress.tls` | ingress TLS configuration | None:
`image.pullPolicy` | image pull policy | `IfNotPresent`
`image.repository` | image repository | `stefanprodan/podinfo`
`image.tag` | image tag | `0.0.1`
`ingress.enabled` | enables ingress | `false`
`ingress.annotations` | ingress annotations | None
`ingress.hosts` | ingress accepted hostnames | None
`ingress.tls` | ingress TLS configuration | None
`message` | UI greetings message | None
`nodeSelector` | node labels for pod assignment | `{}`
`podAnnotations` | annotations to add to each pod | `{}`
`replicaCount` | desired number of pods | `1`
`replicaCount` | desired number of pods | `2`
`resources.requests/cpu` | pod CPU request | `1m`
`resources.requests/memory` | pod memory request | `16Mi`
`resources.limits/cpu` | pod CPU limit | None
@@ -56,7 +61,7 @@ Parameter | Description | Default
`service.internalPort` | internal port for the service | `9898`
`service.nodePort` | node port for the service | `31198`
`service.type` | type of service | `ClusterIP`
`tolerations` | List of node taints to tolerate | `[]`
`tolerations` | list of node taints to tolerate | `[]`
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example,

View File

@@ -38,33 +38,40 @@ spec:
imagePullPolicy: {{ .Values.image.pullPolicy }}
command:
- ./podinfo
- -port={{ .Values.service.containerPort }}
- -logLevel={{ .Values.logLevel }}
- --port={{ .Values.service.containerPort }}
- --level={{ .Values.logLevel }}
- --random-delay={{ .Values.faults.delay }}
- --random-error={{ .Values.faults.error }}
env:
- name: color
- name: PODINFO_UI_COLOR
value: {{ .Values.color }}
{{- if .Values.message }}
- name: PODINFO_UI_MESSAGE
value: {{ .Values.message }}
{{- end }}
{{- if .Values.backend }}
- name: backendURL
- name: PODINFO_BACKEND_URL
value: {{ .Values.backend }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.service.containerPort }}
protocol: TCP
readinessProbe:
httpGet:
path: /readyz
port: 9898
initialDelaySeconds: 1
periodSeconds: 2
failureThreshold: 1
livenessProbe:
httpGet:
path: /healthz
port: 9898
initialDelaySeconds: 1
periodSeconds: 10
failureThreshold: 2
exec:
command:
- podcli
- check
- http
- localhost:{{ .Values.service.containerPort }}/healthz
readinessProbe:
exec:
command:
- podcli
- check
- http
- localhost:{{ .Values.service.containerPort }}/readyz
periodSeconds: 3
volumeMounts:
- name: data
mountPath: /data

View File

@@ -1,12 +1,18 @@
# Default values for podinfo.
replicaCount: 2
logLevel: info
color: blue
backend: #http://backend-podinfo:9898/echo
message: #UI greetings
faults:
delay: false
error: false
image:
repository: quay.io/stefanprodan/podinfo
tag: 0.6.0
tag: 1.3.1
pullPolicy: IfNotPresent
service:
@@ -51,4 +57,3 @@ tolerations: []
affinity: {}
logLevel: info

245
cmd/podcli/check.go Normal file
View File

@@ -0,0 +1,245 @@
package main
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
var (
retryCount int
retryDelay time.Duration
method string
body string
timeout time.Duration
)
var checkCmd = &cobra.Command{
Use: `check`,
Short: "Health check commands",
Long: "Commands for running health checks",
}
var checkUrlCmd = &cobra.Command{
Use: `http [address]`,
Short: "HTTP(S) health check",
Example: ` check http https://httpbin.org/anything --method=POST --retry=2 --delay=2s --timeout=3s --body='{"test"=1}'`,
RunE: runCheck,
}
var checkTcpCmd = &cobra.Command{
Use: `tcp [address]`,
Short: "TCP health check",
Example: ` check tcp httpbin.org:443 --retry=1 --delay=2s --timeout=2s`,
RunE: runCheckTCP,
}
var checkCertCmd = &cobra.Command{
Use: `cert [address]`,
Short: "SSL/TLS certificate validity check",
Example: ` check cert httpbin.org`,
RunE: runCheckCert,
}
func init() {
checkUrlCmd.Flags().StringVar(&method, "method", "GET", "HTTP method")
checkUrlCmd.Flags().StringVar(&body, "body", "", "HTTP POST/PUT content")
checkUrlCmd.Flags().IntVar(&retryCount, "retry", 0, "times to retry the HTTP call")
checkUrlCmd.Flags().DurationVar(&retryDelay, "delay", 1*time.Second, "wait duration between retries")
checkUrlCmd.Flags().DurationVar(&timeout, "timeout", 5*time.Second, "timeout")
checkCmd.AddCommand(checkUrlCmd)
checkTcpCmd.Flags().IntVar(&retryCount, "retry", 0, "times to retry the TCP check")
checkTcpCmd.Flags().DurationVar(&retryDelay, "delay", 1*time.Second, "wait duration between retries")
checkTcpCmd.Flags().DurationVar(&timeout, "timeout", 5*time.Second, "timeout")
checkCmd.AddCommand(checkTcpCmd)
checkCmd.AddCommand(checkCertCmd)
rootCmd.AddCommand(checkCmd)
}
func runCheck(cmd *cobra.Command, args []string) error {
if retryCount < 0 {
return fmt.Errorf("--retry is required")
}
if len(args) < 1 {
return fmt.Errorf("address is required! example: check http https://httpbin.org")
}
address := args[0]
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
address = fmt.Sprintf("http://%s", address)
}
for n := 0; n <= retryCount; n++ {
if n != 1 {
time.Sleep(retryDelay)
}
req, err := http.NewRequest(method, address, bytes.NewBuffer([]byte(body)))
if err != nil {
logger.Info("check failed",
zap.String("address", address),
zap.Error(err))
os.Exit(1)
}
ctx, cancel := context.WithTimeout(req.Context(), timeout)
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
cancel()
if err != nil {
logger.Info("check failed",
zap.String("address", address),
zap.Error(err))
continue
}
if resp.Body != nil {
resp.Body.Close()
}
if resp.StatusCode >= 200 && resp.StatusCode < 400 {
logger.Info("check succeed",
zap.String("address", address),
zap.Int("status code", resp.StatusCode),
zap.String("response size", fmtContentLength(resp.ContentLength)))
os.Exit(0)
} else {
logger.Info("check failed",
zap.String("address", address),
zap.Int("status code", resp.StatusCode))
continue
}
}
os.Exit(1)
return nil
}
func runCheckTCP(cmd *cobra.Command, args []string) error {
if retryCount < 0 {
return fmt.Errorf("--retry is required")
}
if len(args) < 1 {
return fmt.Errorf("address is required! example: check tcp httpbin.org:80")
}
address := args[0]
for n := 0; n <= retryCount; n++ {
if n != 1 {
time.Sleep(retryDelay)
}
conn, err := net.DialTimeout("tcp", address, timeout)
if err != nil {
logger.Info("check failed",
zap.String("address", address),
zap.Error(err))
continue
}
conn.Close()
logger.Info("check succeed", zap.String("address", address))
os.Exit(0)
}
os.Exit(1)
return nil
}
func runCheckCert(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("address is required! example: check cert httpbin.org")
}
host := args[0]
if !strings.HasPrefix(host, "https://") {
host = "https://" + host
}
u, err := url.Parse(host)
if err != nil {
logger.Info("check failed",
zap.String("address", host),
zap.Error(err))
os.Exit(1)
}
address := u.Hostname() + ":443"
ipConn, err := net.DialTimeout("tcp", address, 5*time.Second)
if err != nil {
logger.Info("check failed",
zap.String("address", address),
zap.Error(err))
os.Exit(1)
}
defer ipConn.Close()
conn := tls.Client(ipConn, &tls.Config{
InsecureSkipVerify: true,
ServerName: u.Hostname(),
})
if err = conn.Handshake(); err != nil {
logger.Info("check failed",
zap.String("address", address),
zap.Error(err))
os.Exit(1)
}
defer conn.Close()
addr := conn.RemoteAddr()
_, _, err = net.SplitHostPort(addr.String())
if err != nil {
logger.Info("check failed",
zap.String("address", address),
zap.Error(err))
os.Exit(1)
}
cert := conn.ConnectionState().PeerCertificates[0]
timeNow := time.Now()
if timeNow.After(cert.NotAfter) {
logger.Info("check failed",
zap.String("address", address),
zap.String("issuer", cert.Issuer.CommonName),
zap.String("subject", cert.Subject.CommonName),
zap.Time("expired", cert.NotAfter))
os.Exit(1)
}
logger.Info("check succeed",
zap.String("address", address),
zap.String("issuer", cert.Issuer.CommonName),
zap.String("subject", cert.Subject.CommonName),
zap.Time("notAfter", cert.NotAfter),
zap.Time("notBefore", cert.NotBefore))
return nil
}
func fmtContentLength(b int64) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
}

332
cmd/podcli/code.go Normal file
View File

@@ -0,0 +1,332 @@
package main
import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/hashicorp/go-getter"
"github.com/spf13/cobra"
)
var (
codeProjectName string
codeGitUser string
codeVersion string
codeProjectPath string
)
var codeCmd = &cobra.Command{
Use: `code`,
Short: "Code commands",
}
var codeInitCmd = &cobra.Command{
Use: `init [name]`,
Short: "initialize podinfo code repo",
Example: ` code init demo-app --version=v1.2.0 --git-user=stefanprodan`,
RunE: runCodeInit,
}
func init() {
codeInitCmd.Flags().StringVar(&codeGitUser, "git-user", "", "GitHub user or org")
codeInitCmd.Flags().StringVar(&codeVersion, "version", "master", "podinfo repo tag or branch name")
codeInitCmd.Flags().StringVar(&codeProjectPath, "path", ".", "destination repo")
codeCmd.AddCommand(codeInitCmd)
rootCmd.AddCommand(codeCmd)
}
func runCodeInit(cmd *cobra.Command, args []string) error {
if len(codeGitUser) < 0 {
return fmt.Errorf("--git-user is required")
}
if len(args) < 1 {
return fmt.Errorf("project name is required")
}
codeProjectName = args[0]
pwd, err := os.Getwd()
if err != nil {
log.Fatalf("Error getting pwd: %s", err)
os.Exit(1)
}
tmpPath := "/tmp/k8s-podinfo"
versionName := fmt.Sprintf("k8s-podinfo-%s", codeVersion)
downloadURL := fmt.Sprintf("https://github.com/stefanprodan/k8s-podinfo/archive/%s.zip", codeVersion)
client := &getter.Client{
Src: downloadURL,
Dst: tmpPath,
Pwd: pwd,
Mode: getter.ClientModeAny,
}
fmt.Printf("Downloading %s\n", downloadURL)
if err := client.Get(); err != nil {
log.Fatalf("Error downloading: %s", err)
os.Exit(1)
}
pkgFrom := "github.com/stefanprodan/k8s-podinfo"
pkgTo := fmt.Sprintf("github.com/%s/%s", codeGitUser, codeProjectName)
if err := replaceImports(tmpPath, pkgFrom, pkgTo); err != nil {
log.Fatalf("Error parsing imports: %s", err)
os.Exit(1)
}
dirs := []string{"pkg", "cmd", "ui", "vendor", ".github"}
for _, dir := range dirs {
err = os.MkdirAll(path.Join(codeProjectPath, dir), os.ModePerm)
if err != nil {
log.Fatalf("Error: %s", err)
os.Exit(1)
}
if err := copyDir(path.Join(tmpPath, versionName, dir), path.Join(codeProjectPath, dir)); err != nil {
log.Fatalf("Error: %s", err)
os.Exit(1)
}
}
files := []string{"Gopkg.toml", "Gopkg.lock"}
for _, file := range files {
if err := copyFile(path.Join(tmpPath, versionName, file), path.Join(codeProjectPath, file)); err != nil {
log.Fatalf("Error: %s", err)
os.Exit(1)
}
fileContent, err := ioutil.ReadFile(path.Join(codeProjectPath, file))
if err != nil {
log.Fatalf("Error: %s", err)
os.Exit(1)
}
newContent := strings.Replace(string(fileContent), pkgFrom, pkgTo, -1)
err = ioutil.WriteFile(path.Join(codeProjectPath, file), []byte(newContent), os.ModePerm)
if err != nil {
log.Fatalf("Error: %s", err)
os.Exit(1)
}
}
projFrom := "stefanprodan/k8s-podinfo"
projTo := fmt.Sprintf("%s/%s", codeGitUser, codeProjectName)
makeFiles := []string{"Makefile.gh", "Dockerfile.gh"}
for _, file := range makeFiles {
fileContent, err := ioutil.ReadFile(path.Join(tmpPath, versionName, file))
if err != nil {
log.Fatalf("Error: %s", err)
os.Exit(1)
}
destFile := strings.Replace(file, ".gh", "", -1)
newContent := strings.Replace(string(fileContent), projFrom, projTo, -1)
err = ioutil.WriteFile(path.Join(codeProjectPath, destFile), []byte(newContent), os.ModePerm)
if err != nil {
log.Fatalf("Error: %s", err)
os.Exit(1)
}
}
workflows := []string{".github/main.workflow"}
for _, file := range workflows {
fileContent, err := ioutil.ReadFile(path.Join(codeProjectPath, file))
if err != nil {
log.Fatalf("Error: %s", err)
os.Exit(1)
}
newContent := strings.Replace(string(fileContent), "Dockerfile.gh", "Dockerfile", -1)
err = ioutil.WriteFile(path.Join(codeProjectPath, file), []byte(newContent), os.ModePerm)
if err != nil {
log.Fatalf("Error: %s", err)
os.Exit(1)
}
}
err = gitPush()
if err != nil {
log.Fatalf("git push error: %s", err)
os.Exit(1)
}
fmt.Println("Initialization finished")
return nil
}
func gitPush() error {
cmdPush := fmt.Sprintf("git add . && git commit -m \"sync %s\" && git push", codeVersion)
cmd := exec.Command("sh", "-c", cmdPush)
output, err := cmd.Output()
if err != nil {
return err
}
fmt.Println(string(output))
return nil
}
func replaceImports(projectPath string, pkgFrom string, pkgTo string) error {
regexImport, err := regexp.Compile(`(?s)(import(.*?)\)|import.*$)`)
if err != nil {
return err
}
regexImportedPackage, err := regexp.Compile(`"(.*?)"`)
if err != nil {
return err
}
found := []string{}
err = filepath.Walk(projectPath, func(path string, info os.FileInfo, err error) error {
if filepath.Ext(path) == ".go" {
bts, err := ioutil.ReadFile(path)
if err != nil {
return err
}
content := string(bts)
matches := regexImport.FindAllString(content, -1)
isExists := false
isReplaceable:
for _, each := range matches {
for _, eachLine := range strings.Split(each, "\n") {
matchesInline := regexImportedPackage.FindAllString(eachLine, -1)
if err != nil {
return err
}
for _, eachSubline := range matchesInline {
if strings.Contains(eachSubline, pkgFrom) {
isExists = true
break isReplaceable
}
}
}
}
if isExists {
content = strings.Replace(content, `"`+pkgFrom+`"`, `"`+pkgTo+`"`, -1)
content = strings.Replace(content, `"`+pkgFrom+`/`, `"`+pkgTo+`/`, -1)
found = append(found, path)
}
err = ioutil.WriteFile(path, []byte(content), info.Mode())
if err != nil {
return err
}
}
return nil
})
if err != nil {
fmt.Println("ERROR", err.Error())
}
if len(found) == 0 {
fmt.Println("Nothing replaced")
} else {
fmt.Printf("Go imports total %d file replaced\n", len(found))
}
return nil
}
func copyDir(src string, dst string) error {
si, err := os.Stat(src)
if err != nil {
return err
}
if !si.IsDir() {
return fmt.Errorf("source is not a directory")
}
err = os.MkdirAll(dst, si.Mode())
if err != nil {
return err
}
entries, err := ioutil.ReadDir(src)
if err != nil {
return err
}
for _, entry := range entries {
srcPath := filepath.Join(src, entry.Name())
dstPath := filepath.Join(dst, entry.Name())
if entry.IsDir() {
err = copyDir(srcPath, dstPath)
if err != nil {
return err
}
} else {
// Skip symlinks.
if entry.Mode()&os.ModeSymlink != 0 {
continue
}
err = copyFile(srcPath, dstPath)
if err != nil {
return err
}
}
}
return nil
}
func copyFile(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
if e := out.Close(); e != nil {
err = e
}
}()
_, err = io.Copy(out, in)
if err != nil {
return
}
err = out.Sync()
if err != nil {
return
}
si, err := os.Stat(src)
if err != nil {
return
}
err = os.Chmod(dst, si.Mode())
if err != nil {
return
}
return
}

39
cmd/podcli/main.go Normal file
View File

@@ -0,0 +1,39 @@
package main
import (
"fmt"
"log"
"os"
"strings"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
var rootCmd = &cobra.Command{
Use: "podcli",
Short: "podinfo command line",
Long: `
podinfo command line utilities`,
}
var (
logger *zap.Logger
)
func main() {
var err error
logger, err = zap.NewDevelopment()
if err != nil {
log.Fatalf("can't initialize zap logger: %v", err)
}
defer logger.Sync()
rootCmd.SetArgs(os.Args[1:])
if err := rootCmd.Execute(); err != nil {
e := err.Error()
fmt.Println(strings.ToUpper(e[:1]) + e[1:])
os.Exit(1)
}
}

21
cmd/podcli/version.go Normal file
View File

@@ -0,0 +1,21 @@
package main
import (
"fmt"
"github.com/spf13/cobra"
"github.com/stefanprodan/k8s-podinfo/pkg/version"
)
func init() {
rootCmd.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
Use: `version`,
Short: "Prints podcli version",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println(version.VERSION)
return nil
},
}

143
cmd/podcli/ws.go Normal file
View File

@@ -0,0 +1,143 @@
package main
import (
"encoding/hex"
"fmt"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"github.com/chzyer/readline"
"github.com/fatih/color"
"github.com/gorilla/websocket"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
var origin string
func init() {
wsCmd.Flags().StringVarP(&origin, "origin", "o", "", "websocket origin")
rootCmd.AddCommand(wsCmd)
}
var wsCmd = &cobra.Command{
Use: `ws [address]`,
Short: "Websocket client",
Example: ` ws localhost:9898/ws/echo`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("address is required")
}
address := args[0]
if !strings.HasPrefix(address, "ws://") && !strings.HasPrefix(address, "wss://") {
address = fmt.Sprintf("ws://%s", address)
}
dest, err := url.Parse(address)
if err != nil {
return err
}
if origin != "" {
} else {
originURL := *dest
if dest.Scheme == "wss" {
originURL.Scheme = "https"
} else {
originURL.Scheme = "http"
}
origin = originURL.String()
}
err = connect(dest.String(), origin, &readline.Config{
Prompt: "> ",
})
if err != nil {
logger.Info("websocket closed", zap.Error(err))
}
return nil
},
}
type session struct {
ws *websocket.Conn
rl *readline.Instance
errChan chan error
}
func connect(url, origin string, rlConf *readline.Config) error {
headers := make(http.Header)
headers.Add("Origin", origin)
ws, _, err := websocket.DefaultDialer.Dial(url, headers)
if err != nil {
return err
}
rl, err := readline.NewEx(rlConf)
if err != nil {
return err
}
defer rl.Close()
sess := &session{
ws: ws,
rl: rl,
errChan: make(chan error),
}
go sess.readConsole()
go sess.readWebsocket()
return <-sess.errChan
}
func (s *session) readConsole() {
for {
line, err := s.rl.Readline()
if err != nil {
s.errChan <- err
return
}
err = s.ws.WriteMessage(websocket.TextMessage, []byte(line))
if err != nil {
s.errChan <- err
return
}
}
}
func bytesToFormattedHex(bytes []byte) string {
text := hex.EncodeToString(bytes)
return regexp.MustCompile("(..)").ReplaceAllString(text, "$1 ")
}
func (s *session) readWebsocket() {
rxSprintf := color.New(color.FgGreen).SprintfFunc()
for {
msgType, buf, err := s.ws.ReadMessage()
if err != nil {
fmt.Fprint(s.rl.Stdout(), rxSprintf("< %s\n", err.Error()))
os.Exit(1)
return
}
var text string
switch msgType {
case websocket.TextMessage:
text = string(buf)
case websocket.BinaryMessage:
text = bytesToFormattedHex(buf)
default:
s.errChan <- fmt.Errorf("unknown websocket frame type: %d", msgType)
return
}
fmt.Fprint(s.rl.Stdout(), rxSprintf("< %s\n", text))
}
}

View File

@@ -1,82 +1,160 @@
package main
import (
"flag"
"fmt"
"io/ioutil"
stdlog "log"
"os"
"path/filepath"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/stefanprodan/k8s-podinfo/pkg/server"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/stefanprodan/k8s-podinfo/pkg/api"
"github.com/stefanprodan/k8s-podinfo/pkg/signals"
"github.com/stefanprodan/k8s-podinfo/pkg/version"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var (
port string
debug bool
logLevel string
stressCPU int
stressMemory int
stressMemoryPayload []byte
)
func init() {
flag.StringVar(&port, "port", "9898", "Port to listen on.")
flag.BoolVar(&debug, "debug", false, "sets log level to debug")
flag.StringVar(&logLevel, "logLevel", "debug", "sets log level as debug, info, warn, error, flat or panic ")
flag.IntVar(&stressCPU, "stressCPU", 0, "Number of CPU cores with 100% load")
flag.IntVar(&stressMemory, "stressMemory", 0, "MB of data to load into memory")
}
func main() {
flag.Parse()
setLogging()
// flags definition
fs := pflag.NewFlagSet("default", pflag.ContinueOnError)
fs.Int("port", 9898, "port")
fs.String("level", "info", "log level debug, info, warn, error, flat or panic")
fs.String("backend-url", "", "backend service URL")
fs.Duration("http-client-timeout", 2*time.Minute, "client timeout duration")
fs.Duration("http-server-timeout", 30*time.Second, "server read and write timeout duration")
fs.Duration("http-server-shutdown-timeout", 5*time.Second, "server graceful shutdown timeout duration")
fs.String("data-path", "/data", "data local path")
fs.String("config-path", "", "config dir path")
fs.String("config", "config.yaml", "config file name")
fs.String("ui-path", "./ui", "UI local path")
fs.String("ui-color", "blue", "UI color")
fs.String("ui-message", fmt.Sprintf("greetings from podinfo v%v", version.VERSION), "UI message")
fs.Bool("random-delay", false, "between 0 and 5 seconds random delay")
fs.Bool("random-error", false, "1/3 chances of a random response error")
fs.Int("stress-cpu", 0, "Number of CPU cores with 100 load")
fs.Int("stress-memory", 0, "MB of data to load into memory")
log.Info().Msgf("Starting podinfo version %s commit %s", version.VERSION, version.GITCOMMIT)
log.Debug().Msgf("Starting HTTP server on port %v", port)
versionFlag := fs.BoolP("version", "v", false, "get version number")
// parse flags
err := fs.Parse(os.Args[1:])
switch {
case err == pflag.ErrHelp:
os.Exit(0)
case err != nil:
fmt.Fprintf(os.Stderr, "Error: %s\n\n", err.Error())
fs.PrintDefaults()
os.Exit(2)
case *versionFlag:
fmt.Println(version.VERSION)
os.Exit(0)
}
// bind flags and environment variables
viper.BindPFlags(fs)
viper.RegisterAlias("backendUrl", "backend-url")
hostname, _ := os.Hostname()
viper.SetDefault("jwt-secret", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9")
viper.Set("hostname", hostname)
viper.Set("version", version.VERSION)
viper.Set("revision", version.REVISION)
viper.SetEnvPrefix("PODINFO")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
viper.AutomaticEnv()
// load config from file
if _, err := os.Stat(filepath.Join(viper.GetString("config-path"), viper.GetString("config"))); err == nil {
viper.SetConfigName(strings.Split(viper.GetString("config"), ".")[0])
viper.AddConfigPath(viper.GetString("config-path"))
if err := viper.ReadInConfig(); err != nil {
fmt.Printf("Error reading config file, %v\n", err)
}
}
// configure logging
logger, _ := initZap(viper.GetString("level"))
defer logger.Sync()
stdLog := zap.RedirectStdLog(logger)
defer stdLog()
// start stress tests if any
beginStressTest(viper.GetInt("stress-cpu"), viper.GetInt("stress-memory"), logger)
// load HTTP server config
var srvCfg api.Config
if err := viper.Unmarshal(&srvCfg); err != nil {
logger.Panic("config unmarshal failed", zap.Error(err))
}
// log version and port
logger.Info("Starting podinfo",
zap.String("version", viper.GetString("version")),
zap.String("revision", viper.GetString("revision")),
zap.String("port", viper.GetString("port")),
)
// start HTTP server
srv, _ := api.NewServer(&srvCfg, logger)
stopCh := signals.SetupSignalHandler()
beginStressTest(stressCPU, stressMemory)
server.ListenAndServe(port, 5*time.Second, stopCh)
srv.ListenAndServe(stopCh)
}
func setLogging() {
// set global log level
func initZap(logLevel string) (*zap.Logger, error) {
level := zap.NewAtomicLevelAt(zapcore.InfoLevel)
switch logLevel {
case "debug":
zerolog.SetGlobalLevel(zerolog.DebugLevel)
level = zap.NewAtomicLevelAt(zapcore.DebugLevel)
case "info":
zerolog.SetGlobalLevel(zerolog.InfoLevel)
level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
case "warn":
zerolog.SetGlobalLevel(zerolog.WarnLevel)
level = zap.NewAtomicLevelAt(zapcore.WarnLevel)
case "error":
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
level = zap.NewAtomicLevelAt(zapcore.ErrorLevel)
case "fatal":
zerolog.SetGlobalLevel(zerolog.FatalLevel)
level = zap.NewAtomicLevelAt(zapcore.FatalLevel)
case "panic":
zerolog.SetGlobalLevel(zerolog.PanicLevel)
default:
zerolog.SetGlobalLevel(zerolog.InfoLevel)
level = zap.NewAtomicLevelAt(zapcore.PanicLevel)
}
// keep for backwards compatibility
if debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
zapEncoderConfig := zapcore.EncoderConfig{
TimeKey: "ts",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
// set zerolog as standard logger
stdlog.SetFlags(0)
stdlog.SetOutput(log.Logger)
zapConfig := zap.Config{
Level: level,
Development: false,
Sampling: &zap.SamplingConfig{
Initial: 100,
Thereafter: 100,
},
Encoding: "json",
EncoderConfig: zapEncoderConfig,
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
return zapConfig.Build()
}
func beginStressTest(cpus int, mem int) {
var stressMemoryPayload []byte
func beginStressTest(cpus int, mem int, logger *zap.Logger) {
done := make(chan int)
if cpus > 0 {
log.Info().Msgf("Starting CPU stress, %v core(s)", cpus)
logger.Info("starting CPU stress", zap.Int("cores", cpus))
for i := 0; i < cpus; i++ {
go func() {
for {
@@ -96,19 +174,19 @@ func beginStressTest(cpus int, mem int) {
f, err := os.Create(path)
if err != nil {
log.Error().Err(err).Msgf("Memory stress failed")
logger.Error("memory stress failed", zap.Error(err))
}
if err := f.Truncate(1000000 * int64(mem)); err != nil {
log.Error().Err(err).Msgf("Memory stress failed")
logger.Error("memory stress failed", zap.Error(err))
}
stressMemoryPayload, err = ioutil.ReadFile(path)
f.Close()
os.Remove(path)
if err != nil {
log.Error().Err(err).Msgf("Memory stress failed")
logger.Error("memory stress failed", zap.Error(err))
}
log.Info().Msgf("Starting memory stress, size %v", len(stressMemoryPayload))
logger.Info("starting CPU stress", zap.Int("memory", len(stressMemoryPayload)))
}
}

View File

@@ -9,75 +9,41 @@ spec:
metadata:
labels:
app: podinfo
#role: openfaas-system
annotations:
prometheus.io/scrape: 'true'
spec:
containers:
- name: podinfod
image: stefanprodan/podinfo:0.0.9
imagePullPolicy: Always
image: quay.io/stefanprodan/podinfo:1.0.1
command:
- ./podinfo
- -port=9898
- -logtostderr=true
- -v=2
volumeMounts:
- name: metadata
mountPath: /etc/podinfod/metadata
readOnly: true
- name: resources
mountPath: /etc/podinfod/resources
readOnly: true
- --port=9898
- --level=debug
ports:
- containerPort: 9898
- name: http
containerPort: 9898
protocol: TCP
readinessProbe:
httpGet:
path: /readyz
port: 9898
initialDelaySeconds: 1
periodSeconds: 2
failureThreshold: 1
livenessProbe:
httpGet:
path: /healthz
port: 9898
initialDelaySeconds: 1
periodSeconds: 3
failureThreshold: 2
exec:
command:
- /bin/sh
- -c
- wget --quiet --tries=1 --spider http://localhost:9898/healthz || exit 1
readinessProbe:
exec:
command:
- /bin/sh
- -c
- wget --quiet --tries=1 --spider http://localhost:9898/readyz || exit 1
resources:
requests:
memory: "32Mi"
cpu: "10m"
limits:
memory: "256Mi"
cpu: "100m"
volumes:
- name: metadata
downwardAPI:
items:
- path: "labels"
fieldRef:
fieldPath: metadata.labels
- path: "annotations"
fieldRef:
fieldPath: metadata.annotations
- name: resources
downwardAPI:
items:
- path: "cpu_limit"
resourceFieldRef:
containerName: podinfod
resource: limits.cpu
- path: "cpu_request"
resourceFieldRef:
containerName: podinfod
resource: requests.cpu
- path: "mem_limit"
resourceFieldRef:
containerName: podinfod
resource: limits.memory
- path: "mem_request"
resourceFieldRef:
containerName: podinfod
resource: requests.memory
env:
- name: color
value: "blue"
- name: message
value: "Greetings from podinfo blue"
- name: backendURL
value: "http://podinfo-backend:9898/backend"

View File

@@ -2,13 +2,14 @@
apiVersion: v1
kind: Service
metadata:
name: podinfo-clusterip
name: podinfo
labels:
app: podinfo
spec:
type: ClusterIP
ports:
- port: 9898
- name: http
port: 9898
targetPort: 9898
protocol: TCP
selector:

View File

@@ -0,0 +1,32 @@
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: grafana
namespace: adm
spec:
hosts:
- "helm.iowa.weavedx.com"
gateways:
- public-gateway.istio-system.svc.cluster.local
http:
- route:
- destination:
host: chartmuseum-chartmuseum
timeout: 30s
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: kubeapps
namespace: adm
spec:
hosts:
- "kubeapps.iowa.weavedx.com"
gateways:
- public-gateway.istio-system.svc.cluster.local
http:
- route:
- destination:
host: kubeapps
timeout: 30s

View File

@@ -0,0 +1,19 @@
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: env
namespace: openfaas-fn
spec:
hosts:
- env
http:
- route:
- destination:
host: env
weight: 90
- destination:
host: env-canary
weight: 10
timeout: 30s

View File

@@ -0,0 +1,51 @@
apiVersion: authentication.istio.io/v1alpha1
kind: Policy
metadata:
name: default
namespace: openfaas
spec:
peers:
- mtls: {}
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: default
namespace: openfaas
spec:
host: "*.openfaas.svc.cluster.local"
trafficPolicy:
tls:
mode: ISTIO_MUTUAL
---
apiVersion: authentication.istio.io/v1alpha1
kind: Policy
metadata:
name: default
namespace: openfaas-fn
spec:
peers:
- mtls: {}
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: default
namespace: openfaas-fn
spec:
host: "*.openfaas-fn.svc.cluster.local"
trafficPolicy:
tls:
mode: ISTIO_MUTUAL
---
apiVersion: authentication.istio.io/v1alpha1
kind: Policy
metadata:
name: openfaas-permissive
namespace: openfaas
spec:
targets:
- name: gateway
peers:
- mtls:
mode: PERMISSIVE

View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Namespace
metadata:
labels:
istio-injection: enabled
name: openfaas
---
apiVersion: v1
kind: Namespace
metadata:
labels:
istio-injection: enabled
name: openfaas-fn

View File

@@ -0,0 +1,55 @@
apiVersion: config.istio.io/v1alpha2
kind: denier
metadata:
name: denyhandler
namespace: openfaas
spec:
status:
code: 7
message: Not allowed
---
apiVersion: config.istio.io/v1alpha2
kind: checknothing
metadata:
name: denyrequest
namespace: openfaas
spec:
---
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: denyopenfaasfn
namespace: openfaas
spec:
match: destination.namespace == "openfaas" && source.namespace == "openfaas-fn" && source.labels["role"] != "openfaas-system"
actions:
- handler: denyhandler.denier
instances: [ denyrequest.checknothing ]
---
apiVersion: config.istio.io/v1alpha2
kind: denier
metadata:
name: denyhandler
namespace: openfaas-fn
spec:
status:
code: 7
message: Not allowed
---
apiVersion: config.istio.io/v1alpha2
kind: checknothing
metadata:
name: denyrequest
namespace: openfaas-fn
spec:
---
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: denyopenfaasfn
namespace: openfaas-fn
spec:
match: destination.namespace == "openfaas-fn" && source.namespace != "openfaas" && source.labels["role"] != "openfaas-system"
actions:
- handler: denyhandler.denier
instances: [ denyrequest.checknothing ]

View File

@@ -0,0 +1,17 @@
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: gateway
namespace: openfaas
spec:
hosts:
- "openfaas.istio.weavedx.com"
gateways:
- public-gateway.istio-system.svc.cluster.local
http:
- route:
- destination:
host: gateway
timeout: 30s

40
docs/8-gh-actions.md Normal file
View File

@@ -0,0 +1,40 @@
# GitHub Actions
Create a private repository named `demo-app` on GitHub and navigate to Settings/Secrets and add the following secrets:
* `DOCKER_IMAGE` eg stefanprodan/demo-app
* `DOCKER_USERNAME` eg stefanprodan
* `DOCKER_PASSWORD` eg my-docker-hub-pass
Install podinfo CLI:
```bash
brew install weaveworks/tap/podcli
```
For linux or Windows go to the
[release page](https://github.com/stefanprodan/k8s-podinfo/releases), download the latest podcli release and add it to your path.
Clone your private repository (preferable in your `$GOPATH`) and initialize podinfo.
```bash
git clone https://github.com/stefanprodan/demo-app
cd demo-app
podcli code init demo-app --git-user=stefanprodan --version=v1.3.1
```
The above command does the following:
* downloads podinfo source code v1.3.1 from GitHub
* replaces golang imports with your git username and project name
* creates a Dockerfile and Makefile customized for GitHub actions
* creates the main workflow for GitHub actions
* commits and pushes the code to GitHub
When the code init command finishes, GitHub will test, build and push a Docker image
`${DOCKER_IMAGE}:${GIT-BRANCH}-${GIT-SHORT-SHA}` to your Docker Hub account.
If you create a GitHub release a Docker image with the format `${DOCKER_IMAGE}:${GIT-TAG}` will be published to Docker Hub.
![github-actions-ci](https://github.com/stefanprodan/k8s-podinfo/blob/master/docs/screens/github-actions-ci.png)

Binary file not shown.

Binary file not shown.

View File

@@ -3,9 +3,9 @@ entries:
ambassador:
- apiVersion: v1
appVersion: 0.29.0
created: 2018-08-17T18:44:47.405027259+03:00
created: 2018-10-27T12:02:03.482966+03:00
description: A Helm chart for Datawire Ambassador
digest: a30c8cb38e696b09fda8269ad8465ce6fec6100cfc108ca85ecbc85913ca5c7f
digest: 6b637ed4b5d7190ea52b8ef6e4ac123001fd0e247be1cde5b90b6334c5d768d9
engine: gotpl
maintainers:
- email: stefanprodan@users.noreply.github.com
@@ -19,9 +19,9 @@ entries:
grafana:
- apiVersion: v1
appVersion: "1.0"
created: 2018-08-17T18:44:47.405800273+03:00
created: 2018-10-27T12:02:03.483771+03:00
description: A Helm chart for Kubernetes
digest: abdcadc5cddcb7c015aa5bb64e59bfa246774ad9243b3eb3c2a814abb38f2776
digest: 845d2b2e42b8dc0ae8848ec2ddb5de9a22554fbc1112a8417a8b6f4f818596eb
name: grafana
urls:
- https://stefanprodan.github.io/k8s-podinfo/grafana-0.1.0.tgz
@@ -29,9 +29,9 @@ entries:
loadtest:
- apiVersion: v1
appVersion: "1.0"
created: 2018-08-17T18:44:47.406037468+03:00
created: 2018-10-27T12:02:03.484003+03:00
description: Hey load test Helm chart for Kubernetes
digest: fd3c3cd1eafa9d496250356aa52de1430f3467535fc2972eba5c5c392714eb1f
digest: e36c02267ee82cc16e5c5783543f7e8147646e6637d8fd22265e447e9b043446
name: loadtest
urls:
- https://stefanprodan.github.io/k8s-podinfo/loadtest-0.1.0.tgz
@@ -39,17 +39,122 @@ entries:
ngrok:
- apiVersion: v1
appVersion: "1.0"
created: 2018-08-17T18:44:47.406355266+03:00
created: 2018-10-27T12:02:03.484773+03:00
description: A Ngrok Helm chart for Kubernetes
digest: 7bf5ed2ef63ccd5efb76bcd9a086b04816a162c51d6ab592bccf58c283acd2ea
digest: 3d8316b560c30631cdd1ba4a7ccadb611c119452f5cbbebe70ae356cec9e6b53
name: ngrok
urls:
- https://stefanprodan.github.io/k8s-podinfo/ngrok-0.2.0.tgz
version: 0.2.0
- apiVersion: v1
appVersion: "1.0"
created: 2018-10-27T12:02:03.484477+03:00
description: A Ngrok Helm chart for Kubernetes
digest: 5678de7c8aac246df507f40b3f4f8fd6d06f4447e636398819a232dd26e1fc41
name: ngrok
urls:
- https://stefanprodan.github.io/k8s-podinfo/ngrok-0.1.0.tgz
version: 0.1.0
podinfo:
- apiVersion: v1
appVersion: 1.3.1
created: 2018-10-27T12:02:03.494429+03:00
description: Podinfo Helm chart for Kubernetes
digest: 9116966f2b1300655be11669789842a3d01e326fe1feb205198df3ba22bac3a5
engine: gotpl
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
- email: stefanprodan@users.noreply.github.com
name: stefanprodan
name: podinfo
sources:
- https://github.com/stefanprodan/k8s-podinfo
urls:
- https://stefanprodan.github.io/k8s-podinfo/podinfo-1.3.1.tgz
version: 1.3.1
- apiVersion: v1
appVersion: 1.3.0
created: 2018-10-27T12:02:03.493988+03:00
description: Podinfo Helm chart for Kubernetes
digest: 7111b868a11014ab56da11b7edbea9ce0d1a483500969252f66c9ae1943bac67
engine: gotpl
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
- email: stefanprodan@users.noreply.github.com
name: stefanprodan
name: podinfo
sources:
- https://github.com/stefanprodan/k8s-podinfo
urls:
- https://stefanprodan.github.io/k8s-podinfo/podinfo-1.3.0.tgz
version: 1.3.0
- apiVersion: v1
appVersion: 1.2.1
created: 2018-10-27T12:02:03.493261+03:00
description: Podinfo Helm chart for Kubernetes
digest: c750c1d4a7606a06cd89ae4433431c7e3ecf2ccc9a0e5f6baa26095397bd72da
engine: gotpl
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
- email: stefanprodan@users.noreply.github.com
name: stefanprodan
name: podinfo
sources:
- https://github.com/stefanprodan/k8s-podinfo
urls:
- https://stefanprodan.github.io/k8s-podinfo/podinfo-1.2.1.tgz
version: 1.2.1
- apiVersion: v1
appVersion: 1.2.0
created: 2018-10-27T12:02:03.491318+03:00
description: Podinfo Helm chart for Kubernetes
digest: 24460e15e6da77106eb5212cd683dc2c1d4404b927be6b9f0c89433ba865722e
engine: gotpl
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
- email: stefanprodan@users.noreply.github.com
name: stefanprodan
name: podinfo
sources:
- https://github.com/stefanprodan/k8s-podinfo
urls:
- https://stefanprodan.github.io/k8s-podinfo/podinfo-1.2.0.tgz
version: 1.2.0
- apiVersion: v1
appVersion: 1.1.0
created: 2018-10-27T12:02:03.489961+03:00
description: Podinfo Helm chart for Kubernetes
digest: 973d60c629d7ae476776098ad6b654d78d91f91b3faa8bc016b94c73c42be094
engine: gotpl
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
- email: stefanprodan@users.noreply.github.com
name: stefanprodan
name: podinfo
sources:
- https://github.com/stefanprodan/k8s-podinfo
urls:
- https://stefanprodan.github.io/k8s-podinfo/podinfo-1.1.0.tgz
version: 1.1.0
- apiVersion: v1
appVersion: 1.0.0
created: 2018-10-27T12:02:03.488849+03:00
description: Podinfo Helm chart for Kubernetes
digest: 82068727ba5b552341b14a980e954e27a8517f0ef76aab314c160b0f075e6de4
engine: gotpl
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
- email: stefanprodan@users.noreply.github.com
name: stefanprodan
name: podinfo
sources:
- https://github.com/stefanprodan/k8s-podinfo
urls:
- https://stefanprodan.github.io/k8s-podinfo/podinfo-1.0.0.tgz
version: 1.0.0
- apiVersion: v1
appVersion: 0.6.0
created: 2018-08-17T18:44:47.410049875+03:00
created: 2018-10-27T12:02:03.487979+03:00
description: Podinfo Helm chart for Kubernetes
digest: bd25a710eddb3985d3bd921a11022b5c68a04d37cf93a1a4aab17eeda35aa2f8
engine: gotpl
@@ -65,7 +170,7 @@ entries:
version: 0.2.2
- apiVersion: v1
appVersion: 0.5.1
created: 2018-08-17T18:44:47.409648714+03:00
created: 2018-10-27T12:02:03.487248+03:00
description: Podinfo Helm chart for Kubernetes
digest: 631ca3e2db5553541a50b625f538e6a1f2a103c13aa8148fdd38baf2519e5235
engine: gotpl
@@ -81,7 +186,7 @@ entries:
version: 0.2.1
- apiVersion: v1
appVersion: 0.5.0
created: 2018-08-17T18:44:47.408857715+03:00
created: 2018-10-27T12:02:03.48656+03:00
description: Podinfo Helm chart for Kubernetes
digest: dfe7cf44aef0d170549918b00966422a07e7611f9d0081fb34f5b5beb0641c00
engine: gotpl
@@ -97,7 +202,7 @@ entries:
version: 0.2.0
- apiVersion: v1
appVersion: 0.3.0
created: 2018-08-17T18:44:47.407706617+03:00
created: 2018-10-27T12:02:03.48566+03:00
description: Podinfo Helm chart for Kubernetes
digest: 4865a2d8b269cf453935cda9661c2efb82c16411471f8c11221a6d03d9bb58b1
engine: gotpl
@@ -113,10 +218,58 @@ entries:
version: 0.1.0
podinfo-istio:
- apiVersion: v1
appVersion: 0.6.0
created: 2018-08-17T18:44:47.410653895+03:00
appVersion: 1.2.1
created: 2018-10-27T12:02:03.497708+03:00
description: Podinfo Helm chart for Istio
digest: 79d9cbe4ba8b83ced977d895e56c1223d1fcd88a15f2df1981365c39cf6f4de7
digest: cb0cff1a5d77dadede477f5bf0e93d460de705c3642e93f9a6901f0bc2edec7f
engine: gotpl
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
- email: stefanprodan@users.noreply.github.com
name: stefanprodan
name: podinfo-istio
sources:
- https://github.com/stefanprodan/k8s-podinfo
urls:
- https://stefanprodan.github.io/k8s-podinfo/podinfo-istio-1.2.1.tgz
version: 1.2.1
- apiVersion: v1
appVersion: 1.2.0
created: 2018-10-27T12:02:03.497134+03:00
description: Podinfo Helm chart for Istio
digest: 8115e72f232f82eb3e6da1965364cfede7c069f95a627dddac45cfbe6cb90dc4
engine: gotpl
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
- email: stefanprodan@users.noreply.github.com
name: stefanprodan
name: podinfo-istio
sources:
- https://github.com/stefanprodan/k8s-podinfo
urls:
- https://stefanprodan.github.io/k8s-podinfo/podinfo-istio-1.2.0.tgz
version: 1.2.0
- apiVersion: v1
appVersion: 1.1.0
created: 2018-10-27T12:02:03.496261+03:00
description: Podinfo Helm chart for Istio
digest: bcceb63ff780a8f0ba0b30997040e4e82170f9cce17c26ec817648ed024c83f5
engine: gotpl
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
- email: stefanprodan@users.noreply.github.com
name: stefanprodan
name: podinfo-istio
sources:
- https://github.com/stefanprodan/k8s-podinfo
urls:
- https://stefanprodan.github.io/k8s-podinfo/podinfo-istio-0.2.0.tgz
version: 0.2.0
- apiVersion: v1
appVersion: 0.6.0
created: 2018-10-27T12:02:03.495373+03:00
description: Podinfo Helm chart for Istio
digest: f12f8aa1eca1328e9eaa30bd757f6ed3ff97205e2bf016a47265bc2de6a63d8f
engine: gotpl
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
@@ -128,4 +281,4 @@ entries:
urls:
- https://stefanprodan.github.io/k8s-podinfo/podinfo-istio-0.1.0.tgz
version: 0.1.0
generated: 2018-08-17T18:44:47.404359545+03:00
generated: 2018-10-27T12:02:03.482443+03:00

Binary file not shown.

Binary file not shown.

BIN
docs/ngrok-0.2.0.tgz Normal file

Binary file not shown.

BIN
docs/podinfo-1.0.0.tgz Normal file

Binary file not shown.

BIN
docs/podinfo-1.1.0.tgz Normal file

Binary file not shown.

BIN
docs/podinfo-1.2.0.tgz Normal file

Binary file not shown.

BIN
docs/podinfo-1.2.1.tgz Normal file

Binary file not shown.

BIN
docs/podinfo-1.3.0.tgz Normal file

Binary file not shown.

BIN
docs/podinfo-1.3.1.tgz Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

15
pkg/api/configs.go Normal file
View File

@@ -0,0 +1,15 @@
package api
import "net/http"
func (s *Server) configReadHandler(w http.ResponseWriter, r *http.Request) {
files := make(map[string]string)
if watcher != nil {
watcher.Cache.Range(func(key interface{}, value interface{}) bool {
files[key.(string)] = value.(string)
return true
})
}
s.JSONResponse(w, r, files)
}

23
pkg/api/delay.go Normal file
View File

@@ -0,0 +1,23 @@
package api
import (
"net/http"
"github.com/gorilla/mux"
"strconv"
"time"
)
func (s *Server) delayHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
delay, err := strconv.Atoi(vars["wait"])
if err != nil {
s.ErrorResponse(w, r, err.Error(), http.StatusBadRequest)
return
}
time.Sleep(time.Duration(delay) * time.Second)
s.JSONResponse(w, r, map[string]int{"delay": delay})
}

35
pkg/api/delay_test.go Normal file
View File

@@ -0,0 +1,35 @@
package api
import (
"net/http"
"net/http/httptest"
"regexp"
"testing"
)
func TestDelayHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/delay/0", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
srv := NewMockServer()
srv.router.HandleFunc("/delay/{wait}", srv.delayHandler)
srv.router.ServeHTTP(rr, req)
// Check the status code is what we expect.
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
// Check the response body is what we expect.
expected := ".*delay.*0.*"
r := regexp.MustCompile(expected)
if !r.MatchString(rr.Body.String()) {
t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s",
rr.Body.String(), expected)
}
}

79
pkg/api/echo.go Normal file
View File

@@ -0,0 +1,79 @@
package api
import (
"bytes"
"context"
"io/ioutil"
"net/http"
"github.com/stefanprodan/k8s-podinfo/pkg/version"
"go.uber.org/zap"
)
func (s *Server) echoHandler(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
s.logger.Error("reading the request body failed", zap.Error(err))
s.ErrorResponse(w, r, "invalid request body", http.StatusBadRequest)
return
}
defer r.Body.Close()
if len(s.config.BackendURL) > 0 {
backendReq, err := http.NewRequest("POST", s.config.BackendURL, bytes.NewReader(body))
if err != nil {
s.logger.Error("backend call failed", zap.Error(err), zap.String("url", s.config.BackendURL))
s.ErrorResponse(w, r, "backend call failed", http.StatusInternalServerError)
return
}
// forward headers
copyTracingHeaders(r, backendReq)
backendReq.Header.Set("X-API-Version", version.VERSION)
backendReq.Header.Set("X-API-Revision", version.REVISION)
ctx, cancel := context.WithTimeout(backendReq.Context(), s.config.HttpClientTimeout)
defer cancel()
// call backend
resp, err := http.DefaultClient.Do(backendReq.WithContext(ctx))
if err != nil {
s.logger.Error("backend call failed", zap.Error(err), zap.String("url", s.config.BackendURL))
s.ErrorResponse(w, r, "backend call failed", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
// copy error status from backend and exit
if resp.StatusCode >= 400 {
s.ErrorResponse(w, r, "backend error", resp.StatusCode)
return
}
// forward the received body
rbody, err := ioutil.ReadAll(resp.Body)
if err != nil {
s.logger.Error(
"reading the backend request body failed",
zap.Error(err),
zap.String("url", s.config.BackendURL))
s.ErrorResponse(w, r, "backend call failed", http.StatusInternalServerError)
return
}
s.logger.Debug(
"payload received from backend",
zap.String("response", string(rbody)),
zap.String("url", s.config.BackendURL))
w.Header().Set("X-Color", s.config.UIColor)
w.WriteHeader(http.StatusAccepted)
w.Write(rbody)
} else {
w.Header().Set("X-Color", s.config.UIColor)
w.WriteHeader(http.StatusAccepted)
w.Write(body)
}
}

34
pkg/api/echo_test.go Normal file
View File

@@ -0,0 +1,34 @@
package api
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestEchoHandler(t *testing.T) {
expected := `{"test": true}`
req, err := http.NewRequest("POST", "/api/echo", strings.NewReader(expected))
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
srv := NewMockServer()
handler := http.HandlerFunc(srv.echoHandler)
handler.ServeHTTP(rr, req)
// Check the status code is what we expect.
if status := rr.Code; status != http.StatusAccepted {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusAccepted)
}
// Check the response body is what we expect.
if rr.Body.String() != expected {
t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s",
rr.Body.String(), expected)
}
}

82
pkg/api/echows.go Normal file
View File

@@ -0,0 +1,82 @@
package api
import (
"net/http"
"strings"
"time"
"github.com/gorilla/websocket"
"go.uber.org/zap"
)
var wsCon = websocket.Upgrader{}
// Test: go run ./cmd/podcli/* ws localhost:9898/ws/echo
func (s *Server) echoWsHandler(w http.ResponseWriter, r *http.Request) {
c, err := wsCon.Upgrade(w, r, nil)
if err != nil {
if err != nil {
s.logger.Warn("websocket upgrade error", zap.Error(err))
return
}
}
defer c.Close()
done := make(chan struct{})
defer close(done)
in := make(chan interface{})
defer close(in)
go s.writeWs(c, in)
go s.sendHostWs(c, in, done)
for {
_, message, err := c.ReadMessage()
if err != nil {
if !strings.Contains(err.Error(), "close") {
s.logger.Warn("websocket read error", zap.Error(err))
}
break
}
var response = struct {
Time time.Time `json:"ts"`
Message string `json:"msg"`
}{
Time: time.Now(),
Message: string(message),
}
in <- response
}
}
func (s *Server) sendHostWs(ws *websocket.Conn, in chan interface{}, done chan struct{}) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
var status = struct {
Time time.Time `json:"ts"`
Host string `json:"server"`
}{
Time: time.Now(),
Host: s.config.Hostname,
}
in <- status
case <-done:
s.logger.Debug("websocket exit")
return
}
}
}
func (s *Server) writeWs(ws *websocket.Conn, in chan interface{}) {
for {
select {
case msg := <-in:
if err := ws.WriteJSON(msg); err != nil {
if !strings.Contains(err.Error(), "close") {
s.logger.Warn("websocket write error", zap.Error(err))
}
return
}
}
}
}

11
pkg/api/env.go Normal file
View File

@@ -0,0 +1,11 @@
package api
import (
"net/http"
"os"
)
func (s *Server) envHandler(w http.ResponseWriter, r *http.Request) {
s.JSONResponse(w, r, os.Environ())
}

35
pkg/api/env_test.go Normal file
View File

@@ -0,0 +1,35 @@
package api
import (
"net/http"
"net/http/httptest"
"regexp"
"testing"
)
func TestEnvHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/api/env", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
srv := NewMockServer()
handler := http.HandlerFunc(srv.infoHandler)
handler.ServeHTTP(rr, req)
// Check the status code is what we expect.
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
// Check the response body is what we expect.
expected := ".*hostname.*"
r := regexp.MustCompile(expected)
if !r.MatchString(rr.Body.String()) {
t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s",
rr.Body.String(), expected)
}
}

9
pkg/api/headers.go Normal file
View File

@@ -0,0 +1,9 @@
package api
import (
"net/http"
)
func (s *Server) echoHeadersHandler(w http.ResponseWriter, r *http.Request) {
s.JSONResponse(w, r, r.Header)
}

36
pkg/api/headers_test.go Normal file
View File

@@ -0,0 +1,36 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"testing"
)
func TestEchoHeadersHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/api/headers", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set("X-Test", "testing")
rr := httptest.NewRecorder()
srv := NewMockServer()
handler := http.HandlerFunc(srv.echoHeadersHandler)
handler.ServeHTTP(rr, req)
// Check the status code is what we expect.
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
// Check the response body is what we expect.
expected := "testing"
r := regexp.MustCompile(fmt.Sprintf("(?m:%s)", expected))
if !r.MatchString(rr.Body.String()) {
t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s",
rr.Body.String(), expected)
}
}

32
pkg/api/health.go Normal file
View File

@@ -0,0 +1,32 @@
package api
import (
"net/http"
"sync/atomic"
)
func (s *Server) healthzHandler(w http.ResponseWriter, r *http.Request) {
if atomic.LoadInt32(&healthy) == 1 {
s.JSONResponse(w, r, map[string]string{"status": "OK"})
return
}
w.WriteHeader(http.StatusServiceUnavailable)
}
func (s *Server) readyzHandler(w http.ResponseWriter, r *http.Request) {
if atomic.LoadInt32(&ready) == 1 {
s.JSONResponse(w, r, map[string]string{"status": "OK"})
return
}
w.WriteHeader(http.StatusServiceUnavailable)
}
func (s *Server) enableReadyHandler(w http.ResponseWriter, r *http.Request) {
atomic.StoreInt32(&ready, 1)
w.WriteHeader(http.StatusAccepted)
}
func (s *Server) disableReadyHandler(w http.ResponseWriter, r *http.Request) {
atomic.StoreInt32(&ready, 0)
w.WriteHeader(http.StatusAccepted)
}

45
pkg/api/health_test.go Normal file
View File

@@ -0,0 +1,45 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestHealthzHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/healthz", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
srv := NewMockServer()
handler := http.HandlerFunc(srv.healthzHandler)
handler.ServeHTTP(rr, req)
// Check the status code is what we expect.
if status := rr.Code; status != http.StatusServiceUnavailable {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusServiceUnavailable)
}
}
func TestReadyzHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/readyz", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
srv := NewMockServer()
handler := http.HandlerFunc(srv.readyzHandler)
handler.ServeHTTP(rr, req)
// Check the status code is what we expect.
if status := rr.Code; status != http.StatusServiceUnavailable {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusServiceUnavailable)
}
}

122
pkg/api/http.go Normal file
View File

@@ -0,0 +1,122 @@
package api
import (
"bytes"
"encoding/json"
"math/rand"
"net/http"
"time"
"github.com/stefanprodan/k8s-podinfo/pkg/version"
"go.uber.org/zap"
)
func randomDelayMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
min := 0
max := 5
rand.Seed(time.Now().Unix())
delay := rand.Intn(max-min) + min
time.Sleep(time.Duration(delay) * time.Second)
next.ServeHTTP(w, r)
})
}
func randomErrorMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rand.Seed(time.Now().Unix())
if rand.Int31n(3) == 0 {
errors := []int{http.StatusInternalServerError, http.StatusBadRequest, http.StatusConflict}
w.WriteHeader(errors[rand.Intn(len(errors))])
return
}
next.ServeHTTP(w, r)
})
}
func versionMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Header.Set("X-API-Version", version.VERSION)
r.Header.Set("X-API-Revision", version.REVISION)
next.ServeHTTP(w, r)
})
}
// TODO: use Istio tracing package
// https://github.com/istio/istio/blob/master/pkg/tracing/config.go
func copyTracingHeaders(from *http.Request, to *http.Request) {
headers := []string{
"x-request-id",
"x-b3-traceid",
"x-b3-spanid",
"x-b3-parentspanid",
"x-b3-sampled",
"x-b3-flags",
"x-ot-span-context",
}
for i := range headers {
headerValue := from.Header.Get(headers[i])
if len(headerValue) > 0 {
to.Header.Set(headers[i], headerValue)
}
}
}
func (s *Server) JSONResponse(w http.ResponseWriter, r *http.Request, result interface{}) {
body, err := json.Marshal(result)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
s.logger.Error("JSON marshal failed", zap.Error(err))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
w.Write(prettyJSON(body))
}
func (s *Server) JSONResponseCode(w http.ResponseWriter, r *http.Request, result interface{}, responseCode int) {
body, err := json.Marshal(result)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
s.logger.Error("JSON marshal failed", zap.Error(err))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(responseCode)
w.Write(prettyJSON(body))
}
func (s *Server) ErrorResponse(w http.ResponseWriter, r *http.Request, error string, code int) {
data := struct {
Code int `json:"code"`
Message string `json:"message"`
}{
Code: code,
Message: error,
}
body, err := json.Marshal(data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
s.logger.Error("JSON marshal failed", zap.Error(err))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
w.Write(prettyJSON(body))
}
func prettyJSON(b []byte) []byte {
var out bytes.Buffer
json.Indent(&out, b, "", " ")
return out.Bytes()
}

26
pkg/api/index.go Normal file
View File

@@ -0,0 +1,26 @@
package api
import (
"html/template"
"net/http"
"path"
)
func (s *Server) indexHandler(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.New("vue.html").ParseFiles(path.Join(s.config.UIPath, "vue.html"))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(path.Join(s.config.UIPath, "vue.html") + err.Error()))
return
}
data := struct {
Title string
}{
Title: s.config.Hostname,
}
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, path.Join(s.config.UIPath, "vue.html")+err.Error(), http.StatusInternalServerError)
}
}

38
pkg/api/info.go Normal file
View File

@@ -0,0 +1,38 @@
package api
import (
"net/http"
"runtime"
"strconv"
"github.com/stefanprodan/k8s-podinfo/pkg/version"
)
func (s *Server) infoHandler(w http.ResponseWriter, r *http.Request) {
data := struct {
Hostname string `json:"hostname"`
Version string `json:"version"`
Revision string `json:"revision"`
Color string `json:"color"`
Message string `json:"message"`
GOOS string `json:"goos"`
GOARCH string `json:"goarch"`
Runtime string `json:"runtime"`
NumGoroutine string `json:"num_goroutine"`
NumCPU string `json:"num_cpu"`
}{
Hostname: s.config.Hostname,
Version: version.VERSION,
Revision: version.REVISION,
Color: s.config.UIColor,
Message: s.config.UIMessage,
GOOS: runtime.GOOS,
GOARCH: runtime.GOARCH,
Runtime: runtime.Version(),
NumGoroutine: strconv.FormatInt(int64(runtime.NumGoroutine()), 10),
NumCPU: strconv.FormatInt(int64(runtime.NumCPU()), 10),
}
s.JSONResponse(w, r, data)
}

35
pkg/api/info_test.go Normal file
View File

@@ -0,0 +1,35 @@
package api
import (
"net/http"
"net/http/httptest"
"regexp"
"testing"
)
func TestInfoHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/api/info", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
srv := NewMockServer()
handler := http.HandlerFunc(srv.infoHandler)
handler.ServeHTTP(rr, req)
// Check the status code is what we expect.
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
// Check the response body is what we expect.
expected := ".*color.*blue.*"
r := regexp.MustCompile(expected)
if !r.MatchString(rr.Body.String()) {
t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s",
rr.Body.String(), expected)
}
}

31
pkg/api/logging.go Normal file
View File

@@ -0,0 +1,31 @@
package api
import (
"net/http"
"go.uber.org/zap"
)
type LoggingMiddleware struct {
logger *zap.Logger
}
func NewLoggingMiddleware(logger *zap.Logger) *LoggingMiddleware {
return &LoggingMiddleware{
logger: logger,
}
}
func (m *LoggingMiddleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
m.logger.Debug(
"request started",
zap.String("proto", r.Proto),
zap.String("uri", r.RequestURI),
zap.String("method", r.Method),
zap.String("remote", r.RemoteAddr),
zap.String("user-agent", r.UserAgent()),
)
next.ServeHTTP(w, r)
})
}

View File

@@ -1,4 +1,4 @@
package server
package api
import (
"bufio"
@@ -10,15 +10,16 @@ import (
"strings"
"time"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus"
)
type Instrument struct {
type PrometheusMiddleware struct {
Histogram *prometheus.HistogramVec
Counter *prometheus.CounterVec
}
func NewInstrument() *Instrument {
func NewPrometheusMiddleware() *PrometheusMiddleware {
// used for monitoring and alerting (RED method)
histogram := prometheus.NewHistogramVec(prometheus.HistogramOpts{
Subsystem: "http",
@@ -39,27 +40,42 @@ func NewInstrument() *Instrument {
prometheus.MustRegister(histogram)
prometheus.MustRegister(counter)
return &Instrument{
return &PrometheusMiddleware{
Histogram: histogram,
Counter: counter,
}
}
func (i Instrument) Wrap(next http.Handler) http.Handler {
func (p *PrometheusMiddleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
begin := time.Now()
interceptor := &interceptor{ResponseWriter: w, statusCode: http.StatusOK}
path := urlToLabel(r.URL.Path)
path := p.getRouteName(r)
next.ServeHTTP(interceptor, r)
var (
status = strconv.Itoa(interceptor.statusCode)
took = time.Since(begin)
)
i.Histogram.WithLabelValues(r.Method, path, status).Observe(took.Seconds())
i.Counter.WithLabelValues(status).Inc()
p.Histogram.WithLabelValues(r.Method, path, status).Observe(took.Seconds())
p.Counter.WithLabelValues(status).Inc()
})
}
// converts gorilla mux routes from '/api/delay/{wait}' to 'api_delay_wait'
func (p *PrometheusMiddleware) getRouteName(r *http.Request) string {
if mux.CurrentRoute(r) != nil {
if name := mux.CurrentRoute(r).GetName(); len(name) > 0 {
return urlToLabel(name)
}
if path, err := mux.CurrentRoute(r).GetPathTemplate(); err == nil {
if len(path) > 0 {
return urlToLabel(path)
}
}
}
return urlToLabel(r.RequestURI)
}
var invalidChars = regexp.MustCompile(`[^a-zA-Z0-9]+`)
// converts a URL path to a string compatible with Prometheus label value.

32
pkg/api/mock.go Normal file
View File

@@ -0,0 +1,32 @@
package api
import (
"time"
"github.com/gorilla/mux"
"go.uber.org/zap"
)
func NewMockServer() *Server {
config := &Config{
Port: "9898",
HttpServerShutdownTimeout: 5 * time.Second,
HttpServerTimeout: 30 * time.Second,
BackendURL: "",
ConfigPath: "/config",
DataPath: "/data",
HttpClientTimeout: 30 * time.Second,
UIColor: "blue",
UIPath: ".ui",
UIMessage: "Greetings",
Hostname: "localhost",
}
logger, _ := zap.NewDevelopment()
return &Server{
router: mux.NewRouter(),
logger: logger,
config: config,
}
}

9
pkg/api/panic.go Normal file
View File

@@ -0,0 +1,9 @@
package api
import (
"net/http"
)
func (s *Server) panicHandler(w http.ResponseWriter, r *http.Request) {
s.logger.Panic("Panic command received")
}

187
pkg/api/server.go Normal file
View File

@@ -0,0 +1,187 @@
package api
import (
"context"
"fmt"
"net/http"
_ "net/http/pprof"
"os"
"strings"
"sync/atomic"
"time"
"github.com/spf13/viper"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/stefanprodan/k8s-podinfo/pkg/fscache"
"go.uber.org/zap"
)
var (
healthy int32
ready int32
watcher *fscache.Watcher
)
type Config struct {
HttpClientTimeout time.Duration `mapstructure:"http-client-timeout"`
HttpServerTimeout time.Duration `mapstructure:"http-server-timeout"`
HttpServerShutdownTimeout time.Duration `mapstructure:"http-server-shutdown-timeout"`
BackendURL string `mapstructure:"backend-url"`
UIMessage string `mapstructure:"ui-message"`
UIColor string `mapstructure:"ui-color"`
UIPath string `mapstructure:"ui-path"`
DataPath string `mapstructure:"data-path"`
ConfigPath string `mapstructure:"config-path"`
Port string `mapstructure:"port"`
Hostname string `mapstructure:"hostname"`
RandomDelay bool `mapstructure:"random-delay"`
RandomError bool `mapstructure:"random-error"`
JWTSecret string `mapstructure:"jwt-secret"`
}
type Server struct {
router *mux.Router
logger *zap.Logger
config *Config
}
func NewServer(config *Config, logger *zap.Logger) (*Server, error) {
srv := &Server{
router: mux.NewRouter(),
logger: logger,
config: config,
}
return srv, nil
}
func (s *Server) registerHandlers() {
s.router.Handle("/metrics", promhttp.Handler())
s.router.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux)
s.router.HandleFunc("/", s.indexHandler).HeadersRegexp("User-Agent", "^Mozilla.*").Methods("GET")
s.router.HandleFunc("/", s.infoHandler).Methods("GET")
s.router.HandleFunc("/version", s.versionHandler).Methods("GET")
s.router.HandleFunc("/echo", s.echoHandler).Methods("POST")
s.router.HandleFunc("/env", s.envHandler).Methods("GET", "POST")
s.router.HandleFunc("/headers", s.echoHeadersHandler).Methods("GET", "POST")
s.router.HandleFunc("/delay/{wait:[0-9]+}", s.delayHandler).Methods("GET").Name("delay")
s.router.HandleFunc("/healthz", s.healthzHandler).Methods("GET")
s.router.HandleFunc("/readyz", s.readyzHandler).Methods("GET")
s.router.HandleFunc("/readyz/enable", s.enableReadyHandler).Methods("POST")
s.router.HandleFunc("/readyz/disable", s.disableReadyHandler).Methods("POST")
s.router.HandleFunc("/panic", s.panicHandler).Methods("GET")
s.router.HandleFunc("/status/{code:[0-9]+}", s.statusHandler).Methods("GET", "POST", "PUT").Name("status")
s.router.HandleFunc("/store", s.storeWriteHandler).Methods("POST")
s.router.HandleFunc("/store/{hash}", s.storeReadHandler).Methods("GET").Name("store")
s.router.HandleFunc("/configs", s.configReadHandler).Methods("GET")
s.router.HandleFunc("/token", s.tokenGenerateHandler).Methods("POST")
s.router.HandleFunc("/token/validate", s.tokenValidateHandler).Methods("GET")
s.router.HandleFunc("/api/info", s.infoHandler).Methods("GET")
s.router.HandleFunc("/api/echo", s.echoHandler).Methods("POST")
s.router.HandleFunc("/ws/echo", s.echoWsHandler)
}
func (s *Server) registerMiddlewares() {
prom := NewPrometheusMiddleware()
s.router.Use(prom.Handler)
httpLogger := NewLoggingMiddleware(s.logger)
s.router.Use(httpLogger.Handler)
s.router.Use(versionMiddleware)
if s.config.RandomDelay {
s.router.Use(randomDelayMiddleware)
}
if s.config.RandomError {
s.router.Use(randomErrorMiddleware)
}
}
func (s *Server) ListenAndServe(stopCh <-chan struct{}) {
s.registerHandlers()
s.registerMiddlewares()
srv := &http.Server{
Addr: ":" + s.config.Port,
WriteTimeout: s.config.HttpServerTimeout,
ReadTimeout: s.config.HttpServerTimeout,
IdleTimeout: 2 * s.config.HttpServerTimeout,
Handler: s.router,
}
//s.printRoutes()
// load configs in memory and start watching for changes in the config dir
if stat, err := os.Stat(s.config.ConfigPath); err == nil && stat.IsDir() {
var err error
watcher, err = fscache.NewWatch(s.config.ConfigPath)
if err != nil {
s.logger.Error("config watch error", zap.Error(err), zap.String("path", s.config.ConfigPath))
} else {
watcher.Watch()
}
}
// run server in background
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
s.logger.Fatal("HTTP server crashed", zap.Error(err))
}
}()
// signal Kubernetes the server is ready to receive traffic
atomic.StoreInt32(&healthy, 1)
atomic.StoreInt32(&ready, 1)
// wait for SIGTERM or SIGINT
<-stopCh
ctx, cancel := context.WithTimeout(context.Background(), s.config.HttpServerShutdownTimeout)
defer cancel()
// all calls to /healthz and /readyz will fail from now on
atomic.StoreInt32(&healthy, 0)
atomic.StoreInt32(&ready, 0)
s.logger.Info("Shutting down HTTP server", zap.Duration("timeout", s.config.HttpServerShutdownTimeout))
// wait for Kubernetes readiness probe to remove this instance from the load balancer
// the readiness check interval must be lower than the timeout
if viper.GetString("level") != "debug" {
time.Sleep(3 * time.Second)
}
// attempt graceful shutdown
if err := srv.Shutdown(ctx); err != nil {
s.logger.Warn("HTTP server graceful shutdown failed", zap.Error(err))
} else {
s.logger.Info("HTTP server stopped")
}
}
func (s *Server) printRoutes() {
s.router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
pathTemplate, err := route.GetPathTemplate()
if err == nil {
fmt.Println("ROUTE:", pathTemplate)
}
pathRegexp, err := route.GetPathRegexp()
if err == nil {
fmt.Println("Path regexp:", pathRegexp)
}
queriesTemplates, err := route.GetQueriesTemplates()
if err == nil {
fmt.Println("Queries templates:", strings.Join(queriesTemplates, ","))
}
queriesRegexps, err := route.GetQueriesRegexp()
if err == nil {
fmt.Println("Queries regexps:", strings.Join(queriesRegexps, ","))
}
methods, err := route.GetMethods()
if err == nil {
fmt.Println("Methods:", strings.Join(methods, ","))
}
fmt.Println()
return nil
})
}

20
pkg/api/status.go Normal file
View File

@@ -0,0 +1,20 @@
package api
import (
"net/http"
"github.com/gorilla/mux"
"strconv"
)
func (s *Server) statusHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
code, err := strconv.Atoi(vars["code"])
if err != nil {
s.ErrorResponse(w, r, err.Error(), http.StatusBadRequest)
return
}
s.JSONResponseCode(w, r, map[string]int{"status": code}, code)
}

26
pkg/api/status_test.go Normal file
View File

@@ -0,0 +1,26 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestStatusHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/status/404", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
srv := NewMockServer()
srv.router.HandleFunc("/status/{code}", srv.statusHandler)
srv.router.ServeHTTP(rr, req)
// Check the status code is what we expect.
if status := rr.Code; status != http.StatusNotFound {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusNotFound)
}
}

48
pkg/api/store.go Normal file
View File

@@ -0,0 +1,48 @@
package api
import (
"crypto/sha1"
"encoding/hex"
"io/ioutil"
"net/http"
"path"
"github.com/gorilla/mux"
"go.uber.org/zap"
)
func (s *Server) storeWriteHandler(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
s.ErrorResponse(w, r, "reading the request body failed", http.StatusBadRequest)
return
}
hash := hash(string(body))
err = ioutil.WriteFile(path.Join(s.config.DataPath, hash), body, 0644)
if err != nil {
s.logger.Warn("writing file failed", zap.Error(err), zap.String("file", path.Join(s.config.DataPath, hash)))
s.ErrorResponse(w, r, "writing file failed", http.StatusInternalServerError)
return
}
s.JSONResponseCode(w, r, map[string]string{"hash": hash}, http.StatusAccepted)
}
func (s *Server) storeReadHandler(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["hash"]
content, err := ioutil.ReadFile(path.Join(s.config.DataPath, hash))
if err != nil {
s.logger.Warn("reading file failed", zap.Error(err), zap.String("file", path.Join(s.config.DataPath, hash)))
s.ErrorResponse(w, r, "reading file failed", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusAccepted)
w.Write([]byte(content))
}
func hash(input string) string {
h := sha1.New()
h.Write([]byte(input))
return hex.EncodeToString(h.Sum(nil))
}

102
pkg/api/token.go Normal file
View File

@@ -0,0 +1,102 @@
package api
import (
"fmt"
"net/http"
"strings"
"time"
"io/ioutil"
"github.com/dgrijalva/jwt-go"
"go.uber.org/zap"
)
type jwtCustomClaims struct {
Name string `json:"name"`
jwt.StandardClaims
}
func (s *Server) tokenGenerateHandler(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
s.logger.Error("reading the request body failed", zap.Error(err))
s.ErrorResponse(w, r, "invalid request body", http.StatusBadRequest)
return
}
defer r.Body.Close()
user := "anonymous"
if len(body) > 0 {
user = string(body)
}
claims := &jwtCustomClaims{
user,
jwt.StandardClaims{
Issuer: "podinfo",
ExpiresAt: time.Now().Add(time.Minute * 1).Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
t, err := token.SignedString([]byte(s.config.JWTSecret))
if err != nil {
s.ErrorResponse(w, r, err.Error(), http.StatusBadRequest)
return
}
var result = struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
}{
Token: t,
ExpiresAt: time.Unix(claims.StandardClaims.ExpiresAt, 0),
}
s.JSONResponse(w, r, result)
}
// Get: JWT=$(curl -s -d 'test' localhost:9898/token | jq -r .token)
// Post: curl -H "Authorization: Bearer ${JWT}" localhost:9898/token/validate
func (s *Server) tokenValidateHandler(w http.ResponseWriter, r *http.Request) {
authorizationHeader := r.Header.Get("authorization")
if authorizationHeader == "" {
s.ErrorResponse(w, r, "authorization bearer header required", http.StatusUnauthorized)
return
}
bearerToken := strings.Split(authorizationHeader, " ")
if len(bearerToken) != 2 || strings.ToLower(bearerToken[0]) != "bearer" {
s.ErrorResponse(w, r, "authorization bearer header required", http.StatusUnauthorized)
return
}
claims := jwtCustomClaims{}
token, err := jwt.ParseWithClaims(bearerToken[1], &claims, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("invalid signing method")
}
return []byte(s.config.JWTSecret), nil
})
if err != nil {
s.ErrorResponse(w, r, err.Error(), http.StatusUnauthorized)
return
}
if token.Valid {
if claims.StandardClaims.Issuer != "podinfo" {
s.ErrorResponse(w, r, "invalid issuer", http.StatusUnauthorized)
} else {
var result = struct {
TokenName string `json:"token_name"`
ExpiresAt time.Time `json:"expires_at"`
}{
TokenName: claims.Name,
ExpiresAt: time.Unix(claims.StandardClaims.ExpiresAt, 0),
}
s.JSONResponse(w, r, result)
}
} else {
s.ErrorResponse(w, r, "Invalid authorization token", http.StatusUnauthorized)
}
}

15
pkg/api/version.go Normal file
View File

@@ -0,0 +1,15 @@
package api
import (
"net/http"
"github.com/stefanprodan/k8s-podinfo/pkg/version"
)
func (s *Server) versionHandler(w http.ResponseWriter, r *http.Request) {
result := map[string]string{
"version": version.VERSION,
"commit": version.REVISION,
}
s.JSONResponse(w, r, result)
}

36
pkg/api/version_test.go Normal file
View File

@@ -0,0 +1,36 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"testing"
)
func TestVersionHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/version", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
srv := NewMockServer()
handler := http.HandlerFunc(srv.versionHandler)
handler.ServeHTTP(rr, req)
// Check the status code is what we expect.
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
// Check the response body is what we expect.
expected := "unknown"
r := regexp.MustCompile(fmt.Sprintf("(?m:%s)", expected))
if !r.MatchString(rr.Body.String()) {
t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s",
rr.Body.String(), expected)
}
}

View File

@@ -1,178 +0,0 @@
package server
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"time"
"github.com/rs/zerolog/log"
"github.com/stefanprodan/k8s-podinfo/pkg/version"
)
func (s *Server) apiInfo(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/info" && r.Method != http.MethodGet {
w.WriteHeader(http.StatusNotFound)
return
}
host, _ := os.Hostname()
color := os.Getenv("color")
if len(color) < 1 {
color = "blue"
}
msg := os.Getenv("message")
if len(msg) < 1 {
msg = fmt.Sprintf("Greetings from podinfo v%v", version.VERSION)
}
data := struct {
Message string `json:"message"`
Version string `json:"version"`
Revision string `json:"revision"`
Hostname string `json:"hostname"`
Color string `json:"color"`
}{
Message: msg,
Version: version.VERSION,
Revision: version.GITCOMMIT,
Hostname: host,
Color: color,
}
d, err := json.Marshal(data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
w.Write(d)
}
func (s *Server) apiEcho(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/echo" && r.Method != http.MethodPost {
w.WriteHeader(http.StatusNotFound)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Error().Msgf("Reading the request body failed: %v", err)
jsonError(w, "invalid request body", http.StatusBadRequest)
return
}
backendURL := os.Getenv("backendURL")
if len(backendURL) > 0 {
backendReq, err := http.NewRequest("POST", backendURL, bytes.NewReader(body))
if err != nil {
log.Error().Err(err).Msgf("%v backend call failed", r.URL.Path)
jsonError(w, "backend call failed", http.StatusInternalServerError)
return
}
// forward headers
copyTracingHeaders(r, backendReq)
setVersionHeaders(backendReq)
// TODO: make the timeout configurable
ctx, cancel := context.WithTimeout(backendReq.Context(), 2*time.Minute)
defer cancel()
// call backend
resp, err := http.DefaultClient.Do(backendReq.WithContext(ctx))
if err != nil {
log.Error().Err(err).Msgf("backend call to %s failed", backendURL)
jsonError(w, "backend call failed", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
// copy error status from backend and exit
if resp.StatusCode >= 400 {
w.WriteHeader(resp.StatusCode)
return
}
// forward the received body
rbody, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error().Err(err).Msgf("%v reading the backend request body failed", r.URL.Path)
jsonError(w, "backend call failed", http.StatusInternalServerError)
return
}
// set logLevel=info when load testing
log.Debug().Msgf("Payload received %v from backend: %s", r.URL.Path, string(rbody))
setResponseHeaders(w)
w.Write(rbody)
} else {
setResponseHeaders(w)
w.Write(body)
}
}
func copyTracingHeaders(from *http.Request, to *http.Request) {
headers := []string{
"x-request-id",
"x-b3-traceid",
"x-b3-spanid",
"x-b3-parentspanid",
"x-b3-sampled",
"x-b3-flags",
"x-ot-span-context",
}
for i := range headers {
headerValue := from.Header.Get(headers[i])
if len(headerValue) > 0 {
to.Header.Set(headers[i], headerValue)
}
}
}
func setVersionHeaders(r *http.Request) {
r.Header.Set("X-API-Version", version.VERSION)
r.Header.Set("X-API-Revision", version.GITCOMMIT)
}
func setResponseHeaders(w http.ResponseWriter) {
color := os.Getenv("color")
if len(color) < 1 {
color = "blue"
}
w.Header().Set("X-Color", color)
w.WriteHeader(http.StatusAccepted)
}
func jsonError(w http.ResponseWriter, error string, code int) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(code)
data := struct {
Code int `json:"code"`
Message string `json:"message"`
}{
Code: code,
Message: error,
}
body, err := json.Marshal(data)
if err != nil {
log.Debug().Err(err).Msg("jsonError marshal failed")
} else {
w.Write(body)
}
}

View File

@@ -1,346 +0,0 @@
package server
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"html/template"
"io/ioutil"
"net/http"
"os"
"path"
"strings"
"sync/atomic"
"time"
"github.com/rs/zerolog/log"
"github.com/stefanprodan/k8s-podinfo/pkg/version"
"gopkg.in/yaml.v2"
)
func (s *Server) index(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
w.WriteHeader(http.StatusNotFound)
return
}
log.Debug().Msgf("Request %s received from %s on %s", r.Header.Get("x-request-id"), r.RemoteAddr, r.RequestURI)
if strings.Contains(r.UserAgent(), "Mozilla") {
uiPath := os.Getenv("uiPath")
if len(uiPath) < 1 {
uiPath = "ui"
}
tmpl, err := template.New("vue.html").ParseFiles(path.Join(uiPath, "vue.html"))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(path.Join(uiPath, "vue.html") + err.Error()))
return
}
host, _ := os.Hostname()
data := struct {
Title string
}{
Title: host,
}
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, path.Join(uiPath, "vue.html")+err.Error(), http.StatusInternalServerError)
}
} else {
resp, err := makeResponse()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
d, err := yaml.Marshal(resp)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
w.Write(d)
}
}
func (s *Server) echo(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "POST":
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Error().Msgf("Reading the request body failed: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
hash := hash(string(body))
log.Debug().Msgf("Payload received from %s hash %s", r.RemoteAddr, hash)
w.WriteHeader(http.StatusAccepted)
w.Write(body)
default:
w.WriteHeader(http.StatusNotAcceptable)
}
}
func (s *Server) echoHeaders(w http.ResponseWriter, r *http.Request) {
d, err := yaml.Marshal(r.Header)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
w.Write(d)
}
func (s *Server) backend(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "POST":
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Error().Msgf("Reading the request body failed: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
backendURL := os.Getenv("backendURL")
if len(backendURL) > 0 {
backendReq, err := http.NewRequest("POST", backendURL, bytes.NewReader(body))
if err != nil {
log.Error().Msgf("Backend call failed: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
// forward tracing headers
copyTracingHeaders(r, backendReq)
setVersionHeaders(backendReq)
resp, err := http.DefaultClient.Do(backendReq)
if err != nil {
log.Error().Msgf("Backend call failed: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
w.WriteHeader(resp.StatusCode)
return
}
rbody, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error().Msgf("Reading the backend request body failed: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
log.Debug().Msgf("Payload received from backend: %s", string(rbody))
setResponseHeaders(w)
w.Write(rbody)
} else {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Backend not specified, set backendURL env var"))
}
default:
w.WriteHeader(http.StatusNotAcceptable)
}
}
func (s *Server) job(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "POST":
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Error().Msgf("Reading the request body failed: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
log.Debug().Msgf("Payload received from %s: %s", r.RemoteAddr, string(body))
job := struct {
Wait int `json:"wait"`
}{
Wait: 0,
}
err = json.Unmarshal(body, &job)
if err != nil {
log.Error().Msgf("Reading the request body failed: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
if job.Wait > 0 {
time.Sleep(time.Duration(job.Wait) * time.Second)
}
w.WriteHeader(http.StatusAccepted)
w.Write([]byte("Job done"))
default:
w.WriteHeader(http.StatusNotAcceptable)
}
}
func (s *Server) write(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "POST":
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Error().Msgf("Reading the request body failed: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
hash := hash(string(body))
err = ioutil.WriteFile(path.Join(dataPath, hash), body, 0644)
if err != nil {
log.Error().Msgf("Writing file to /data failed: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
log.Debug().Msgf("Write command received from %s hash %s", r.RemoteAddr, hash)
w.WriteHeader(http.StatusAccepted)
w.Write([]byte(hash))
default:
w.WriteHeader(http.StatusNotAcceptable)
}
}
func (s *Server) read(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "POST":
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Error().Msgf("Reading the request body failed: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
hash := string(body)
content, err := ioutil.ReadFile(path.Join(dataPath, hash))
if err != nil {
log.Error().Msgf("Reading file from /data/%s failed: %v", hash, err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
log.Debug().Msgf("Read command received from %s hash %s", r.RemoteAddr, hash)
w.WriteHeader(http.StatusAccepted)
w.Write([]byte(content))
default:
w.WriteHeader(http.StatusNotAcceptable)
}
}
func (s *Server) configs(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
files := make(map[string]string)
if watcher != nil {
watcher.Cache.Range(func(key interface{}, value interface{}) bool {
files[key.(string)] = value.(string)
return true
})
}
d, err := yaml.Marshal(files)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
w.Write(d)
default:
w.WriteHeader(http.StatusNotAcceptable)
}
}
func (s *Server) version(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/version" {
w.WriteHeader(http.StatusNotFound)
return
}
resp := map[string]string{
"version": version.VERSION,
"commit": version.GITCOMMIT,
}
d, err := yaml.Marshal(resp)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
w.Write(d)
}
func (s *Server) healthz(w http.ResponseWriter, r *http.Request) {
if atomic.LoadInt32(&healthy) == 1 {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
return
}
w.WriteHeader(http.StatusServiceUnavailable)
}
func (s *Server) readyz(w http.ResponseWriter, r *http.Request) {
if atomic.LoadInt32(&ready) == 1 {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
return
}
w.WriteHeader(http.StatusServiceUnavailable)
}
func (s *Server) enable(w http.ResponseWriter, r *http.Request) {
atomic.StoreInt32(&ready, 1)
}
func (s *Server) disable(w http.ResponseWriter, r *http.Request) {
atomic.StoreInt32(&ready, 0)
}
func (s *Server) error(w http.ResponseWriter, r *http.Request) {
log.Error().Msg("Error triggered")
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
return
}
func (s *Server) panic(w http.ResponseWriter, r *http.Request) {
log.Fatal().Msg("Kill switch triggered")
}
func hash(input string) string {
h := sha256.New()
h.Write([]byte(input))
return hex.EncodeToString(h.Sum(nil))
}

View File

@@ -1,91 +0,0 @@
package server
import (
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"
)
func TestYamlResponse(t *testing.T) {
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
srv := &Server{}
handler := http.HandlerFunc(srv.index)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Fatalf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
expected := "external_ip"
r := regexp.MustCompile(fmt.Sprintf("(?m:%s)", expected))
if !r.MatchString(rr.Body.String()) {
t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s",
rr.Body.String(), expected)
}
}
func TestHealthzNotReady(t *testing.T) {
req, err := http.NewRequest("GET", "/healthz", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
srv := &Server{}
handler := http.HandlerFunc(srv.healthz)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusServiceUnavailable {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusServiceUnavailable)
}
}
func TestReadyzNotReady(t *testing.T) {
req, err := http.NewRequest("GET", "/readyz", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
srv := &Server{}
handler := http.HandlerFunc(srv.readyz)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusServiceUnavailable {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusServiceUnavailable)
}
}
func TestEchoResponse(t *testing.T) {
expected := "test"
req, err := http.NewRequest("POST", "/echo", strings.NewReader(expected))
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
srv := &Server{}
handler := http.HandlerFunc(srv.echo)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusAccepted {
t.Fatalf("handler returned wrong status code: got %v want %v",
status, http.StatusAccepted)
}
if rr.Body.String() != expected {
t.Fatalf("handler returned unexpected body:\ngot \n%v \nwant \n%s",
rr.Body.String(), expected)
}
}

View File

@@ -1,143 +0,0 @@
package server
import (
"bufio"
"crypto/tls"
"encoding/json"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
type Response struct {
Runtime map[string]string `json:"runtime" yaml:"runtime"`
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
Environment map[string]string `json:"environment" yaml:"environment"`
}
func makeResponse() (*Response, error) {
labels, err := filesToMap("/etc/podinfod/metadata/labels")
if err != nil {
return nil, err
}
annotations, err := filesToMap("/etc/podinfod/metadata/annotations")
if err != nil {
return nil, err
}
resp := &Response{
Environment: envToMap(),
Runtime: runtimeToMap(),
Labels: labels,
Annotations: annotations,
}
return resp, nil
}
func filesToMap(dir string) (map[string]string, error) {
list := make(map[string]string, 0)
if _, err := os.Stat(dir); err != nil {
// path not found
return list, nil
}
files := make([]string, 0)
err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
files = append(files, path)
return nil
})
if err != nil {
return nil, errors.Wrapf(err, "Reading from %v failed", dir)
}
for _, path := range files {
file, err := os.Open(path)
if err != nil {
continue
}
s := bufio.NewScanner(file)
for s.Scan() {
kv := strings.Split(s.Text(), "=")
if len(kv) > 1 {
list[kv[0]] = strings.Replace(kv[1], "\"", "", -1)
} else {
list[kv[0]] = ""
}
}
file.Close()
}
return list, nil
}
func envToMap() map[string]string {
list := make(map[string]string, 0)
for _, env := range os.Environ() {
kv := strings.Split(env, "=")
if len(kv) > 1 {
list[kv[0]] = strings.Replace(kv[1], "\"", "", -1)
} else {
list[kv[0]] = ""
}
}
return list
}
func runtimeToMap() map[string]string {
info := map[string]string{
"os": runtime.GOOS,
"arch": runtime.GOARCH,
"version": runtime.Version(),
"max_procs": strconv.FormatInt(int64(runtime.GOMAXPROCS(0)), 10),
"num_goroutine": strconv.FormatInt(int64(runtime.NumGoroutine()), 10),
"num_cpu": strconv.FormatInt(int64(runtime.NumCPU()), 10),
"external_ip": findIp("http://httpbin.org/ip"),
}
return info
}
func findIp(url string) string {
ip := ""
client := &http.Client{
Transport: &http.Transport{
DisableKeepAlives: false,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
Timeout: time.Duration(1 * time.Second),
}
req, _ := http.NewRequest("GET", url, nil)
res, err := client.Do(req)
if err != nil {
log.Error().Err(errors.Wrapf(err, "cannot connect to %s", url)).Msg("timeout")
return ip
}
if res.Body != nil {
defer res.Body.Close()
if res.StatusCode == http.StatusOK {
contents, err := ioutil.ReadAll(res.Body)
if err != nil {
return ip
}
jsonMap := make(map[string]string)
err = json.Unmarshal([]byte(contents), &jsonMap)
if err != nil {
return ip
}
return jsonMap["origin"]
}
}
return ip
}

View File

@@ -1,133 +0,0 @@
package server
import (
"context"
"net/http"
"net/http/pprof"
"os"
"runtime"
"sync/atomic"
"time"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/zerolog/log"
"github.com/stefanprodan/k8s-podinfo/pkg/fscache"
)
var (
healthy int32
ready int32
dataPath string
watcher *fscache.Watcher
)
type Server struct {
mux *http.ServeMux
}
func NewServer(options ...func(*Server)) *Server {
s := &Server{mux: http.NewServeMux()}
for _, f := range options {
f(s)
}
s.mux.HandleFunc("/", s.index)
s.mux.HandleFunc("/healthz", s.healthz)
s.mux.HandleFunc("/readyz", s.readyz)
s.mux.HandleFunc("/readyz/enable", s.enable)
s.mux.HandleFunc("/readyz/disable", s.disable)
s.mux.HandleFunc("/echo", s.echo)
s.mux.HandleFunc("/echoheaders", s.echoHeaders)
s.mux.HandleFunc("/backend", s.backend)
s.mux.HandleFunc("/job", s.job)
s.mux.HandleFunc("/read", s.read)
s.mux.HandleFunc("/write", s.write)
s.mux.HandleFunc("/error", s.error)
s.mux.HandleFunc("/panic", s.panic)
s.mux.HandleFunc("/configs", s.configs)
s.mux.HandleFunc("/version", s.version)
s.mux.Handle("/metrics", promhttp.Handler())
// Register pprof handlers
s.mux.HandleFunc("/debug/pprof/", pprof.Index)
s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
s.mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
s.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
s.mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
// API
s.mux.HandleFunc("/api/info", s.apiInfo)
s.mux.HandleFunc("/api/echo", s.apiEcho)
return s
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", runtime.Version())
s.mux.ServeHTTP(w, r)
}
func ListenAndServe(port string, timeout time.Duration, stopCh <-chan struct{}) {
inst := NewInstrument()
srv := &http.Server{
Addr: ":" + port,
Handler: inst.Wrap(NewServer()),
ReadTimeout: 5 * time.Second,
WriteTimeout: 1 * time.Minute,
IdleTimeout: 15 * time.Second,
}
atomic.StoreInt32(&healthy, 1)
atomic.StoreInt32(&ready, 1)
// local storage path
dataPath = os.Getenv("data")
if len(dataPath) < 1 {
dataPath = "/data"
}
// config path
configPath := os.Getenv("configPath")
if len(configPath) > 0 {
var err error
watcher, err = fscache.NewWatch(configPath)
if err != nil {
log.Error().Err(err).Msgf("%s watch error", configPath)
} else {
watcher.Watch()
}
}
// run server in background
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal().Err(err).Msg("HTTP server crashed")
}
}()
// wait for SIGTERM or SIGINT
<-stopCh
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// all calls to /healthz and /readyz will fail from now on
atomic.StoreInt32(&healthy, 0)
atomic.StoreInt32(&ready, 0)
log.Info().Msgf("Shutting down HTTP server with timeout: %v", timeout)
// wait for Kubernetes readiness probe
// to remove this instance from the load balancer
// the readiness check interval must lower than the timeout
time.Sleep(timeout)
// attempt graceful shutdown
if err := srv.Shutdown(ctx); err != nil {
log.Error().Err(err).Msg("HTTP server graceful shutdown failed")
} else {
log.Info().Msg("HTTP server stopped")
}
}

View File

@@ -1,4 +1,4 @@
package version
var VERSION = "0.6.1"
var GITCOMMIT = "unknown"
var VERSION = "1.4.0"
var REVISION = "unknown"

View File

@@ -39,7 +39,7 @@
<span slot="badge">${ pings }</span>
<v-icon left dark>touch_app</v-icon>
</v-badge>
Ping
Ping
</v-btn>
</v-layout>
</v-parallax>
@@ -86,7 +86,7 @@
</v-card-title>
<v-card-text>
Instrumentation with Prometheus (RED method).
Structured logging with zerolog and Fluentd.
Structured logging with zap and Fluentd.
Tracing with Jaeger and Istio.
</v-card-text>
</v-card>
@@ -154,10 +154,13 @@
if (self.info.version) {
if (self.info.version != data.version) {
console.log("New version", data.version)
window.location.reload()
//window.location.reload()
}
}
self.info = data
self.info.color = parseInt(data.version.split('.').reverse()[0], 10) === 0
? 'blue'
: 'green'
document.title = data.hostname
}
xhr.onerror = function() {

202
vendor/github.com/aws/aws-sdk-go/LICENSE.txt generated vendored Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

3
vendor/github.com/aws/aws-sdk-go/NOTICE.txt generated vendored Normal file
View File

@@ -0,0 +1,3 @@
AWS SDK for Go
Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Copyright 2014-2015 Stripe, Inc.

145
vendor/github.com/aws/aws-sdk-go/aws/awserr/error.go generated vendored Normal file
View File

@@ -0,0 +1,145 @@
// Package awserr represents API error interface accessors for the SDK.
package awserr
// An Error wraps lower level errors with code, message and an original error.
// The underlying concrete error type may also satisfy other interfaces which
// can be to used to obtain more specific information about the error.
//
// Calling Error() or String() will always include the full information about
// an error based on its underlying type.
//
// Example:
//
// output, err := s3manage.Upload(svc, input, opts)
// if err != nil {
// if awsErr, ok := err.(awserr.Error); ok {
// // Get error details
// log.Println("Error:", awsErr.Code(), awsErr.Message())
//
// // Prints out full error message, including original error if there was one.
// log.Println("Error:", awsErr.Error())
//
// // Get original error
// if origErr := awsErr.OrigErr(); origErr != nil {
// // operate on original error.
// }
// } else {
// fmt.Println(err.Error())
// }
// }
//
type Error interface {
// Satisfy the generic error interface.
error
// Returns the short phrase depicting the classification of the error.
Code() string
// Returns the error details message.
Message() string
// Returns the original error if one was set. Nil is returned if not set.
OrigErr() error
}
// BatchError is a batch of errors which also wraps lower level errors with
// code, message, and original errors. Calling Error() will include all errors
// that occurred in the batch.
//
// Deprecated: Replaced with BatchedErrors. Only defined for backwards
// compatibility.
type BatchError interface {
// Satisfy the generic error interface.
error
// Returns the short phrase depicting the classification of the error.
Code() string
// Returns the error details message.
Message() string
// Returns the original error if one was set. Nil is returned if not set.
OrigErrs() []error
}
// BatchedErrors is a batch of errors which also wraps lower level errors with
// code, message, and original errors. Calling Error() will include all errors
// that occurred in the batch.
//
// Replaces BatchError
type BatchedErrors interface {
// Satisfy the base Error interface.
Error
// Returns the original error if one was set. Nil is returned if not set.
OrigErrs() []error
}
// New returns an Error object described by the code, message, and origErr.
//
// If origErr satisfies the Error interface it will not be wrapped within a new
// Error object and will instead be returned.
func New(code, message string, origErr error) Error {
var errs []error
if origErr != nil {
errs = append(errs, origErr)
}
return newBaseError(code, message, errs)
}
// NewBatchError returns an BatchedErrors with a collection of errors as an
// array of errors.
func NewBatchError(code, message string, errs []error) BatchedErrors {
return newBaseError(code, message, errs)
}
// A RequestFailure is an interface to extract request failure information from
// an Error such as the request ID of the failed request returned by a service.
// RequestFailures may not always have a requestID value if the request failed
// prior to reaching the service such as a connection error.
//
// Example:
//
// output, err := s3manage.Upload(svc, input, opts)
// if err != nil {
// if reqerr, ok := err.(RequestFailure); ok {
// log.Println("Request failed", reqerr.Code(), reqerr.Message(), reqerr.RequestID())
// } else {
// log.Println("Error:", err.Error())
// }
// }
//
// Combined with awserr.Error:
//
// output, err := s3manage.Upload(svc, input, opts)
// if err != nil {
// if awsErr, ok := err.(awserr.Error); ok {
// // Generic AWS Error with Code, Message, and original error (if any)
// fmt.Println(awsErr.Code(), awsErr.Message(), awsErr.OrigErr())
//
// if reqErr, ok := err.(awserr.RequestFailure); ok {
// // A service error occurred
// fmt.Println(reqErr.StatusCode(), reqErr.RequestID())
// }
// } else {
// fmt.Println(err.Error())
// }
// }
//
type RequestFailure interface {
Error
// The status code of the HTTP response.
StatusCode() int
// The request ID returned by the service for a request failure. This will
// be empty if no request ID is available such as the request failed due
// to a connection error.
RequestID() string
}
// NewRequestFailure returns a new request error wrapper for the given Error
// provided.
func NewRequestFailure(err Error, statusCode int, reqID string) RequestFailure {
return newRequestError(err, statusCode, reqID)
}

Some files were not shown because too many files have changed in this diff Show More