Compare commits

...

81 Commits
2.0.0 ... 3.2.0

Author SHA1 Message Date
Stefan Prodan
af6868a8de Merge pull request #45 from stefanprodan/prep-3.2.0
Release v3.2.0
2020-01-24 11:26:03 +02:00
stefanprodan
910e7139f9 Release v3.2.0 2020-01-24 11:06:02 +02:00
Stefan Prodan
fe65869b6b Merge pull request #43 from stefanprodan/helm-v3-e2e
e2e: Update Helm to v3 and Kubernetes to v1.17
2020-01-24 11:02:58 +02:00
Stefan Prodan
2a319d9d0d Merge pull request #44 from hiddeco/unhealthy-unready
Add `--unhealthy` and `--unready` flags
2020-01-23 22:42:29 +02:00
Hidde Beydals
48402eff7e Add --unhealthy and --unready flags to chart 2020-01-23 21:06:30 +01:00
Hidde Beydals
15600cc7d3 Lowercase all flag descriptions 2020-01-23 21:06:30 +01:00
Hidde Beydals
ed2a774e10 Add --unhealthy and --unready flags
Depending on the flag set, the healthy or ready state is never
reached.
2020-01-23 21:06:22 +01:00
stefanprodan
1d590c07cb e2e: Update Helm to v3 and Kubernetes to v1.17 2020-01-22 13:16:03 +02:00
stefanprodan
948de81ed3 Update manifests to v3.1.5 2019-12-26 15:45:17 +02:00
stefanprodan
78658c0311 Release v3.1.5 cuddle edition 2019-11-07 00:31:49 +02:00
stefanprodan
7b6f11780a Rename GitHub workflow for kustomize testing 2019-11-04 09:59:11 +02:00
stefanprodan
d65044ff2e Release v3.1.4 2019-11-04 09:22:36 +02:00
Stefan Prodan
18c63ad7f7 Merge pull request #42 from mumoshu/h2c
feat: Add H2C support
2019-11-04 09:16:12 +02:00
Yusuke Kuoka
a8260081d9 Add h2c.enabled to chart for toggling H2C upgrading support 2019-11-04 14:17:10 +09:00
Yusuke Kuoka
0ff49e5057 feat: Add H2C support
`podinfo --h2c` allows upgrading a HTTP/1.1 connection to HTTP/2 Cleartext.

This allows `podinfo` to be used in e.g. a H2C load-test like `echo "GET http://localhost:9898/status/200" | vegeta -h2c`, or a H2C connectivity test like done with `curl -v http2 http://localhost:9898/status/200`.

I have manually verified this to work by running `curl -v --http2` on macOS and seeing the H2C upgrade happens onl when `-h2c` is provided to `podinfo`.

Without `-h2c`:

```
$ curl -v --http2 localhost:9898/status/200
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9898 (#0)
> GET /status/200 HTTP/1.1
> Host: localhost:9898
> User-Agent: curl/7.54.0
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< X-Content-Type-Options: nosniff
< Date: Mon, 04 Nov 2019 04:58:01 GMT
< Content-Length: 19
<
{
  "status": 200
* Connection #0 to host localhost left intact
}
```

With `-h2c`:

```
$ curl -v --http2 localhost:9898/status/200
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9898 (#0)
> GET /status/200 HTTP/1.1
> Host: localhost:9898
> User-Agent: curl/7.54.0
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
>
< HTTP/1.1 101 Switching Protocols
< Connection: Upgrade
< Upgrade: h2c
* Received 101
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Connection state changed (MAX_CONCURRENT_STREAMS updated)!
< HTTP/2 200
< content-type: application/json; charset=utf-8
< x-content-type-options: nosniff
< content-length: 19
< date: Mon, 04 Nov 2019 04:58:28 GMT
<
{
  "status": 200
* Connection #0 to host localhost left intact
}
`
2019-11-04 14:10:50 +09:00
Stefan Prodan
79cfe56484 Merge pull request #41 from stefanprodan/gh-actions
Add GitHub workflow for manifests validation
2019-10-23 17:35:53 +03:00
stefanprodan
7e36892e26 Add GitHub workflow for manifests validation
- validate kustomize build with kubeval strict mode
- deny containers with latest image tag
- deny deployments and services without app label selector
- warn if deployments have no prometheus pod annotations
2019-10-23 17:10:21 +03:00
Stefan Prodan
3d6d0bed69 Merge pull request #40 from stefanprodan/linkerd-profile
Add Linkerd service profile to Helm chart
2019-10-17 13:56:39 +03:00
stefanprodan
b213e0af0a Release v3.1.3 2019-10-17 13:50:48 +03:00
stefanprodan
42ad3faf5a Add Linkerd service profile to chart 2019-10-17 13:47:44 +03:00
stefanprodan
939fd5b24d Add Go report card 2019-10-17 13:35:54 +03:00
stefanprodan
36ec3ef378 Fix UI parallax img 2019-10-13 09:52:33 +03:00
Stefan Prodan
287e005129 Merge pull request #39 from stefanprodan/ui-logo
Make UI logo URL configurable
2019-10-12 18:10:40 +03:00
stefanprodan
0b3e88d6de Add release namespace to Helm tests 2019-10-12 18:00:14 +03:00
stefanprodan
10139749da Turn off CircleCI docker_layer_caching 2019-10-12 17:50:12 +03:00
stefanprodan
f891e0683b Release v3.1.2 2019-10-12 17:45:54 +03:00
stefanprodan
647b4cba04 Add UI settings to Helm chart 2019-10-12 17:44:37 +03:00
stefanprodan
c5df50c774 Make UI logo URL configurable 2019-10-12 17:41:21 +03:00
Stefan Prodan
2b1d325343 Merge pull request #38 from stefanprodan/go1.13
Update to go 1.13
2019-09-27 17:43:52 +03:00
stefanprodan
319d57cb68 Update to go 1.13 2019-09-27 17:04:39 +03:00
Stefan Prodan
087da02dbb Merge pull request #37 from stefanprodan/chart-fixes
Fix Helm tests when running inside a service mesh
2019-09-27 16:18:46 +03:00
stefanprodan
7d00f68180 Bump version to 3.1.1 2019-09-27 16:10:22 +03:00
stefanprodan
87c9bb8ba2 Exclude Helm test pods for service mesh 2019-09-27 16:09:24 +03:00
Stefan Prodan
5fb970b526 Merge pull request #36 from stefanprodan/backends
Add support for multiple backends
2019-09-27 12:16:28 +03:00
stefanprodan
56b404bd84 Release v3.1.0 2019-09-27 12:10:29 +03:00
stefanprodan
a12d0a1ed7 Add support for multiple backends
When calling /echo, the backends requests will be run in parallel and the results are aggregated and returned to the caller as a json array
2019-09-27 11:52:22 +03:00
Stefan Prodan
51979787b0 Fix Helm repo address 2019-09-26 12:29:49 +03:00
Stefan Prodan
8b37756118 Merge pull request #35 from eladb/patch-1
remove duplicate "ingress" entries in readme
2019-09-14 09:20:06 +02:00
Elad Ben-Israel
1eb1da110b remove duplicate "ingress" entries in readme 2019-09-11 14:10:06 +03:00
stefanprodan
d1ed907f1e Make tests work with Helm v3 2019-09-05 20:53:20 +03:00
stefanprodan
8e6eccecda Fix chart for old Helm versions 2019-09-05 16:01:34 +03:00
Stefan Prodan
f3db1adb27 Merge pull request #34 from stefanprodan/ingress-fix
Allow ingress with no hosts set
2019-09-05 14:48:49 +03:00
stefanprodan
7f3e11c1ce Allow ingress with no hosts set 2019-09-05 14:42:36 +03:00
Stefan Prodan
a7eb7e4995 Merge pull request #33 from stefanprodan/prep-3.0.0
Release v3.0.0
2019-09-05 12:23:02 +03:00
stefanprodan
43194bb342 Release v3.0.0 2019-09-05 12:14:18 +03:00
Stefan Prodan
c7d21968e7 Merge pull request #32 from stefanprodan/gprc-health
Implement gRPC health endpoint
2019-09-05 11:43:13 +03:00
stefanprodan
214a19fb0f Add gRPC service name flag to check command 2019-09-05 09:41:02 +03:00
stefanprodan
82ea2fa993 Update Kubernetes Kind to v0.5.1 2019-09-05 09:29:51 +03:00
stefanprodan
d84913c31e Add gPRC Helm test 2019-09-05 09:22:49 +03:00
stefanprodan
6bac5ffaa2 Add gPRC port and service name to chart 2019-09-05 01:08:26 +03:00
stefanprodan
eacf909c4a Add gPRC health check to CLI 2019-09-05 00:29:09 +03:00
stefanprodan
f7c1669125 Run gPRC health server if grpc-port flag is set 2019-09-05 00:28:32 +03:00
stefanprodan
158d6e82da Add gRPC health server 2019-09-05 00:20:28 +03:00
Stefan Prodan
4d890382e5 Merge pull request #30 from stefanprodan/service-account
Add service account to Helm chart
2019-08-13 12:30:04 +03:00
stefanprodan
83842e01f7 Rename service account create to enabled 2019-08-13 12:12:52 +03:00
stefanprodan
37b453fbbc Release v2.1.3 2019-08-13 12:08:50 +03:00
stefanprodan
53c6b472de Fix ClusterIP creation 2019-08-13 12:02:32 +03:00
stefanprodan
c759f958c0 e2e: print logs after tests finished 2019-08-13 11:53:28 +03:00
stefanprodan
5d14183809 Add service account to Helm chart 2019-08-13 11:37:10 +03:00
stefanprodan
ab74d6ef0b Release v2.1.2
Make the ClusterIP service optional in helm chart (should be disabled when using Flagger)
2019-08-13 10:50:03 +03:00
stefanprodan
fefcae34c1 Rename test config 2019-08-09 18:00:41 +03:00
stefanprodan
ed81a06a82 Release v2.1.1
Use Docker Hub instead of Quay
2019-08-09 17:53:15 +03:00
stefanprodan
633982b0e5 Use Docker Hub repo 2019-08-09 17:21:19 +03:00
Stefan Prodan
4154e01fdd Merge pull request #29 from stefanprodan/swagger
Implement Swagger support
2019-08-07 16:01:15 +03:00
stefanprodan
02d7f06d35 Release 2.1.0 2019-08-07 15:54:03 +03:00
stefanprodan
555450868e Move Swagger doc to server.go 2019-08-07 15:22:05 +03:00
stefanprodan
94085d6dc6 Add schemes to Swagger docs 2019-08-07 15:17:34 +03:00
stefanprodan
630841d81b Push latest tag to Docker Hub and Quay 2019-08-07 15:01:58 +03:00
stefanprodan
ea7f4fcdf7 Add the swagger UI link to docs 2019-08-07 14:54:47 +03:00
stefanprodan
e97c926611 Add license to swagger docs 2019-08-07 14:54:33 +03:00
stefanprodan
2479134e78 Implement swagger support
- add swagger definitions for all API routes
- self-host the swagger UI on `/swagger/`
- serve swagger spec on `/swagger.json`
2019-08-07 14:17:35 +03:00
Stefan Prodan
b0bbd16a77 Merge pull request #28 from stefanprodan/prep-2.0.2
Release v2.0.2
2019-08-06 22:58:51 +03:00
stefanprodan
7564949695 Add e2e docs 2019-08-06 22:53:20 +03:00
stefanprodan
d34da2ab91 Release v2.0.2 2019-08-06 22:15:19 +03:00
stefanprodan
182156d9b4 Add build step to e2e tests 2019-08-06 22:13:19 +03:00
Stefan Prodan
e0864b6e20 Merge pull request #27 from stefanprodan/ci
Migrate to CircleCI and run e2e tests with Kubernetes Kind
2019-08-06 21:59:13 +03:00
stefanprodan
de2a9c464a Release v2.0.1 2019-08-06 21:53:16 +03:00
stefanprodan
fed04ad692 Add e2e testing with Kubernetes Kind and Helm 2019-08-06 21:36:25 +03:00
stefanprodan
c907163073 Add JWT test to helm chart 2019-08-06 21:08:54 +03:00
stefanprodan
67578338c9 Toggle timeline background based on version 2019-08-06 20:46:17 +03:00
stefanprodan
0c27729000 Migrate CI to CircleCI 2019-08-06 20:36:57 +03:00
57 changed files with 2675 additions and 250 deletions

View File

@@ -1,8 +1,50 @@
version: 2.1
jobs:
e2e-kubernetes:
machine: true
steps:
- checkout
- run:
name: Build podinfo container
command: e2e/build.sh
- run:
name: Start Kubernetes Kind cluster
command: e2e/bootstrap.sh
- run:
name: Install podinfo with Helm v3
command: e2e/install.sh
- run:
name: Run Helm tests
command: e2e/test.sh
push-container:
docker:
- image: circleci/golang:1.13
working_directory: ~/build
steps:
- checkout
- setup_remote_docker:
docker_layer_caching: false
- run: make build-container
- run: |
if [ -z "$CIRCLE_TAG" ]; then
echo "Not a release, skipping container push";
else
echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin;
echo $QUAY_PASS | docker login -u $QUAY_USER --password-stdin quay.io;
make push-container;
fi
push-binary:
docker:
- image: circleci/golang:1.13
steps:
- checkout
- run: curl -sL https://git.io/goreleaser | bash
push-helm-charts:
docker:
- image: circleci/golang:1.12
- image: circleci/golang:1.13
steps:
- checkout
- run:
@@ -40,11 +82,29 @@ jobs:
else
echo "Not a release! Skip charts publish"
fi
workflows:
version: 2
build-test:
jobs:
- e2e-kubernetes
release:
jobs:
- push-binary:
filters:
branches:
ignore: /.*/
tags:
ignore: /^chart.*/
- push-container:
filters:
branches:
ignore: /.*/
tags:
ignore: /^chart.*/
- push-helm-charts:
requires:
- push-container
filters:
branches:
ignore: /.*/

51
.github/policy/kubernetes.rego vendored Normal file
View File

@@ -0,0 +1,51 @@
package kubernetes
name = input.metadata.name
kind = input.kind
is_service {
input.kind = "Service"
}
is_deployment {
input.kind = "Deployment"
}
is_pod {
input.kind = "Pod"
}
split_image(image) = [image, "latest"] {
not contains(image, ":")
}
split_image(image) = [image_name, tag] {
[image_name, tag] = split(image, ":")
}
pod_containers(pod) = all_containers {
keys = {"containers", "initContainers"}
all_containers = [c | keys[k]; c = pod.spec[k][_]]
}
containers[container] {
pods[pod]
all_containers = pod_containers(pod)
container = all_containers[_]
}
containers[container] {
all_containers = pod_containers(input)
container = all_containers[_]
}
pods[pod] {
is_deployment
pod = input.spec.template
}
pods[pod] {
is_pod
pod = input
}

43
.github/policy/rules.rego vendored Normal file
View File

@@ -0,0 +1,43 @@
package main
import data.kubernetes
name = input.metadata.name
# Deny containers with latest image tag
deny[msg] {
kubernetes.containers[container]
[image_name, "latest"] = kubernetes.split_image(container.image)
msg = sprintf("%s in the %s %s has an image %s, using the latest tag", [container.name, kubernetes.kind, kubernetes.name, image_name])
}
# Deny services without app label selector
service_labels {
input.spec.selector["app"]
}
deny[msg] {
kubernetes.is_service
not service_labels
msg = sprintf("Service %s should set app label selector", [name])
}
# Deny deployments without app label selector
match_labels {
input.spec.selector.matchLabels["app"]
}
deny[msg] {
kubernetes.is_deployment
not match_labels
msg = sprintf("Service %s should set app label selector", [name])
}
# Warn if deployments have no prometheus pod annotations
annotations {
input.spec.template.metadata.annotations["prometheus.io/scrape"]
input.spec.template.metadata.annotations["prometheus.io/port"]
}
warn[msg] {
kubernetes.is_deployment
not annotations
msg = sprintf("Deployment %s should set prometheus.io/scrape and prometheus.io/port pod annotations", [name])
}

17
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
on: [push, pull_request]
name: test
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: kubeval
uses: stefanprodan/kube-tools@v1
with:
command: |
kustomize build ./kustomize | kubeval --strict
- name: conftest
uses: stefanprodan/kube-tools@v1
with:
command: |
kustomize build ./kustomize | conftest test -p .github/policy -

View File

@@ -1,32 +0,0 @@
sudo: required
language: go
go:
- 1.12.x
services:
- docker
addons:
apt:
packages:
- docker-ce
script:
- make build-container
after_success:
- if [ -z "$TRAVIS_TAG" ]; then
echo "Not a release, skipping container push";
else
echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin;
echo $QUAY_PASS | docker login -u $QUAY_USER --password-stdin quay.io;
make push-container;
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.12 as builder
FROM golang:1.13 as builder
RUN mkdir -p /podinfo/
@@ -6,7 +6,7 @@ WORKDIR /podinfo
COPY . .
RUN GOPROXY=https://proxy.golang.org go mod download
RUN go mod download
RUN go test -v -race ./...

View File

@@ -6,18 +6,21 @@ TAG?=latest
NAME:=podinfo
DOCKER_REPOSITORY:=stefanprodan
DOCKER_IMAGE_NAME:=$(DOCKER_REPOSITORY)/$(NAME)
GITCOMMIT:=$(shell git describe --dirty --always)
GIT_COMMIT:=$(shell git describe --dirty --always)
VERSION:=$(shell grep 'VERSION' pkg/version/version.go | awk '{ print $$4 }' | tr -d '"')
EXTRA_RUN_ARGS?=
run:
GO111MODULE=on go run cmd/podinfo/*
GO111MODULE=on go run -ldflags "-s -w -X github.com/stefanprodan/podinfo/pkg/version.REVISION=$(GIT_COMMIT)" cmd/podinfo/* \
--level=debug --grpc-port=9999 --backend-url=https://httpbin.org/status/401 --backend-url=https://httpbin.org/status/500 \
--ui-logo=https://raw.githubusercontent.com/stefanprodan/podinfo/gh-pages/cuddle_clap.gif --ui-color=#34577c $(EXTRA_RUN_ARGS)
test:
GO111MODULE=on go test -v -race ./...
build:
GO111MODULE=on GIT_COMMIT=$$(git rev-list -1 HEAD) && GO111MODULE=on CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/stefanprodan/podinfo/pkg/version.REVISION=$${GIT_COMMIT}" -a -o ./bin/podinfo ./cmd/podinfo/*
GO111MODULE=on GIT_COMMIT=$$(git rev-list -1 HEAD) && GO111MODULE=on CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/stefanprodan/podinfo/pkg/version.REVISION=$${GIT_COMMIT}" -a -o ./bin/podcli ./cmd/podcli/*
GO111MODULE=on GIT_COMMIT=$$(git rev-list -1 HEAD) && GO111MODULE=on CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/stefanprodan/podinfo/pkg/version.REVISION=$(GIT_COMMIT)" -a -o ./bin/podinfo ./cmd/podinfo/*
GO111MODULE=on GIT_COMMIT=$$(git rev-list -1 HEAD) && GO111MODULE=on CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/stefanprodan/podinfo/pkg/version.REVISION=$(GIT_COMMIT)" -a -o ./bin/podcli ./cmd/podcli/*
build-charts:
helm lint charts/*
@@ -26,10 +29,21 @@ build-charts:
build-container:
docker build -t $(DOCKER_IMAGE_NAME):$(VERSION) .
test-container:
@docker rm -f podinfo || true
@docker run -dp 9898:9898 --name=podinfo $(DOCKER_IMAGE_NAME):$(VERSION)
@docker ps
@TOKEN=$$(curl -sd 'test' localhost:9898/token | jq -r .token) && \
curl -sH "Authorization: Bearer $${TOKEN}" localhost:9898/token/validate | grep test
push-container:
docker tag $(DOCKER_IMAGE_NAME):$(VERSION) $(DOCKER_IMAGE_NAME):latest
docker push $(DOCKER_IMAGE_NAME):$(VERSION)
docker push $(DOCKER_IMAGE_NAME):latest
docker tag $(DOCKER_IMAGE_NAME):$(VERSION) quay.io/$(DOCKER_IMAGE_NAME):$(VERSION)
docker tag $(DOCKER_IMAGE_NAME):$(VERSION) quay.io/$(DOCKER_IMAGE_NAME):latest
docker push quay.io/$(DOCKER_IMAGE_NAME):$(VERSION)
docker push quay.io/$(DOCKER_IMAGE_NAME):latest
version-set:
@next="$(TAG)" && \
@@ -44,3 +58,7 @@ version-set:
release:
git tag $(VERSION)
git push origin $(VERSION)
swagger:
GO111MODULE=on go get github.com/swaggo/swag/cmd/swag
cd pkg/api && $$(go env GOPATH)/bin/swag init -g server.go

View File

@@ -1,10 +1,11 @@
# podinfo
[![Build Status](https://travis-ci.org/stefanprodan/podinfo.svg?branch=master)](https://travis-ci.org/stefanprodan/podinfo)
[![CircleCI](https://circleci.com/gh/stefanprodan/podinfo.svg?style=svg)](https://circleci.com/gh/stefanprodan/podinfo)
[![conftest](https://github.com/stefanprodan/podinfo/workflows/test/badge.svg)](https://github.com/stefanprodan/podinfo/blob/master/.github/workflows/test.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/stefanprodan/podinfo)](https://goreportcard.com/report/github.com/stefanprodan/podinfo)
[![Docker Pulls](https://img.shields.io/docker/pulls/stefanprodan/podinfo)](https://hub.docker.com/r/stefanprodan/podinfo)
Podinfo is a tiny web application made with Go
that showcases best practices of running microservices in Kubernetes.
Podinfo is a tiny web application made with Go that showcases best practices of running microservices in Kubernetes.
Specifications:
@@ -13,10 +14,14 @@ Specifications:
* File watcher for secrets and configmaps
* Instrumented with Prometheus
* Tracing with Istio and Jaeger
* Linkerd service profile
* Structured logging with zap
* 12-factor app with viper
* Fault injection (random errors and latency)
* Helm chart
* Swagger docs
* Helm and Kustomize installers
* End-to-End testing with Kubernetes Kind and Helm
* Kustomize testing with GitHub Actions and Open Policy Agent
Web API:
@@ -40,6 +45,17 @@ Web API:
* `GET /store/{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`
* `GET /chunked/{seconds}` uses `transfer-encoding` type `chunked` to give a partial response and then waits for the specified period
* `GET /swagger.json` returns the API Swagger docs, used for Linkerd service profiling and Gloo routes discovery
gRPC API:
* `/grpc.health.v1.Health/Check` health checking
Web UI:
![podinfo-ui](https://raw.githubusercontent.com/stefanprodan/podinfo/gh-pages/screens/podinfo-ui-v3.png)
To access the Swagger UI open `<podinfo-host>/swagger/index.html` in a browser.
### Guides
@@ -53,17 +69,20 @@ Web API:
Helm:
```bash
helm repo add sp https://stefanprodan.github.io/podinfo
helm repo add podinfo https://stefanprodan.github.io/podinfo
helm upgrade --install --wait frontend \
--namespace test \
--set replicaCount=2 \
--set backend=http://backend-podinfo:9898/echo \
sp/podinfo
podinfo/podinfo
helm test frontend --cleanup
helm upgrade --install --wait backend \
--namespace test \
--set hpa.enabled=true \
sp/podinfo
podinfo/podinfo
```
Kustomize:
@@ -71,3 +90,9 @@ Kustomize:
```bash
kubectl apply -k github.com/stefanprodan/podinfo//kustomize
```
Docker:
```bash
docker run -dp 9898:9898 stefanprodan/podinfo
```

View File

@@ -1,6 +1,6 @@
apiVersion: v1
version: 2.0.0
appVersion: 2.0.0
version: 3.2.0
appVersion: 3.2.0
name: podinfo
engine: gotpl
description: Podinfo Helm chart for Kubernetes

View File

@@ -8,7 +8,7 @@ that showcases best practices of running microservices in Kubernetes.
To install the chart with the release name `my-release`:
```console
$ helm repo add sp https://stefanprodan.github.io/k8s-podinfo
$ helm repo add sp https://stefanprodan.github.io/podinfo
$ helm upgrade my-release --install sp/podinfo
```
@@ -32,20 +32,20 @@ The following tables lists the configurable parameters of the podinfo chart and
Parameter | Description | Default
--- | --- | ---
`affinity` | node/pod affinities | None
`color` | UI color | blue
`backend` | echo backend URL | None
`backends` | echo backend URL array | 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`
`faults.unhealthy` | when set, the healthy state is never reached | `false`
`faults.unready` | when set, the ready state is never reached | `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`
`image.tag` | image tag | `<VERSION>`
`ingress.enabled` | enables ingress | `false`
`ingress.annotations` | ingress annotations | None
`ingress.hosts` | ingress accepted hostnames | None
@@ -57,11 +57,18 @@ Parameter | Description | Default
`resources.requests/memory` | pod memory request | `16Mi`
`resources.limits/cpu` | pod CPU limit | None
`resources.limits/memory` | pod memory limit | None
`service.externalPort` | external port for the service | `9898`
`service.internalPort` | internal port for the service | `9898`
`service.nodePort` | node port for the service | `31198`
`service.enabled` | create Kubernetes service (should be disabled when using Flagger) | `true`
`service.metricsPort` | Prometheus metrics endpoint port | `9797`
`service.externalPort` | ClusterIP HTTP port | `9898`
`service.httpPort` | container HTTP port | `9898`
`service.nodePort` | NodePort for the HTTP endpoint | `31198`
`service.grpcPort` | ClusterIP gPRC port | `9999`
`service.grpcService` | gPRC service name | `podinfo`
`service.type` | type of service | `ClusterIP`
`tolerations` | list of node taints to tolerate | `[]`
`serviceAccount.enabled` | specifies whether a service account should be created | `false`
`serviceAccount.name` | the name of the service account to use, if not set and create is true, a name is generated using the fullname template | None
`linkerd.profile.enabled` | create Linkerd service profile | `false`
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example,

View File

@@ -30,3 +30,14 @@ Create chart name and version as used by the chart label.
{{- define "podinfo.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create the name of the service account to use
*/}}
{{- define "podinfo.serviceAccountName" -}}
{{- if .Values.serviceAccount.enabled -}}
{{ default (include "podinfo.fullname" .) .Values.serviceAccount.name }}
{{- else -}}
{{ default "default" .Values.serviceAccount.name }}
{{- end -}}
{{- end -}}

View File

@@ -8,7 +8,9 @@ metadata:
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
{{- if not .Values.hpa.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
strategy:
type: RollingUpdate
rollingUpdate:
@@ -23,25 +25,56 @@ spec:
app: {{ template "podinfo.name" . }}
release: {{ .Release.Name }}
annotations:
prometheus.io/scrape: 'true'
prometheus.io/scrape: "true"
prometheus.io/port: "{{ .Values.service.httpPort }}"
spec:
terminationGracePeriodSeconds: 30
{{- if .Values.serviceAccount.enabled }}
serviceAccountName: {{ template "podinfo.serviceAccountName" . }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command:
- ./podinfo
- --port={{ .Values.service.containerPort }}
- --port={{ .Values.service.httpPort | default 9898 }}
{{- if .Values.service.metricsPort }}
- --port-metrics={{ .Values.service.metricsPort }}
{{- end }}
{{- if .Values.service.grpcPort }}
- --grpc-port={{ .Values.service.grpcPort }}
{{- end }}
{{- if .Values.service.grpcService }}
- --grpc-service-name={{ .Values.service.grpcService }}
{{- end }}
{{- range .Values.backends }}
- --backend-url={{ . }}
{{- end }}
- --level={{ .Values.logLevel }}
- --random-delay={{ .Values.faults.delay }}
- --random-error={{ .Values.faults.error }}
{{- if .Values.faults.unhealthy }}
- --unhealthy
{{- end }}
{{- if .Values.faults.unready }}
- --unready
{{- end }}
{{- if .Values.h2c.enabled }}
- --h2c
{{- end }}
env:
- name: PODINFO_UI_COLOR
value: {{ .Values.color }}
{{- if .Values.message }}
{{- if .Values.ui.message }}
- name: PODINFO_UI_MESSAGE
value: {{ .Values.message }}
value: {{ .Values.ui.message }}
{{- end }}
{{- if .Values.ui.logo }}
- name: PODINFO_UI_LOGO
value: {{ .Values.ui.logo }}
{{- end }}
{{- if .Values.ui.color }}
- name: PODINFO_UI_COLOR
value: {{ .Values.ui.color }}
{{- end }}
{{- if .Values.backend }}
- name: PODINFO_BACKEND_URL
@@ -49,16 +82,26 @@ spec:
{{- end }}
ports:
- name: http
containerPort: {{ .Values.service.containerPort }}
containerPort: {{ .Values.service.httpPort | default 9898 }}
protocol: TCP
{{- if .Values.service.metricsPort }}
- name: http-metrics
containerPort: {{ .Values.service.metricsPort }}
protocol: TCP
{{- end }}
{{- if .Values.service.grpcPort }}
- name: grpc
containerPort: {{ .Values.service.grpcPort }}
protocol: TCP
{{- end }}
livenessProbe:
exec:
command:
- podcli
- check
- http
- localhost:{{ .Values.service.containerPort }}/healthz
initialDelaySeconds: 5
- localhost:{{ .Values.service.httpPort | default 9898 }}/healthz
initialDelaySeconds: 1
timeoutSeconds: 5
readinessProbe:
exec:
@@ -66,8 +109,8 @@ spec:
- podcli
- check
- http
- localhost:{{ .Values.service.containerPort }}/readyz
initialDelaySeconds: 5
- localhost:{{ .Values.service.httpPort | default 9898 }}/readyz
initialDelaySeconds: 1
timeoutSeconds: 5
volumeMounts:
- name: data

View File

@@ -5,7 +5,7 @@ metadata:
name: {{ template "podinfo.fullname" . }}
spec:
scaleTargetRef:
apiVersion: apps/v1beta2
apiVersion: apps/v1
kind: Deployment
name: {{ template "podinfo.fullname" . }}
minReplicas: {{ .Values.replicaCount }}

View File

@@ -1,6 +1,5 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "podinfo.fullname" . -}}
{{- $servicePort := .Values.service.port -}}
{{- $ingressPath := .Values.ingress.path -}}
apiVersion: extensions/v1beta1
kind: Ingress
@@ -36,4 +35,12 @@ spec:
serviceName: {{ $fullName }}
servicePort: http
{{- end }}
{{- if not .Values.ingress.hosts }}
- http:
paths:
- path: {{ $ingressPath }}
backend:
serviceName: {{ $fullName }}
servicePort: http
{{- end }}
{{- end }}

View File

@@ -0,0 +1,88 @@
{{- if .Values.linkerd.profile.enabled -}}
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
name: {{ template "podinfo.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local
spec:
routes:
- condition:
method: GET
pathRegex: /
name: GET /
- condition:
method: POST
pathRegex: /api/echo
name: POST /api/echo
- condition:
method: GET
pathRegex: /api/info
name: GET /api/info
- condition:
method: GET
pathRegex: /chunked/[^/]*
name: GET /chunked/{seconds}
- condition:
method: GET
pathRegex: /delay/[^/]*
name: GET /delay/{seconds}
- condition:
method: GET
pathRegex: /env
name: GET /env
- condition:
method: GET
pathRegex: /headers
name: GET /headers
- condition:
method: GET
pathRegex: /healthz
name: GET /healthz
- condition:
method: GET
pathRegex: /metrics
name: GET /metrics
- condition:
method: GET
pathRegex: /panic
name: GET /panic
- condition:
method: GET
pathRegex: /readyz
name: GET /readyz
- condition:
method: POST
pathRegex: /readyz/disable
name: POST /readyz/disable
- condition:
method: POST
pathRegex: /readyz/enable
name: POST /readyz/enable
- condition:
method: GET
pathRegex: /status/[^/]*
name: GET /status/{code}
- condition:
method: POST
pathRegex: /store
name: POST /store
- condition:
method: GET
pathRegex: /store/[^/]*
name: GET /store/{hash}
- condition:
method: POST
pathRegex: /token
name: POST /token
- condition:
method: POST
pathRegex: /token/validate
name: POST /token/validate
- condition:
method: GET
pathRegex: /version
name: GET /version
- condition:
method: POST
pathRegex: /ws/echo
name: POST /ws/echo
{{- end }}

View File

@@ -1,3 +1,4 @@
{{- if .Values.service.enabled -}}
apiVersion: v1
kind: Service
metadata:
@@ -17,6 +18,13 @@ spec:
{{- if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.nodePort))) }}
nodePort: {{ .Values.service.nodePort }}
{{- end }}
{{- if .Values.service.grpcPort }}
- port: {{ .Values.service.grpcPort }}
targetPort: grpc
protocol: TCP
name: grpc
{{- end }}
selector:
app: {{ template "podinfo.name" . }}
release: {{ .Release.Name }}
{{- end }}

View File

@@ -0,0 +1,11 @@
{{- if .Values.serviceAccount.enabled -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ template "podinfo.serviceAccountName" . }}
labels:
app: {{ template "podinfo.name" . }}
chart: {{ template "podinfo.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- end -}}

View File

@@ -0,0 +1,21 @@
apiVersion: v1
kind: Pod
metadata:
name: {{ template "podinfo.fullname" . }}-grpc-test-{{ randAlphaNum 5 | lower }}
labels:
heritage: {{ .Release.Service }}
release: {{ .Release.Name }}
chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app: {{ template "podinfo.name" . }}
annotations:
"helm.sh/hook": test-success
sidecar.istio.io/inject: "false"
linkerd.io/inject: disabled
appmesh.k8s.aws/sidecarInjectorWebhook: disabled
spec:
containers:
- name: grpc-health-probe
image: stefanprodan/grpc_health_probe:v0.3.0
command: ['grpc_health_probe']
args: ['-addr={{ template "podinfo.fullname" . }}.{{ .Release.Namespace }}:{{ .Values.service.grpcPort }}']
restartPolicy: Never

View File

@@ -0,0 +1,29 @@
apiVersion: v1
kind: Pod
metadata:
name: {{ template "podinfo.fullname" . }}-jwt-test-{{ randAlphaNum 5 | lower }}
labels:
heritage: {{ .Release.Service }}
release: {{ .Release.Name }}
chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app: {{ template "podinfo.name" . }}
annotations:
"helm.sh/hook": test-success
sidecar.istio.io/inject: "false"
linkerd.io/inject: disabled
appmesh.k8s.aws/sidecarInjectorWebhook: disabled
spec:
containers:
- name: tools
image: giantswarm/tiny-tools
command:
- sh
- -c
- |
TOKEN=$(curl -sd 'test' ${PODINFO_SVC}/token | jq -r .token) &&
curl -sH "Authorization: Bearer ${TOKEN}" ${PODINFO_SVC}/token/validate | grep test
env:
- name: PODINFO_SVC
value: {{ template "podinfo.fullname" . }}.{{ .Release.Namespace }}:{{ .Values.service.externalPort }}
restartPolicy: Never

View File

@@ -9,10 +9,19 @@ metadata:
app: {{ template "podinfo.name" . }}
annotations:
"helm.sh/hook": test-success
sidecar.istio.io/inject: "false"
linkerd.io/inject: disabled
appmesh.k8s.aws/sidecarInjectorWebhook: disabled
spec:
containers:
- name: curl
image: radial/busyboxplus:curl
command: ['curl']
args: ['{{ template "podinfo.fullname" . }}:{{ .Values.service.externalPort }}']
image: giantswarm/tiny-tools
command:
- sh
- -c
- |
curl -s ${PODINFO_SVC}/api/info | grep version
env:
- name: PODINFO_SVC
value: {{ template "podinfo.fullname" . }}.{{ .Release.Namespace }}:{{ .Values.service.externalPort }}
restartPolicy: Never

View File

@@ -1,41 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: {{ template "podinfo.fullname" . }}-storage-test-{{ randAlphaNum 5 | lower }}
labels:
heritage: {{ .Release.Service }}
release: {{ .Release.Name }}
chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app: {{ template "podinfo.name" . }}
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: tools
image: giantswarm/tiny-tools
command: ["/bin/sh", "/scripts/ping.sh"]
env:
- name: PODINFO_SVC
value: {{ template "podinfo.fullname" . }}:{{ .Values.service.externalPort }}
volumeMounts:
- name: scripts
mountPath: /scripts
restartPolicy: Never
volumes:
- name: scripts
configMap:
name: {{ template "podinfo.fullname" . }}-storage-cfg
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ template "podinfo.fullname" . }}-storage-cfg
labels:
heritage: {{ .Release.Service }}
release: {{ .Release.Name }}
chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app: {{ template "podinfo.name" . }}
data:
ping.sh: |
#!/bin/sh
curl -sS ${PODINFO_SVC}/store/$(curl -sSd 'test' ${PODINFO_SVC}/store | jq -r .hash) |grep test

View File

@@ -2,23 +2,36 @@
replicaCount: 1
logLevel: info
color: blue
backend: #http://backend-podinfo:9898/echo
message: #UI greetings
backends: []
ui:
color: "#34577c"
message: ""
logo: ""
faults:
delay: false
error: false
unhealthy: false
unready: false
h2c:
enabled: false
image:
repository: quay.io/stefanprodan/podinfo
tag: 2.0.0
repository: stefanprodan/podinfo
tag: 3.2.0
pullPolicy: IfNotPresent
service:
enabled: true
type: ClusterIP
metricsPort: 9797
httpPort: 9898
externalPort: 9898
containerPort: 9898
grpcPort: 9999
grpcService: podinfo
nodePort: 31198
# metrics-server add-on required
@@ -32,14 +45,25 @@ hpa:
# average http requests per second per pod (k8s-prometheus-adapter)
requests:
serviceAccount:
# Specifies whether a service account should be created
enabled: false
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name:
linkerd:
profile:
enabled: false
ingress:
enabled: false
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
path: /
hosts:
- podinfo.local
path: /*
hosts: []
# - podinfo.local
tls: []
# - secretName: chart-example-tls
# hosts:

View File

@@ -14,14 +14,19 @@ import (
"github.com/spf13/cobra"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/status"
)
var (
retryCount int
retryDelay time.Duration
method string
body string
timeout time.Duration
retryCount int
retryDelay time.Duration
method string
body string
timeout time.Duration
grpcServiceName string
)
var checkCmd = &cobra.Command{
@@ -51,6 +56,13 @@ var checkCertCmd = &cobra.Command{
RunE: runCheckCert,
}
var checkgRPCCmd = &cobra.Command{
Use: `grpc [address]`,
Short: "gRPC health check",
Example: ` check grpc localhost:8080 --service=podinfo --retry=1 --delay=2s --timeout=2s`,
RunE: runCheckgPRC,
}
func init() {
checkUrlCmd.Flags().StringVar(&method, "method", "GET", "HTTP method")
checkUrlCmd.Flags().StringVar(&body, "body", "", "HTTP POST/PUT content")
@@ -64,6 +76,12 @@ func init() {
checkTcpCmd.Flags().DurationVar(&timeout, "timeout", 5*time.Second, "timeout")
checkCmd.AddCommand(checkTcpCmd)
checkgRPCCmd.Flags().IntVar(&retryCount, "retry", 0, "times to retry the TCP check")
checkgRPCCmd.Flags().DurationVar(&retryDelay, "delay", 1*time.Second, "wait duration between retries")
checkgRPCCmd.Flags().DurationVar(&timeout, "timeout", 5*time.Second, "timeout")
checkgRPCCmd.Flags().StringVar(&grpcServiceName, "service", "", "gRPC service name")
checkCmd.AddCommand(checkgRPCCmd)
checkCmd.AddCommand(checkCertCmd)
rootCmd.AddCommand(checkCmd)
@@ -243,3 +261,53 @@ func fmtContentLength(b int64) string {
}
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
}
func runCheckgPRC(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 grpc localhost:8080")
}
address := args[0]
for n := 0; n <= retryCount; n++ {
if n != 1 {
time.Sleep(retryDelay)
}
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
logger.Info("check failed",
zap.String("address", address),
zap.Error(err))
continue
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
resp, err := grpc_health_v1.NewHealthClient(conn).Check(ctx, &grpc_health_v1.HealthCheckRequest{
Service: grpcServiceName,
})
cancel()
if err != nil {
if stat, ok := status.FromError(err); ok && stat.Code() == codes.Unimplemented {
logger.Info("gPRC health protocol not implemented")
os.Exit(1)
} else {
logger.Info("check failed",
zap.String("address", address),
zap.Error(err))
}
continue
}
conn.Close()
logger.Info("check succeed",
zap.String("status", resp.GetStatus().String()))
os.Exit(0)
}
os.Exit(1)
return nil
}

View File

@@ -2,28 +2,33 @@ package main
import (
"fmt"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/stefanprodan/podinfo/pkg/api"
"github.com/stefanprodan/podinfo/pkg/signals"
"github.com/stefanprodan/podinfo/pkg/version"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/stefanprodan/podinfo/pkg/api"
"github.com/stefanprodan/podinfo/pkg/grpc"
"github.com/stefanprodan/podinfo/pkg/signals"
"github.com/stefanprodan/podinfo/pkg/version"
)
func main() {
// flags definition
fs := pflag.NewFlagSet("default", pflag.ContinueOnError)
fs.Int("port", 9898, "port")
fs.Int("port", 9898, "HTTP port")
fs.Int("port-metrics", 0, "metrics port")
fs.Int("grpc-port", 0, "gRPC port")
fs.String("grpc-service-name", "podinfo", "gPRC service name")
fs.String("level", "info", "log level debug, info, warn, error, flat or panic")
fs.String("backend-url", "", "backend service URL")
fs.StringSlice("backend-url", []string{}, "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")
@@ -31,11 +36,15 @@ func main() {
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-logo", "", "UI logo")
fs.String("ui-color", "cyan", "UI color")
fs.String("ui-message", fmt.Sprintf("greetings from podinfo v%v", version.VERSION), "UI message")
fs.Bool("h2c", false, "allow upgrading to H2C")
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.Bool("unhealthy", false, "when set, healthy state is never reached")
fs.Bool("unready", false, "when set, ready state is never reached")
fs.Int("stress-cpu", 0, "number of CPU cores with 100 load")
fs.Int("stress-memory", 0, "MB of data to load into memory")
versionFlag := fs.BoolP("version", "v", false, "get version number")
@@ -59,6 +68,7 @@ func main() {
viper.RegisterAlias("backendUrl", "backend-url")
hostname, _ := os.Hostname()
viper.SetDefault("jwt-secret", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9")
viper.SetDefault("ui-logo", "https://raw.githubusercontent.com/stefanprodan/podinfo/gh-pages/cuddle_clap.gif")
viper.Set("hostname", hostname)
viper.Set("version", version.VERSION)
viper.Set("revision", version.REVISION)
@@ -90,6 +100,18 @@ func main() {
viper.Set("port", strconv.Itoa(port))
}
// load gRPC server config
var grpcCfg grpc.Config
if err := viper.Unmarshal(&grpcCfg); err != nil {
logger.Panic("config unmarshal failed", zap.Error(err))
}
// start gRPC server
if grpcCfg.Port > 0 {
grpcSrv, _ := grpc.NewServer(&grpcCfg, logger)
go grpcSrv.ListenAndServe()
}
// load HTTP server config
var srvCfg api.Config
if err := viper.Unmarshal(&srvCfg); err != nil {

35
e2e/README.md Normal file
View File

@@ -0,0 +1,35 @@
# podinfo end-to-end testing
The e2e testing infrastructure is powered by CircleCI and [Kubernetes Kind](https://github.com/kubernetes-sigs/kind).
### CI workflow
* download go modules
* run unit tests
* build container
* install kubectl, Helm v3 and Kubernetes Kind CLIs
* create local Kubernetes cluster with kind
* load podinfo image onto the local cluster
* deploy podinfo with Helm
* set the podinfo image to the locally built one
* run Helm tests
```yaml
jobs:
e2e-kubernetes:
machine: true
steps:
- checkout
- run:
name: Build podinfo container
command: e2e/build.sh
- run:
name: Start Kubernetes Kind cluster
command: e2e/bootstrap.sh
- run:
name: Install podinfo with Helm
command: e2e/install.sh
- run:
name: Run Helm tests
command: e2e/test.sh
```

26
e2e/bootstrap.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -o errexit
REPO_ROOT=$(git rev-parse --show-toplevel)
KIND_VERSION=v0.7.0
if [[ "$1" ]]; then
KIND_VERSION=$1
fi
echo ">>> Installing kubectl"
curl -sLO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl && \
chmod +x kubectl && \
sudo mv kubectl /usr/local/bin/
echo ">>> Installing kind"
curl -sSLo kind "https://github.com/kubernetes-sigs/kind/releases/download/$KIND_VERSION/kind-linux-amd64"
chmod +x kind
sudo mv kind /usr/local/bin/kind
echo ">>> Creating kind cluster"
kind create cluster --wait 5m
echo ">>> Installing Helm v3"
curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash

6
e2e/build.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -o errexit
docker build -t test/podinfo:latest .

13
e2e/install.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -o errexit
REPO_ROOT=$(git rev-parse --show-toplevel)
echo '>>> Loading image in Kind'
kind load docker-image test/podinfo:latest
echo '>>> Installing'
helm upgrade -i podinfo ${REPO_ROOT}/charts/podinfo --namespace=default
kubectl set image deployment/podinfo podinfo=test/podinfo:latest
kubectl rollout status deployment/podinfo

12
e2e/test.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -o errexit
function finish {
echo '>>> Test logs'
kubectl logs -l app=podinfo
}
trap finish EXIT
echo '>>> Start integration tests'
helm test podinfo

17
go.mod
View File

@@ -1,20 +1,19 @@
module github.com/stefanprodan/podinfo
go 1.12
go 1.13
require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/aws/aws-sdk-go v1.15.63 // indirect
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/readline v0.0.0-20160726135117-62c6fe619375
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/fatih/color v1.7.0
github.com/fsnotify/fsnotify v1.4.7
github.com/golang/protobuf v1.2.0 // indirect
github.com/go-chi/chi v4.0.2+incompatible // indirect
github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.0
github.com/hashicorp/go-cleanhttp v0.5.0 // indirect
@@ -31,7 +30,6 @@ require (
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699 // indirect
github.com/pelletier/go-toml v1.2.0 // indirect
github.com/pkg/errors v0.8.1 // indirect
github.com/prometheus/client_golang v0.8.0
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 // indirect
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e // indirect
@@ -42,12 +40,13 @@ require (
github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834 // indirect
github.com/spf13/pflag v1.0.2
github.com/spf13/viper v1.1.0
github.com/stretchr/testify v1.3.0 // indirect
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 // indirect
github.com/swaggo/http-swagger v0.0.0-20190614090009-c2865af9083e
github.com/swaggo/swag v1.6.2
github.com/ulikunitz/xz v0.5.4 // indirect
go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.9.1
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 // indirect
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
gopkg.in/yaml.v2 v2.2.1 // indirect
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80
google.golang.org/grpc v1.23.0
)

52
go.sum
View File

@@ -1,5 +1,14 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/aws/aws-sdk-go v1.15.63 h1:rPr7eEm/FSK23DoDKhbd9oLMYGT7JU9pkyfBUVOHXUo=
github.com/aws/aws-sdk-go v1.15.63/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
@@ -12,6 +21,7 @@ github.com/chzyer/readline v0.0.0-20160726135117-62c6fe619375 h1:JVe1zduaiPlSLOu
github.com/chzyer/readline v0.0.0-20160726135117-62c6fe619375/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -21,8 +31,25 @@ github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/jsonreference v0.19.0 h1:BqWKpV1dFd+AuiKlgtddwVIFQsuMpxfBDBHGfM2yNpk=
github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/spec v0.19.0 h1:A4SZ6IWh3lnjH0rG0Z5lkxazMGBECtrZcbyYQi+64k4=
github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
@@ -43,6 +70,8 @@ github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Ao
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
@@ -82,10 +111,19 @@ github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spf13/viper v1.1.0 h1:V7OZpY8i3C1x/pDmU0zNNlfVoDz112fSYvtWMjjS3f4=
github.com/spf13/viper v1.1.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM=
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
github.com/swaggo/http-swagger v0.0.0-20190614090009-c2865af9083e h1:m5sYJ43teIUlESuKRFQRRm7kqi6ExiYwVKfoXNuRgHU=
github.com/swaggo/http-swagger v0.0.0-20190614090009-c2865af9083e/go.mod h1:eycbshptIv+tqTMlLEaGC2noPNcetbrcYEelLafrIDI=
github.com/swaggo/swag v1.6.2 h1:WQMAtT/FmMBb7g0rAuHDhG3vvdtHKJ3WZ+Ssb0p4Y6E=
github.com/swaggo/swag v1.6.2/go.mod h1:YyZstMc22WYm6GEDx/CYWxq+faBbjQ5EqwQcrjREDBo=
github.com/ulikunitz/xz v0.5.4 h1:zATC2OoZ8H1TZll3FpbX+ikwmadbO699PE06cIkm9oU=
github.com/ulikunitz/xz v0.5.4/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
@@ -93,15 +131,29 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468 h1:fTfk6GjmihJbK0mSUFgPPgYpsdmApQ86Mcd4GuKax9U=
golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -5,7 +5,7 @@ metadata:
labels:
app: podinfo
spec:
minReadySeconds: 5
minReadySeconds: 3
revisionHistoryLimit: 5
progressDeadlineSeconds: 60
strategy:
@@ -19,26 +19,36 @@ spec:
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9797"
labels:
app: podinfo
spec:
containers:
- name: podinfod
image: quay.io/stefanprodan/podinfo:2.0.0
image: stefanprodan/podinfo:3.2.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9898
name: http
- name: http
containerPort: 9898
protocol: TCP
- name: http-metrics
containerPort: 9797
protocol: TCP
- name: grpc
containerPort: 9999
protocol: TCP
command:
- ./podinfo
- --port=9898
- --port-metrics=9797
- --grpc-port=9999
- --grpc-service-name=podinfo
- --level=info
- --random-delay=false
- --random-error=false
env:
- name: PODINFO_UI_COLOR
value: blue
value: "#34577c"
livenessProbe:
exec:
command:

View File

@@ -2,4 +2,3 @@ resources:
- hpa.yaml
- deployment.yaml
- service.yaml

View File

@@ -13,3 +13,7 @@ spec:
port: 9898
protocol: TCP
targetPort: http
- port: 9999
targetPort: grpc
protocol: TCP
name: grpc

View File

@@ -9,6 +9,14 @@ import (
"github.com/gorilla/mux"
)
// Chunked godoc
// @Summary Chunked transfer encoding
// @Description uses transfer-encoding type chunked to give a partial response and then waits for the specified period
// @Tags HTTP API
// @Accept json
// @Produce json
// @Router /chunked/{seconds} [get]
// @Success 200 {object} api.MapResponse
func (s *Server) chunkedHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)

View File

@@ -8,6 +8,14 @@ import (
"time"
)
// Delay godoc
// @Summary Delay
// @Description waits for the specified period
// @Tags HTTP API
// @Accept json
// @Produce json
// @Router /delay/{seconds} [get]
// @Success 200 {object} api.MapResponse
func (s *Server) delayHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)

594
pkg/api/docs/docs.go Normal file
View File

@@ -0,0 +1,594 @@
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag at
// 2019-08-07 15:52:23.918713 +0300 EEST m=+0.023438272
package docs
import (
"bytes"
"encoding/json"
"github.com/alecthomas/template"
"github.com/swaggo/swag"
)
var doc = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "Go microservice template for Kubernetes.",
"title": "Podinfo API",
"contact": {
"name": "Source Code",
"url": "https://github.com/stefanprodan/podinfo"
},
"license": {
"name": "MIT License",
"url": "https://github.com/stefanprodan/podinfo/blob/master/LICENSE"
},
"version": "2.0"
},
"host": "localhost:9898",
"basePath": "/",
"paths": {
"/": {
"get": {
"description": "renders podinfo UI",
"produces": [
"text/html"
],
"tags": [
"HTTP API"
],
"summary": "Index",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/api/echo": {
"post": {
"description": "forwards the call to the backend service and echos the posted content",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Echo",
"responses": {
"202": {
"description": "Accepted",
"schema": {
"type": "object",
"$ref": "#/definitions/api.MapResponse"
}
}
}
}
},
"/api/info": {
"get": {
"description": "returns the runtime information",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Runtime information",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.RuntimeResponse"
}
}
}
}
},
"/chunked/{seconds}": {
"get": {
"description": "uses transfer-encoding type chunked to give a partial response and then waits for the specified period",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Chunked transfer encoding",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.MapResponse"
}
}
}
}
},
"/delay/{seconds}": {
"get": {
"description": "waits for the specified period",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Delay",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.MapResponse"
}
}
}
}
},
"/env": {
"get": {
"description": "returns the environment variables as a JSON array",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Environment",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.ArrayResponse"
}
}
}
}
},
"/headers": {
"get": {
"description": "returns a JSON array with the request HTTP headers",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Headers",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.ArrayResponse"
}
}
}
}
},
"/healthz": {
"get": {
"description": "used by Kubernetes liveness probe",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Kubernetes"
],
"summary": "Liveness check",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/metrics": {
"get": {
"description": "returns HTTP requests duration and Go runtime metrics",
"produces": [
"text/plain"
],
"tags": [
"Kubernetes"
],
"summary": "Prometheus metrics",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/panic": {
"get": {
"description": "crashes the process with exit code 255",
"tags": [
"HTTP API"
],
"summary": "Panic"
}
},
"/readyz": {
"get": {
"description": "used by Kubernetes readiness probe",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Kubernetes"
],
"summary": "Readiness check",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/readyz/disable": {
"post": {
"description": "signals the Kubernetes LB to stop sending requests to this instance",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Kubernetes"
],
"summary": "Disable ready state",
"responses": {
"202": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/readyz/enable": {
"post": {
"description": "signals the Kubernetes LB that this instance is ready to receive traffic",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Kubernetes"
],
"summary": "Enable ready state",
"responses": {
"202": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/status/{code}": {
"get": {
"description": "sets the response status code to the specified code",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Status code",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.MapResponse"
}
}
}
}
},
"/store": {
"post": {
"description": "writes the posted content to disk at /data/hash and returns the SHA1 hash of the content",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Upload file",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.MapResponse"
}
}
}
}
},
"/store/{hash}": {
"get": {
"description": "returns the content of the file /data/hash if exists",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
],
"tags": [
"HTTP API"
],
"summary": "Download file",
"responses": {
"200": {
"description": "file",
"schema": {
"type": "string"
}
}
}
}
},
"/token": {
"post": {
"description": "issues a JWT token valid for one minute",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Generate JWT token",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.TokenResponse"
}
}
}
}
},
"/token/validate": {
"post": {
"description": "validates the JWT token",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Validate JWT token",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.TokenValidationResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "string"
}
}
}
}
},
"/version": {
"get": {
"description": "returns podinfo version and git commit hash",
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Version",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.MapResponse"
}
}
}
}
},
"/ws/echo": {
"post": {
"description": "echos content via websockets",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Echo over websockets",
"responses": {
"202": {
"description": "Accepted",
"schema": {
"type": "object",
"$ref": "#/definitions/api.MapResponse"
}
}
}
}
}
},
"definitions": {
"api.ArrayResponse": {
"type": "array",
"items": {}
},
"api.MapResponse": {
"type": "object",
"additionalProperties": {}
},
"api.RuntimeResponse": {
"type": "object",
"properties": {
"color": {
"type": "string"
},
"goarch": {
"type": "string"
},
"goos": {
"type": "string"
},
"hostname": {
"type": "string"
},
"message": {
"type": "string"
},
"num_cpu": {
"type": "string"
},
"num_goroutine": {
"type": "string"
},
"revision": {
"type": "string"
},
"runtime": {
"type": "string"
},
"version": {
"type": "string"
}
}
},
"api.TokenResponse": {
"type": "object",
"properties": {
"expires_at": {
"type": "string"
},
"token": {
"type": "string"
}
}
},
"api.TokenValidationResponse": {
"type": "object",
"properties": {
"expires_at": {
"type": "string"
},
"token_name": {
"type": "string"
}
}
}
}
}`
type swaggerInfo struct {
Version string
Host string
BasePath string
Schemes []string
Title string
Description string
}
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = swaggerInfo{Schemes: []string{"http", "https"}}
type s struct{}
func (s *s) ReadDoc() string {
t, err := template.New("swagger_info").Funcs(template.FuncMap{
"marshal": func(v interface{}) string {
a, _ := json.Marshal(v)
return string(a)
},
}).Parse(doc)
if err != nil {
return doc
}
var tpl bytes.Buffer
if err := t.Execute(&tpl, SwaggerInfo); err != nil {
return doc
}
return tpl.String()
}
func init() {
swag.Register(swag.Name, &s{})
}

542
pkg/api/docs/swagger.json Normal file
View File

@@ -0,0 +1,542 @@
{
"swagger": "2.0",
"info": {
"description": "Go microservice template for Kubernetes.",
"title": "Podinfo API",
"contact": {
"name": "Source Code",
"url": "https://github.com/stefanprodan/podinfo"
},
"license": {
"name": "MIT License",
"url": "https://github.com/stefanprodan/podinfo/blob/master/LICENSE"
},
"version": "2.0"
},
"host": "localhost:9898",
"basePath": "/",
"paths": {
"/": {
"get": {
"description": "renders podinfo UI",
"produces": [
"text/html"
],
"tags": [
"HTTP API"
],
"summary": "Index",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/api/echo": {
"post": {
"description": "forwards the call to the backend service and echos the posted content",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Echo",
"responses": {
"202": {
"description": "Accepted",
"schema": {
"type": "object",
"$ref": "#/definitions/api.MapResponse"
}
}
}
}
},
"/api/info": {
"get": {
"description": "returns the runtime information",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Runtime information",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.RuntimeResponse"
}
}
}
}
},
"/chunked/{seconds}": {
"get": {
"description": "uses transfer-encoding type chunked to give a partial response and then waits for the specified period",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Chunked transfer encoding",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.MapResponse"
}
}
}
}
},
"/delay/{seconds}": {
"get": {
"description": "waits for the specified period",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Delay",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.MapResponse"
}
}
}
}
},
"/env": {
"get": {
"description": "returns the environment variables as a JSON array",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Environment",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.ArrayResponse"
}
}
}
}
},
"/headers": {
"get": {
"description": "returns a JSON array with the request HTTP headers",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Headers",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.ArrayResponse"
}
}
}
}
},
"/healthz": {
"get": {
"description": "used by Kubernetes liveness probe",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Kubernetes"
],
"summary": "Liveness check",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/metrics": {
"get": {
"description": "returns HTTP requests duration and Go runtime metrics",
"produces": [
"text/plain"
],
"tags": [
"Kubernetes"
],
"summary": "Prometheus metrics",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/panic": {
"get": {
"description": "crashes the process with exit code 255",
"tags": [
"HTTP API"
],
"summary": "Panic"
}
},
"/readyz": {
"get": {
"description": "used by Kubernetes readiness probe",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Kubernetes"
],
"summary": "Readiness check",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/readyz/disable": {
"post": {
"description": "signals the Kubernetes LB to stop sending requests to this instance",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Kubernetes"
],
"summary": "Disable ready state",
"responses": {
"202": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/readyz/enable": {
"post": {
"description": "signals the Kubernetes LB that this instance is ready to receive traffic",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Kubernetes"
],
"summary": "Enable ready state",
"responses": {
"202": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/status/{code}": {
"get": {
"description": "sets the response status code to the specified code",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Status code",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.MapResponse"
}
}
}
}
},
"/store": {
"post": {
"description": "writes the posted content to disk at /data/hash and returns the SHA1 hash of the content",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Upload file",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.MapResponse"
}
}
}
}
},
"/store/{hash}": {
"get": {
"description": "returns the content of the file /data/hash if exists",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
],
"tags": [
"HTTP API"
],
"summary": "Download file",
"responses": {
"200": {
"description": "file",
"schema": {
"type": "string"
}
}
}
}
},
"/token": {
"post": {
"description": "issues a JWT token valid for one minute",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Generate JWT token",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.TokenResponse"
}
}
}
}
},
"/token/validate": {
"post": {
"description": "validates the JWT token",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Validate JWT token",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.TokenValidationResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "string"
}
}
}
}
},
"/version": {
"get": {
"description": "returns podinfo version and git commit hash",
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Version",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/api.MapResponse"
}
}
}
}
},
"/ws/echo": {
"post": {
"description": "echos content via websockets",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"HTTP API"
],
"summary": "Echo over websockets",
"responses": {
"202": {
"description": "Accepted",
"schema": {
"type": "object",
"$ref": "#/definitions/api.MapResponse"
}
}
}
}
}
},
"definitions": {
"api.ArrayResponse": {
"type": "array",
"items": {}
},
"api.MapResponse": {
"type": "object",
"additionalProperties": {}
},
"api.RuntimeResponse": {
"type": "object",
"properties": {
"color": {
"type": "string"
},
"goarch": {
"type": "string"
},
"goos": {
"type": "string"
},
"hostname": {
"type": "string"
},
"message": {
"type": "string"
},
"num_cpu": {
"type": "string"
},
"num_goroutine": {
"type": "string"
},
"revision": {
"type": "string"
},
"runtime": {
"type": "string"
},
"version": {
"type": "string"
}
}
},
"api.TokenResponse": {
"type": "object",
"properties": {
"expires_at": {
"type": "string"
},
"token": {
"type": "string"
}
}
},
"api.TokenValidationResponse": {
"type": "object",
"properties": {
"expires_at": {
"type": "string"
},
"token_name": {
"type": "string"
}
}
}
}
}

362
pkg/api/docs/swagger.yaml Normal file
View File

@@ -0,0 +1,362 @@
basePath: /
definitions:
api.ArrayResponse:
items: {}
type: array
api.MapResponse:
additionalProperties: {}
type: object
api.RuntimeResponse:
properties:
color:
type: string
goarch:
type: string
goos:
type: string
hostname:
type: string
message:
type: string
num_cpu:
type: string
num_goroutine:
type: string
revision:
type: string
runtime:
type: string
version:
type: string
type: object
api.TokenResponse:
properties:
expires_at:
type: string
token:
type: string
type: object
api.TokenValidationResponse:
properties:
expires_at:
type: string
token_name:
type: string
type: object
host: localhost:9898
info:
contact:
name: Source Code
url: https://github.com/stefanprodan/podinfo
description: Go microservice template for Kubernetes.
license:
name: MIT License
url: https://github.com/stefanprodan/podinfo/blob/master/LICENSE
title: Podinfo API
version: "2.0"
paths:
/:
get:
description: renders podinfo UI
produces:
- text/html
responses:
"200":
description: OK
schema:
type: string
summary: Index
tags:
- HTTP API
/api/echo:
post:
consumes:
- application/json
description: forwards the call to the backend service and echos the posted content
produces:
- application/json
responses:
"202":
description: Accepted
schema:
$ref: '#/definitions/api.MapResponse'
type: object
summary: Echo
tags:
- HTTP API
/api/info:
get:
consumes:
- application/json
description: returns the runtime information
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.RuntimeResponse'
type: object
summary: Runtime information
tags:
- HTTP API
/chunked/{seconds}:
get:
consumes:
- application/json
description: uses transfer-encoding type chunked to give a partial response
and then waits for the specified period
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.MapResponse'
type: object
summary: Chunked transfer encoding
tags:
- HTTP API
/delay/{seconds}:
get:
consumes:
- application/json
description: waits for the specified period
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.MapResponse'
type: object
summary: Delay
tags:
- HTTP API
/env:
get:
consumes:
- application/json
description: returns the environment variables as a JSON array
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.ArrayResponse'
type: object
summary: Environment
tags:
- HTTP API
/headers:
get:
consumes:
- application/json
description: returns a JSON array with the request HTTP headers
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.ArrayResponse'
type: object
summary: Headers
tags:
- HTTP API
/healthz:
get:
consumes:
- application/json
description: used by Kubernetes liveness probe
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: Liveness check
tags:
- Kubernetes
/metrics:
get:
description: returns HTTP requests duration and Go runtime metrics
produces:
- text/plain
responses:
"200":
description: OK
schema:
type: string
summary: Prometheus metrics
tags:
- Kubernetes
/panic:
get:
description: crashes the process with exit code 255
summary: Panic
tags:
- HTTP API
/readyz:
get:
consumes:
- application/json
description: used by Kubernetes readiness probe
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: Readiness check
tags:
- Kubernetes
/readyz/disable:
post:
consumes:
- application/json
description: signals the Kubernetes LB to stop sending requests to this instance
produces:
- application/json
responses:
"202":
description: OK
schema:
type: string
summary: Disable ready state
tags:
- Kubernetes
/readyz/enable:
post:
consumes:
- application/json
description: signals the Kubernetes LB that this instance is ready to receive
traffic
produces:
- application/json
responses:
"202":
description: OK
schema:
type: string
summary: Enable ready state
tags:
- Kubernetes
/status/{code}:
get:
consumes:
- application/json
description: sets the response status code to the specified code
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.MapResponse'
type: object
summary: Status code
tags:
- HTTP API
/store:
post:
consumes:
- application/json
description: writes the posted content to disk at /data/hash and returns the
SHA1 hash of the content
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.MapResponse'
type: object
summary: Upload file
tags:
- HTTP API
/store/{hash}:
get:
consumes:
- application/json
description: returns the content of the file /data/hash if exists
produces:
- text/plain
responses:
"200":
description: file
schema:
type: string
summary: Download file
tags:
- HTTP API
/token:
post:
consumes:
- application/json
description: issues a JWT token valid for one minute
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.TokenResponse'
type: object
summary: Generate JWT token
tags:
- HTTP API
/token/validate:
post:
consumes:
- application/json
description: validates the JWT token
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.TokenValidationResponse'
type: object
"401":
description: Unauthorized
schema:
type: string
summary: Validate JWT token
tags:
- HTTP API
/version:
get:
description: returns podinfo version and git commit hash
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.MapResponse'
type: object
summary: Version
tags:
- HTTP API
/ws/echo:
post:
consumes:
- application/json
description: echos content via websockets
produces:
- application/json
responses:
"202":
description: Accepted
schema:
$ref: '#/definitions/api.MapResponse'
type: object
summary: Echo over websockets
tags:
- HTTP API
swagger: "2.0"

View File

@@ -3,13 +3,23 @@ package api
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"sync"
"github.com/stefanprodan/podinfo/pkg/version"
"go.uber.org/zap"
)
// Echo godoc
// @Summary Echo
// @Description forwards the call to the backend service and echos the posted content
// @Tags HTTP API
// @Accept json
// @Produce json
// @Router /api/echo [post]
// @Success 202 {object} api.MapResponse
func (s *Server) echoHandler(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
@@ -18,59 +28,68 @@ func (s *Server) echoHandler(w http.ResponseWriter, r *http.Request) {
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
result := make([]string, len(s.config.BackendURL))
var wg sync.WaitGroup
wg.Add(len(s.config.BackendURL))
for i, b := range s.config.BackendURL {
go func(index int, backend string) {
defer wg.Done()
backendReq, err := http.NewRequest("POST", backend, bytes.NewReader(body))
if err != nil {
s.logger.Error("backend call failed", zap.Error(err), zap.String("url", backend))
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", backend))
result[index] = fmt.Sprintf("backend %v call failed %v", backend, err)
return
}
defer resp.Body.Close()
// copy error status from backend and exit
if resp.StatusCode >= 400 {
s.logger.Error("backend call failed", zap.Int("status", resp.StatusCode), zap.String("url", backend))
result[index] = fmt.Sprintf("backend %v response status code %v", backend, 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", backend))
result[index] = fmt.Sprintf("backend %v call failed %v", backend, err)
return
}
s.logger.Debug(
"payload received from backend",
zap.String("response", string(rbody)),
zap.String("url", backend))
result[index] = string(rbody)
}(i, b)
}
// 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))
wg.Wait()
w.Header().Set("X-Color", s.config.UIColor)
w.WriteHeader(http.StatusAccepted)
w.Write(rbody)
s.JSONResponse(w, r, result)
} else {
w.Header().Set("X-Color", s.config.UIColor)
w.WriteHeader(http.StatusAccepted)

View File

@@ -11,6 +11,14 @@ import (
var wsCon = websocket.Upgrader{}
// EchoWS godoc
// @Summary Echo over websockets
// @Description echos content via websockets
// @Tags HTTP API
// @Accept json
// @Produce json
// @Router /ws/echo [post]
// @Success 202 {object} api.MapResponse
// 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)

View File

@@ -6,6 +6,14 @@ import (
"os"
)
// Env godoc
// @Summary Environment
// @Description returns the environment variables as a JSON array
// @Tags HTTP API
// @Accept json
// @Produce json
// @Router /env [get]
// @Success 200 {object} api.ArrayResponse
func (s *Server) envHandler(w http.ResponseWriter, r *http.Request) {
s.JSONResponse(w, r, os.Environ())
}

View File

@@ -4,6 +4,14 @@ import (
"net/http"
)
// Headers godoc
// @Summary Headers
// @Description returns a JSON array with the request HTTP headers
// @Tags HTTP API
// @Accept json
// @Produce json
// @Router /headers [get]
// @Success 200 {object} api.ArrayResponse
func (s *Server) echoHeadersHandler(w http.ResponseWriter, r *http.Request) {
s.JSONResponse(w, r, r.Header)
}

View File

@@ -5,6 +5,14 @@ import (
"sync/atomic"
)
// Healthz godoc
// @Summary Liveness check
// @Description used by Kubernetes liveness probe
// @Tags Kubernetes
// @Accept json
// @Produce json
// @Router /healthz [get]
// @Success 200 {string} string "OK"
func (s *Server) healthzHandler(w http.ResponseWriter, r *http.Request) {
if atomic.LoadInt32(&healthy) == 1 {
s.JSONResponse(w, r, map[string]string{"status": "OK"})
@@ -13,6 +21,14 @@ func (s *Server) healthzHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}
// Readyz godoc
// @Summary Readiness check
// @Description used by Kubernetes readiness probe
// @Tags Kubernetes
// @Accept json
// @Produce json
// @Router /readyz [get]
// @Success 200 {string} string "OK"
func (s *Server) readyzHandler(w http.ResponseWriter, r *http.Request) {
if atomic.LoadInt32(&ready) == 1 {
s.JSONResponse(w, r, map[string]string{"status": "OK"})
@@ -21,11 +37,27 @@ func (s *Server) readyzHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}
// EnableReady godoc
// @Summary Enable ready state
// @Description signals the Kubernetes LB that this instance is ready to receive traffic
// @Tags Kubernetes
// @Accept json
// @Produce json
// @Router /readyz/enable [post]
// @Success 202 {string} string "OK"
func (s *Server) enableReadyHandler(w http.ResponseWriter, r *http.Request) {
atomic.StoreInt32(&ready, 1)
w.WriteHeader(http.StatusAccepted)
}
// DisableReady godoc
// @Summary Disable ready state
// @Description signals the Kubernetes LB to stop sending requests to this instance
// @Tags Kubernetes
// @Accept json
// @Produce json
// @Router /readyz/disable [post]
// @Success 202 {string} string "OK"
func (s *Server) disableReadyHandler(w http.ResponseWriter, r *http.Request) {
atomic.StoreInt32(&ready, 0)
w.WriteHeader(http.StatusAccepted)

View File

@@ -6,6 +6,13 @@ import (
"path"
)
// Index godoc
// @Summary Index
// @Description renders podinfo UI
// @Tags HTTP API
// @Produce html
// @Router / [get]
// @Success 200 {string} string "OK"
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 {
@@ -16,8 +23,10 @@ func (s *Server) indexHandler(w http.ResponseWriter, r *http.Request) {
data := struct {
Title string
Logo string
}{
Title: s.config.Hostname,
Logo: s.config.UILogo,
}
if err := tmpl.Execute(w, data); err != nil {

View File

@@ -9,22 +9,20 @@ import (
"github.com/stefanprodan/podinfo/pkg/version"
)
// Info godoc
// @Summary Runtime information
// @Description returns the runtime information
// @Tags HTTP API
// @Accept json
// @Produce json
// @Success 200 {object} api.RuntimeResponse
// @Router /api/info [get]
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"`
}{
data := RuntimeResponse{
Hostname: s.config.Hostname,
Version: version.VERSION,
Revision: version.REVISION,
Logo: s.config.UILogo,
Color: s.config.UIColor,
Message: s.config.UIMessage,
GOOS: runtime.GOOS,
@@ -36,3 +34,17 @@ func (s *Server) infoHandler(w http.ResponseWriter, r *http.Request) {
s.JSONResponse(w, r, data)
}
type RuntimeResponse struct {
Hostname string `json:"hostname"`
Version string `json:"version"`
Revision string `json:"revision"`
Color string `json:"color"`
Logo string `json:"logo"`
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"`
}

View File

@@ -47,6 +47,13 @@ func NewPrometheusMiddleware() *PrometheusMiddleware {
}
}
// Metrics godoc
// @Summary Prometheus metrics
// @Description returns HTTP requests duration and Go runtime metrics
// @Tags Kubernetes
// @Produce plain
// @Router /metrics [get]
// @Success 200 {string} string "OK"
func (p *PrometheusMiddleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
begin := time.Now()

View File

@@ -12,7 +12,7 @@ func NewMockServer() *Server {
Port: "9898",
HttpServerShutdownTimeout: 5 * time.Second,
HttpServerTimeout: 30 * time.Second,
BackendURL: "",
BackendURL: []string{},
ConfigPath: "/config",
DataPath: "/data",
HttpClientTimeout: 30 * time.Second,

View File

@@ -4,6 +4,11 @@ import (
"net/http"
)
// Panic godoc
// @Summary Panic
// @Description crashes the process with exit code 255
// @Tags HTTP API
// @Router /panic [get]
func (s *Server) panicHandler(w http.ResponseWriter, r *http.Request) {
s.logger.Panic("Panic command received")
}

View File

@@ -3,6 +3,10 @@ package api
import (
"context"
"fmt"
"github.com/swaggo/swag"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"net/http"
_ "net/http/pprof"
"os"
@@ -13,21 +17,43 @@ import (
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/viper"
_ "github.com/stefanprodan/podinfo/pkg/api/docs"
"github.com/stefanprodan/podinfo/pkg/fscache"
"github.com/swaggo/http-swagger"
"go.uber.org/zap"
)
// @title Podinfo API
// @version 2.0
// @description Go microservice template for Kubernetes.
// @contact.name Source Code
// @contact.url https://github.com/stefanprodan/podinfo
// @license.name MIT License
// @license.url https://github.com/stefanprodan/podinfo/blob/master/LICENSE
// @host localhost:9898
// @BasePath /
// @schemes http https
var (
healthy int32
ready int32
watcher *fscache.Watcher
)
type FluxConfig struct {
GitUrl string `mapstructure:"git-url"`
GitBranch string `mapstructure:"git-branch"`
}
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"`
BackendURL []string `mapstructure:"backend-url"`
UILogo string `mapstructure:"ui-logo"`
UIMessage string `mapstructure:"ui-message"`
UIColor string `mapstructure:"ui-color"`
UIPath string `mapstructure:"ui-path"`
@@ -36,8 +62,11 @@ type Config struct {
Port string `mapstructure:"port"`
PortMetrics int `mapstructure:"port-metrics"`
Hostname string `mapstructure:"hostname"`
H2C bool `mapstructure:"h2c"`
RandomDelay bool `mapstructure:"random-delay"`
RandomError bool `mapstructure:"random-error"`
Unhealthy bool `mapstructure:"unhealthy"`
Unready bool `mapstructure:"unready"`
JWTSecret string `mapstructure:"jwt-secret"`
}
@@ -83,6 +112,19 @@ func (s *Server) registerHandlers() {
s.router.HandleFunc("/ws/echo", s.echoWsHandler)
s.router.HandleFunc("/chunked", s.chunkedHandler)
s.router.HandleFunc("/chunked/{wait:[0-9]+}", s.chunkedHandler)
s.router.PathPrefix("/swagger/").Handler(httpSwagger.Handler(
httpSwagger.URL("/swagger/doc.json"),
))
s.router.PathPrefix("/swagger/").Handler(httpSwagger.Handler(
httpSwagger.URL("/swagger/doc.json"),
))
s.router.HandleFunc("/swagger.json", func(w http.ResponseWriter, r *http.Request) {
doc, err := swag.ReadDoc()
if err != nil {
s.logger.Error("swagger error", zap.Error(err), zap.String("path", "/swagger.json"))
}
w.Write([]byte(doc))
})
}
func (s *Server) registerMiddlewares() {
@@ -105,12 +147,19 @@ func (s *Server) ListenAndServe(stopCh <-chan struct{}) {
s.registerHandlers()
s.registerMiddlewares()
var handler http.Handler
if s.config.H2C {
handler = h2c.NewHandler(s.router, &http2.Server{})
} else {
handler = s.router
}
srv := &http.Server{
Addr: ":" + s.config.Port,
WriteTimeout: s.config.HttpServerTimeout,
ReadTimeout: s.config.HttpServerTimeout,
IdleTimeout: 2 * s.config.HttpServerTimeout,
Handler: s.router,
Handler: handler,
}
//s.printRoutes()
@@ -134,8 +183,12 @@ func (s *Server) ListenAndServe(stopCh <-chan struct{}) {
}()
// signal Kubernetes the server is ready to receive traffic
atomic.StoreInt32(&healthy, 1)
atomic.StoreInt32(&ready, 1)
if !s.config.Unhealthy {
atomic.StoreInt32(&healthy, 1)
}
if !s.config.Unready {
atomic.StoreInt32(&ready, 1)
}
// wait for SIGTERM or SIGINT
<-stopCh
@@ -206,3 +259,6 @@ func (s *Server) printRoutes() {
return nil
})
}
type ArrayResponse []string
type MapResponse map[string]string

View File

@@ -7,6 +7,14 @@ import (
"strconv"
)
// Status godoc
// @Summary Status code
// @Description sets the response status code to the specified code
// @Tags HTTP API
// @Accept json
// @Produce json
// @Router /status/{code} [get]
// @Success 200 {object} api.MapResponse
func (s *Server) statusHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)

View File

@@ -11,6 +11,14 @@ import (
"go.uber.org/zap"
)
// Store godoc
// @Summary Upload file
// @Description writes the posted content to disk at /data/hash and returns the SHA1 hash of the content
// @Tags HTTP API
// @Accept json
// @Produce json
// @Router /store [post]
// @Success 200 {object} api.MapResponse
func (s *Server) storeWriteHandler(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
@@ -29,6 +37,14 @@ func (s *Server) storeWriteHandler(w http.ResponseWriter, r *http.Request) {
s.JSONResponseCode(w, r, map[string]string{"hash": hash}, http.StatusAccepted)
}
// Store godoc
// @Summary Download file
// @Description returns the content of the file /data/hash if exists
// @Tags HTTP API
// @Accept json
// @Produce plain
// @Router /store/{hash} [get]
// @Success 200 {string} string "file"
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))

View File

@@ -17,6 +17,14 @@ type jwtCustomClaims struct {
jwt.StandardClaims
}
// Token godoc
// @Summary Generate JWT token
// @Description issues a JWT token valid for one minute
// @Tags HTTP API
// @Accept json
// @Produce json
// @Router /token [post]
// @Success 200 {object} api.TokenResponse
func (s *Server) tokenGenerateHandler(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
@@ -46,10 +54,7 @@ func (s *Server) tokenGenerateHandler(w http.ResponseWriter, r *http.Request) {
return
}
var result = struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
}{
var result = TokenResponse{
Token: t,
ExpiresAt: time.Unix(claims.StandardClaims.ExpiresAt, 0),
}
@@ -57,6 +62,15 @@ func (s *Server) tokenGenerateHandler(w http.ResponseWriter, r *http.Request) {
s.JSONResponse(w, r, result)
}
// TokenValidate godoc
// @Summary Validate JWT token
// @Description validates the JWT token
// @Tags HTTP API
// @Accept json
// @Produce json
// @Router /token/validate [post]
// @Success 200 {object} api.TokenValidationResponse
// @Failure 401 {string} string "Unauthorized"
// 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) {
@@ -87,10 +101,7 @@ func (s *Server) tokenValidateHandler(w http.ResponseWriter, r *http.Request) {
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"`
}{
var result = TokenValidationResponse{
TokenName: claims.Name,
ExpiresAt: time.Unix(claims.StandardClaims.ExpiresAt, 0),
}
@@ -100,3 +111,13 @@ func (s *Server) tokenValidateHandler(w http.ResponseWriter, r *http.Request) {
s.ErrorResponse(w, r, "Invalid authorization token", http.StatusUnauthorized)
}
}
type TokenResponse struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
}
type TokenValidationResponse struct {
TokenName string `json:"token_name"`
ExpiresAt time.Time `json:"expires_at"`
}

View File

@@ -6,6 +6,13 @@ import (
"github.com/stefanprodan/podinfo/pkg/version"
)
// Version godoc
// @Summary Version
// @Description returns podinfo version and git commit hash
// @Tags HTTP API
// @Produce json
// @Router /version [get]
// @Success 200 {object} api.MapResponse
func (s *Server) versionHandler(w http.ResponseWriter, r *http.Request) {
result := map[string]string{
"version": version.VERSION,

46
pkg/grpc/server.go Normal file
View File

@@ -0,0 +1,46 @@
package grpc
import (
"fmt"
"net"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
)
type Server struct {
logger *zap.Logger
config *Config
}
type Config struct {
Port int `mapstructure:"grpc-port"`
ServiceName string `mapstructure:"grpc-service-name"`
}
func NewServer(config *Config, logger *zap.Logger) (*Server, error) {
srv := &Server{
logger: logger,
config: config,
}
return srv, nil
}
func (s *Server) ListenAndServe() {
listener, err := net.Listen("tcp", fmt.Sprintf(":%v", s.config.Port))
if err != nil {
s.logger.Fatal("failed to listen", zap.Int("port", s.config.Port))
}
srv := grpc.NewServer()
server := health.NewServer()
grpc_health_v1.RegisterHealthServer(srv, server)
server.SetServingStatus(s.config.ServiceName, grpc_health_v1.HealthCheckResponse_SERVING)
if err := srv.Serve(listener); err != nil {
s.logger.Fatal("failed to serve", zap.Error(err))
}
}

View File

@@ -1,4 +1,4 @@
package version
var VERSION = "2.0.0"
var VERSION = "3.2.0"
var REVISION = "unknown"

View File

@@ -10,8 +10,11 @@
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
<style>
[v-cloak] {
display: none;
}
display: none;
}
.v-application .v-parallax {
height: 100vh !important;
}
</style>
</head>
<body>
@@ -19,14 +22,14 @@
<v-app dark>
<v-content>
<section>
<v-parallax id="parallax-hero" src="#" :class="info.color">
<v-parallax id="parallax-hero" :style="cuddleStyle" src="https://upload.wikimedia.org/wikipedia/commons/c/ca/1x1.png">
<v-layout
column
align-center
justify-center
class="white--text"
>
<img src="https://d33wubrfki0l68.cloudfront.net/33a12d8be0bc50be4738443101616e968c7afb8f/cba76/images/scalable.png" alt="kubernetes" height="200">
<img :src="info.logo" height="350">
<h1 class="white--text mb-2 display-1 text-xs-center">${ info.message }</h1>
<div class="subheading mb-3 text-xs-center">Served by <b>${ info.hostname }</b></div>
<v-btn
@@ -59,6 +62,7 @@
Powered
by <a class="white--text" href="https://github.com/stefanprodan/podinfo" target="_blank">podinfo</a>
version ${ info.version } revision ${ info.revision }
Swagger <a class="white--text" href="swagger/">docs</a>
</div>
</v-flex>
</v-layout>
@@ -85,20 +89,20 @@
tlColor1: 'grey',
tlName2: '',
tlColor2: 'grey',
cuddleStyle: {
backgroundColor: '#34577c'
},
}
},
mounted: function() {
document.getElementById('parallax-hero').style.height = '100vh'
},
created: function() {
this.getInfo();
this.timer = setInterval(this.getInfo, 3000)
},
methods: {
getInfo: function() {
var xhr = new XMLHttpRequest()
var self = this
xhr.open('GET', "api/info")
const xhr = new XMLHttpRequest();
let self = this;
xhr.open('GET', "api/info")
xhr.onload = function () {
data = JSON.parse(xhr.responseText)
if (self.info.version) {
@@ -107,22 +111,22 @@
}
}
self.info = data
self.info.color = parseInt(data.version.split('.').reverse()[0], 10) === 0
? 'blue'
: 'green'
self.info.color = data.color
self.cuddleStyle = {
backgroundColor: data.color
}
self.info.logo = data.logo
document.title = data.hostname
if ((self.calls % 2) === 0) {
self.tlColor1 = parseInt(data.version.split('.').reverse()[0], 10) === 0
? 'blue'
: 'green'
self.tlColor2 = 'grey'
self.tlName1 = data.version
} else {
self.tlColor1 = 'grey'
self.tlColor2 = parseInt(data.version.split('.').reverse()[0], 10) === 0
let verColor = (parseInt(data.version.split('.').reverse()[0], 10) % 2 === 0)
? 'blue'
: 'green'
if ((self.calls % 2) === 0) {
self.tlColor2 += ' lighten-2'
self.tlColor1 = verColor
self.tlName1 = data.version
} else {
self.tlColor1 += ' lighten-2'
self.tlColor2 = verColor
self.tlName2 = data.version
}
self.calls++