Compare commits

...

46 Commits
v0.x ... v1.2.0

Author SHA1 Message Date
Stefan Prodan
02e5f233d0 Release v1.2.0 2018-09-11 22:18:58 +03:00
Stefan Prodan
b89f46ac04 Add websocket client command to CLI 2018-09-11 22:14:59 +03:00
Stefan Prodan
59cd692141 Add websocket echo handler 2018-09-11 22:13:54 +03:00
Stefan Prodan
bcd61428d1 Import gorilla/websocket 2018-09-11 22:12:37 +03:00
Stefan Prodan
f8ec9c0947 Fix multi-arch docker push 2018-09-11 18:04:13 +03:00
Stefan Prodan
6c98fbf1f4 Add JWT token issue and validate handlers 2018-09-10 11:36:11 +03:00
Stefan Prodan
54f6d9f74d Add env handler 2018-09-10 01:29:49 +03:00
Stefan Prodan
1d35304d9d OpenfaaS canary deployments 2018-09-09 16:38:37 +03:00
Stefan Prodan
457a56f71a Publish podinfo CLI to GitHub with goreleaser 2018-09-09 12:38:23 +03:00
Stefan Prodan
fbcab6cf56 Upgrade to go 1.11 and alpine 3.8 2018-09-09 12:37:58 +03:00
Stefan Prodan
0126282669 add goreleaser 2018-09-08 19:05:37 +03:00
Stefan Prodan
ff1fb39f43 Release v1.1.0
- add podinfo CLI to Quay docker image
- use podinfo CLI for health checks (Istio mTLS support)
2018-09-08 11:38:48 +03:00
Stefan Prodan
84f0e1c9e2 Add CLI check certificate 2018-09-07 15:26:07 +03:00
Stefan Prodan
3eb4cc90f9 Add CLI to Quay docker image 2018-09-07 14:55:24 +03:00
Stefan Prodan
b6c3d36bde Add CLI check tcp command 2018-09-07 14:54:55 +03:00
Stefan Prodan
a8a85e6aae Add CLI version cmd 2018-09-07 14:54:35 +03:00
Stefan Prodan
79b2d784bf Add OpenFaaS Istio guide WIP 2018-09-07 13:38:39 +03:00
Stefan Prodan
bfd35f6cc0 Make health checks compatible with Istio mTLS 2018-09-07 13:38:18 +03:00
Stefan Prodan
f1775ba090 Add podinfo CLI WIP 2018-09-07 13:37:40 +03:00
Stefan Prodan
7a2d59de8e Add OpenFaaS Istio mTLS and policies 2018-09-05 15:38:53 +03:00
Stefan Prodan
8191871761 Add Istio A/B test dashboard 2018-09-05 15:38:24 +03:00
Stefan Prodan
36bb719b1c Bump version to 1.0.1 2018-08-27 16:26:30 +01:00
Stefan Prodan
ecd204b15e Release v1.0.0 2018-08-22 00:57:53 +03:00
Stefan Prodan
979fd669df Use gorilla mux route name as Prometheus path label 2018-08-21 15:19:21 +03:00
Stefan Prodan
feac686e60 Release v1.0.0-beta.1 2018-08-21 12:03:34 +03:00
Stefan Prodan
d362dc5f81 Set env var prefix to PODINFO 2018-08-21 11:58:37 +03:00
Stefan Prodan
593ccaa0cd Add random delay and errors middleware 2018-08-21 03:12:20 +03:00
Stefan Prodan
0f098cf0f1 Add config file support 2018-08-21 02:02:47 +03:00
Stefan Prodan
2ddbc03371 Replace zerolog with zap 2018-08-21 02:01:26 +03:00
Stefan Prodan
f2d95bbf80 Add logging middleware and log level option 2018-08-20 17:03:07 +03:00
Stefan Prodan
7d18ec68b3 Use plag, viper and zap 2018-08-20 11:30:18 +03:00
Stefan Prodan
774d34c1dd Rewrite HTTP server with gorilla mux 2018-08-20 11:29:11 +03:00
Stefan Prodan
f13d006993 Add Kubernetes probes handlers 2018-08-20 11:28:06 +03:00
Stefan Prodan
aeeb146c2a Add UI handler 2018-08-20 11:27:40 +03:00
Stefan Prodan
11bd74eff2 Add local storage read/write handler 2018-08-20 11:27:08 +03:00
Stefan Prodan
af6d11fd33 Add panic handler 2018-08-20 11:26:24 +03:00
Stefan Prodan
49746fe2fb Add fscache reader handler 2018-08-20 11:26:08 +03:00
Stefan Prodan
da24d729bb Add runtime info handler 2018-08-20 11:25:36 +03:00
Stefan Prodan
449fcca3a9 Add HTTP status code handler 2018-08-20 11:25:15 +03:00
Stefan Prodan
2b0a742974 Add echo headers handler 2018-08-20 11:24:49 +03:00
Stefan Prodan
153f4dce45 Add echo handler with backend propagation 2018-08-20 11:24:23 +03:00
Stefan Prodan
4c8d11cc3e Add delay handler 2018-08-20 11:23:45 +03:00
Stefan Prodan
08415ce2ce Add version handler 2018-08-20 11:23:13 +03:00
Stefan Prodan
d26b7a96d9 Add UI index handler 2018-08-20 11:22:48 +03:00
Stefan Prodan
3c897b8bd7 Rename git commit to revision 2018-08-20 11:21:51 +03:00
Stefan Prodan
511ab87a18 Update deps for v1.0 2018-08-20 11:20:56 +03:00
630 changed files with 94187 additions and 11754 deletions

1
.gitignore vendored
View File

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

21
.goreleaser.yml Normal file
View File

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

View File

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

View File

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

281
Gopkg.lock generated
View File

@@ -3,96 +3,325 @@
[[projects]]
branch = "master"
digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d"
name = "github.com/beorn7/perks"
packages = ["quantile"]
revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9"
pruneopts = "UT"
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
[[projects]]
digest = "1:b95738a1e6ace058b5b8544303c0871fc01d224ef0d672f778f696265d0f2917"
name = "github.com/chzyer/readline"
packages = ["."]
pruneopts = "UT"
revision = "62c6fe6193755f722b8b8788aa7357be55a50ff1"
version = "v1.4"
[[projects]]
digest = "1:76dc72490af7174349349838f2fe118996381b31ea83243812a97e5a0fd5ed55"
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
pruneopts = "UT"
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[[projects]]
digest = "1:865079840386857c809b72ce300be7580cb50d3d3129ce11bf9aa6ca2bc1934a"
name = "github.com/fatih/color"
packages = ["."]
pruneopts = "UT"
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
version = "v1.7.0"
[[projects]]
digest = "1:abeb38ade3f32a92943e5be54f55ed6d6e3b6602761d74b4aab4c9dd45c18abd"
name = "github.com/fsnotify/fsnotify"
packages = ["."]
pruneopts = "UT"
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
version = "v1.4.7"
[[projects]]
digest = "1:97df918963298c287643883209a2c3f642e6593379f97ab400c2a2e219ab647d"
name = "github.com/golang/protobuf"
packages = ["proto"]
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
version = "v1.0.0"
pruneopts = "UT"
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
version = "v1.2.0"
[[projects]]
digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1"
name = "github.com/gorilla/context"
packages = ["."]
pruneopts = "UT"
revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42"
version = "v1.1.1"
[[projects]]
digest = "1:e73f5b0152105f18bc131fba127d9949305c8693f8a762588a82a48f61756f5f"
name = "github.com/gorilla/mux"
packages = ["."]
pruneopts = "UT"
revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf"
version = "v1.6.2"
[[projects]]
digest = "1:7b5c6e2eeaa9ae5907c391a91c132abfd5c9e8a784a341b5625e750c67e6825d"
name = "github.com/gorilla/websocket"
packages = ["."]
pruneopts = "UT"
revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d"
version = "v1.4.0"
[[projects]]
branch = "master"
digest = "1:a361611b8c8c75a1091f00027767f7779b29cb37c456a71b8f2604c88057ab40"
name = "github.com/hashicorp/hcl"
packages = [
".",
"hcl/ast",
"hcl/parser",
"hcl/printer",
"hcl/scanner",
"hcl/strconv",
"hcl/token",
"json/parser",
"json/scanner",
"json/token",
]
pruneopts = "UT"
revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168"
[[projects]]
digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
name = "github.com/inconshreveable/mousetrap"
packages = ["."]
pruneopts = "UT"
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
version = "v1.0"
[[projects]]
digest = "1:c568d7727aa262c32bdf8a3f7db83614f7af0ed661474b24588de635c20024c7"
name = "github.com/magiconair/properties"
packages = ["."]
pruneopts = "UT"
revision = "c2353362d570a7bfa228149c62842019201cfb71"
version = "v1.8.0"
[[projects]]
digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67"
name = "github.com/mattn/go-colorable"
packages = ["."]
pruneopts = "UT"
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
digest = "1:0981502f9816113c9c8c4ac301583841855c8cf4da8c72f696b3ebedf6d0e4e5"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = "UT"
revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c"
version = "v0.0.4"
[[projects]]
digest = "1:ff5ebae34cfbf047d505ee150de27e60570e8c394b3b8fdbb720ff6ac71985fc"
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
version = "v1.0.0"
pruneopts = "UT"
revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c"
version = "v1.0.1"
[[projects]]
name = "github.com/pkg/errors"
branch = "master"
digest = "1:5ab79470a1d0fb19b041a624415612f8236b3c06070161a910562f2b2d064355"
name = "github.com/mitchellh/mapstructure"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
pruneopts = "UT"
revision = "f15292f7a699fcc1a38a80977f80a046874ba8ac"
[[projects]]
digest = "1:95741de3af260a92cc5c7f3f3061e85273f5a81b5db20d4bd68da74bd521675e"
name = "github.com/pelletier/go-toml"
packages = ["."]
pruneopts = "UT"
revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194"
version = "v1.2.0"
[[projects]]
digest = "1:d14a5f4bfecf017cb780bdde1b6483e5deb87e12c332544d2c430eda58734bcb"
name = "github.com/prometheus/client_golang"
packages = [
"prometheus",
"prometheus/promhttp"
"prometheus/promhttp",
]
pruneopts = "UT"
revision = "c5b7fccd204277076155f10851dad72b76a49317"
version = "v0.8.0"
[[projects]]
branch = "master"
digest = "1:2d5cd61daa5565187e1d96bae64dbbc6080dacf741448e9629c64fd93203b0d4"
name = "github.com/prometheus/client_model"
packages = ["go"]
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
pruneopts = "UT"
revision = "5c3871d89910bfb32f5fcab2aa4b9ec68e65a99f"
[[projects]]
branch = "master"
digest = "1:63b68062b8968092eb86bedc4e68894bd096ea6b24920faca8b9dcf451f54bb5"
name = "github.com/prometheus/common"
packages = [
"expfmt",
"internal/bitbucket.org/ww/goautoneg",
"model"
"model",
]
revision = "e4aa40a9169a88835b849a6efb71e05dc04b88f0"
pruneopts = "UT"
revision = "c7de2306084e37d54b8be01f3541a8464345e9a5"
[[projects]]
branch = "master"
digest = "1:8c49953a1414305f2ff5465147ee576dd705487c35b15918fcd4efdc0cb7a290"
name = "github.com/prometheus/procfs"
packages = [
".",
"internal/util",
"nfs",
"xfs"
"xfs",
]
revision = "54d17b57dd7d4a3aa092476596b3f8a933bde349"
pruneopts = "UT"
revision = "05ee40e3a273f7245e8777337fc7b46e533a9a92"
[[projects]]
name = "github.com/rs/zerolog"
digest = "1:bd1ae00087d17c5a748660b8e89e1043e1e5479d0fea743352cda2f8dd8c4f84"
name = "github.com/spf13/afero"
packages = [
".",
"internal/cbor",
"internal/json",
"log"
"mem",
]
revision = "77db4b4f350e31be66a57c332acb7721cf9ff9bb"
version = "v1.8.0"
pruneopts = "UT"
revision = "787d034dfe70e44075ccc060d346146ef53270ad"
version = "v1.1.1"
[[projects]]
digest = "1:516e71bed754268937f57d4ecb190e01958452336fa73dbac880894164e91c1f"
name = "github.com/spf13/cast"
packages = ["."]
pruneopts = "UT"
revision = "8965335b8c7107321228e3e3702cab9832751bac"
version = "v1.2.0"
[[projects]]
digest = "1:645cabccbb4fa8aab25a956cbcbdf6a6845ca736b2c64e197ca7cbb9d210b939"
name = "github.com/spf13/cobra"
packages = ["."]
pruneopts = "UT"
revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385"
version = "v0.0.3"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix"]
revision = "bd9dbc187b6e1dacfdd2722a87e83093c2d7bd6e"
digest = "1:8a020f916b23ff574845789daee6818daf8d25a4852419aae3f0b12378ba432a"
name = "github.com/spf13/jwalterweatherman"
packages = ["."]
pruneopts = "UT"
revision = "14d3d4c518341bea657dd8a226f5121c0ff8c9f2"
[[projects]]
digest = "1:dab83a1bbc7ad3d7a6ba1a1cc1760f25ac38cdf7d96a5cdd55cd915a4f5ceaf9"
name = "github.com/spf13/pflag"
packages = ["."]
pruneopts = "UT"
revision = "9a97c102cda95a86cec2345a6f09f55a939babf5"
version = "v1.0.2"
[[projects]]
digest = "1:4fc8a61287ccfb4286e1ca5ad2ce3b0b301d746053bf44ac38cf34e40ae10372"
name = "github.com/spf13/viper"
packages = ["."]
pruneopts = "UT"
revision = "907c19d40d9a6c9bb55f040ff4ae45271a4754b9"
version = "v1.1.0"
[[projects]]
digest = "1:3c1a69cdae3501bf75e76d0d86dc6f2b0a7421bc205c0cb7b96b19eed464a34d"
name = "go.uber.org/atomic"
packages = ["."]
pruneopts = "UT"
revision = "1ea20fb1cbb1cc08cbd0d913a96dead89aa18289"
version = "v1.3.2"
[[projects]]
digest = "1:60bf2a5e347af463c42ed31a493d817f8a72f102543060ed992754e689805d1a"
name = "go.uber.org/multierr"
packages = ["."]
pruneopts = "UT"
revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a"
version = "v1.1.0"
[[projects]]
digest = "1:c52caf7bd44f92e54627a31b85baf06a68333a196b3d8d241480a774733dcf8b"
name = "go.uber.org/zap"
packages = [
".",
"buffer",
"internal/bufferpool",
"internal/color",
"internal/exit",
"zapcore",
]
pruneopts = "UT"
revision = "ff33455a0e382e8a81d14dd7c922020b6b5e7982"
version = "v1.9.1"
[[projects]]
branch = "master"
digest = "1:e3e7f51633f98fce9404940f74dfd65cf306ee173ddf5a8b247c9c830e42b38a"
name = "golang.org/x/sys"
packages = ["unix"]
pruneopts = "UT"
revision = "1a700e749ce29638d0bbcb531cce1094ea096bd3"
[[projects]]
digest = "1:8029e9743749d4be5bc9f7d42ea1659471767860f0cdc34d37c3111bd308a295"
name = "golang.org/x/text"
packages = [
"internal/gen",
"internal/triegen",
"internal/ucd",
"transform",
"unicode/cldr",
"unicode/norm",
]
pruneopts = "UT"
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202"
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "7f97868eec74b32b0982dd158a51a446d1da7eb5"
version = "v2.1.1"
pruneopts = "UT"
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "1b88c0a618973c53f6b715dd51ad99f2952baf09c4d752bc8a985d26439a739c"
input-imports = [
"github.com/chzyer/readline",
"github.com/dgrijalva/jwt-go",
"github.com/fatih/color",
"github.com/fsnotify/fsnotify",
"github.com/gorilla/mux",
"github.com/gorilla/websocket",
"github.com/prometheus/client_golang/prometheus",
"github.com/prometheus/client_golang/prometheus/promhttp",
"github.com/spf13/cobra",
"github.com/spf13/pflag",
"github.com/spf13/viper",
"go.uber.org/zap",
"go.uber.org/zap/zapcore",
]
solver-name = "gps-cdcl"
solver-version = 1

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

478
docs/8-istio-openfaas.md Normal file
View File

@@ -0,0 +1,478 @@
# OpenFaaS + Istio
### Install Istio
Download latest release:
```bash
curl -L https://git.io/getLatestIstio | sh -
```
Configure Istio with Prometheus, Jaeger and cert-manager:
```yaml
global:
nodePort: false
proxy:
includeIPRanges: "10.28.0.0/14,10.7.240.0/20"
ingress:
enabled: false
sidecarInjectorWebhook:
enabled: true
enableNamespacesByDefault: false
gateways:
enabled: true
grafana:
enabled: true
prometheus:
enabled: true
servicegraph:
enabled: true
tracing:
enabled: true
certmanager:
enabled: true
```
Save the above file as `istio-of.yaml` and install Istio with Helm:
```bash
helm upgrade --install istio ./install/kubernetes/helm/istio \
--namespace=istio-system \
-f ./istio-of.yaml
```
### Configure Istio Gateway with LE certs
Istio Gateway:
```yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: public-gateway
namespace: istio-system
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"
tls:
httpsRedirect: true
- port:
number: 443
name: https
protocol: HTTPS
hosts:
- "*"
tls:
mode: SIMPLE
privateKey: /etc/istio/ingressgateway-certs/tls.key
serverCertificate: /etc/istio/ingressgateway-certs/tls.crt
```
Find the gateway public IP:
```bash
IP=$(kubectl -n istio-system describe svc/istio-ingressgateway | grep 'Ingress' | awk '{print $NF}')
```
Create a zone in GCP Cloud DNS with the following records:
```bash
istio.example.com. A $IP
*.istio.example.com. A $IP
```
Create a service account with Cloud DNS admin role (replace `my-gcp-project` with your project ID):
```bash
GCP_PROJECT=my-gcp-project
gcloud iam service-accounts create dns-admin \
--display-name=dns-admin \
--project=${GCP_PROJECT}
gcloud iam service-accounts keys create ./gcp-dns-admin.json \
--iam-account=dns-admin@${GCP_PROJECT}.iam.gserviceaccount.com \
--project=${GCP_PROJECT}
gcloud projects add-iam-policy-binding ${GCP_PROJECT} \
--member=serviceAccount:dns-admin@${GCP_PROJECT}.iam.gserviceaccount.com \
--role=roles/dns.admin
```
Create a Kubernetes secret with the GCP Cloud DNS admin key:
```bash
kubectl create secret generic cert-manager-credentials \
--from-file=./gcp-dns-admin.json \
--namespace=istio-system
```
LE issuer for GCP Cloud DNS:
```yaml
apiVersion: certmanager.k8s.io/v1alpha1
kind: Issuer
metadata:
name: letsencrypt-prod
namespace: istio-system
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: email@example.com
privateKeySecretRef:
name: letsencrypt-prod
dns01:
providers:
- name: cloud-dns
clouddns:
serviceAccountSecretRef:
name: cert-manager-credentials
key: gcp-dns-admin.json
project: my-gcp-project
```
Wildcard cert:
```yaml
apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
name: istio-gateway
namespace: istio-system
spec:
secretname: istio-ingressgateway-certs
issuerRef:
name: letsencrypt-prod
commonName: "*.istio.example.com"
dnsNames:
- istio.example.com
acme:
config:
- dns01:
provider: cloud-dns
domains:
- "*.istio.example.com"
- "istio.example.com"
```
### Configure OpenFaaS mTLS and access policies
Create the OpenFaaS namespaces:
```yaml
apiVersion: v1
kind: Namespace
metadata:
labels:
istio-injection: enabled
name: openfaas
---
apiVersion: v1
kind: Namespace
metadata:
labels:
istio-injection: enabled
name: openfaas-fn
```
Create an Istio virtual service for OpenFaaS Gateway:
```yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: gateway
namespace: openfaas
spec:
hosts:
- "openfaas.istio.example.com"
gateways:
- public-gateway.istio-system.svc.cluster.local
http:
- route:
- destination:
host: gateway
timeout: 30s
```
Enable mTLS on `openfaas` namespace:
```yaml
apiVersion: authentication.istio.io/v1alpha1
kind: Policy
metadata:
name: default
namespace: openfaas
spec:
peers:
- mtls: {}
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: default
namespace: openfaas
spec:
host: "*.openfaas.svc.cluster.local"
trafficPolicy:
tls:
mode: ISTIO_MUTUAL
```
Allow plaintext traffic to OpenFaaS Gateway:
```yaml
apiVersion: authentication.istio.io/v1alpha1
kind: Policy
metadata:
name: permissive
namespace: openfaas
spec:
targets:
- name: gateway
peers:
- mtls:
mode: PERMISSIVE
```
Enable mTLS on `openfaas-fn` namespace:
```yaml
apiVersion: authentication.istio.io/v1alpha1
kind: Policy
metadata:
name: default
namespace: openfaas-fn
spec:
peers:
- mtls: {}
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: default
namespace: openfaas-fn
spec:
host: "*.openfaas-fn.svc.cluster.local"
trafficPolicy:
tls:
mode: ISTIO_MUTUAL
```
Deny access to OpenFaaS core services from the `openfaas-fn` namespace except for system functions:
```yaml
apiVersion: config.istio.io/v1alpha2
kind: denier
metadata:
name: denyhandler
namespace: openfaas
spec:
status:
code: 7
message: Not allowed
---
apiVersion: config.istio.io/v1alpha2
kind: checknothing
metadata:
name: denyrequest
namespace: openfaas
spec:
---
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: denyopenfaasfn
namespace: openfaas
spec:
match: destination.namespace == "openfaas" && source.namespace == "openfaas-fn" && source.labels["role"] != "openfaas-system"
actions:
- handler: denyhandler.denier
instances: [ denyrequest.checknothing ]
```
Deny access to functions except for OpenFaaS core services:
```yaml
apiVersion: config.istio.io/v1alpha2
kind: denier
metadata:
name: denyhandler
namespace: openfaas-fn
spec:
status:
code: 7
message: Not allowed
---
apiVersion: config.istio.io/v1alpha2
kind: checknothing
metadata:
name: denyrequest
namespace: openfaas-fn
spec:
---
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
name: denyopenfaasfn
namespace: openfaas-fn
spec:
match: destination.namespace == "openfaas-fn" && source.namespace != "openfaas" && source.labels["role"] != "openfaas-system"
actions:
- handler: denyhandler.denier
instances: [ denyrequest.checknothing ]
```
### Install OpenFaaS
Add the OpenFaaS `helm` chart:
```bash
$ helm repo add openfaas https://openfaas.github.io/faas-netes/
```
Create a secret named `basic-auth` in the `openfaas` namespace:
```bash
# generate a random password
password=$(head -c 12 /dev/urandom | shasum| cut -d' ' -f1)
kubectl -n openfaas create secret generic basic-auth \
--from-literal=basic-auth-user=admin \
--from-literal=basic-auth-password=$password
```
Install OpenFaaS with Helm:
```bash
helm upgrade --install openfaas ./chart/openfaas \
--namespace openfaas \
--set functionNamespace=openfaas-fn \
--set operator.create=true \
--set securityContext=true \
--set basic_auth=true \
--set exposeServices=false \
--set operator.createCRD=true
```
Wait for OpenFaaS Gateway to come online:
```bash
watch curl -v http://openfaas.istio.example.com/heathz
```
Save your credentials in faas-cli store:
```bash
echo $password | faas-cli login -g https://openfaas.istio.example.com -u admin --password-stdin
```
### Canary deployments for OpenFaaS functions
Create a general available release for the `env` function version 1.0.0:
```yaml
apiVersion: openfaas.com/v1alpha2
kind: Function
metadata:
name: env
namespace: openfaas-fn
spec:
name: env
image: stefanprodan/of-env:1.0.0
resources:
requests:
memory: "32Mi"
cpu: "10m"
limits:
memory: "64Mi"
cpu: "100m"
```
Create a canary release for version 1.1.0:
```yaml
apiVersion: openfaas.com/v1alpha2
kind: Function
metadata:
name: env-canaray
namespace: openfaas-fn
spec:
name: env-canaray
image: stefanprodan/of-env:1.1.0
resources:
requests:
memory: "32Mi"
cpu: "10m"
limits:
memory: "64Mi"
cpu: "100m"
```
Create an Istio virtual service with 10% traffic going to canary:
```yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: env
namespace: openfaas-fn
spec:
hosts:
- env
http:
- route:
- destination:
host: env
weight: 90
- destination:
host: env-canary
weight: 10
timeout: 30s
```
Test traffic routing (one in ten calls should hit the canary release):
```bash
while true; do sleep 1; curl -sS https://openfaas.istio.example.com/function/env | grep HOSTNAME; done
HOSTNAME=env-59bf48fb9d-cjsjw
HOSTNAME=env-59bf48fb9d-cjsjw
HOSTNAME=env-59bf48fb9d-cjsjw
HOSTNAME=env-59bf48fb9d-cjsjw
HOSTNAME=env-59bf48fb9d-cjsjw
HOSTNAME=env-59bf48fb9d-cjsjw
HOSTNAME=env-59bf48fb9d-cjsjw
HOSTNAME=env-59bf48fb9d-cjsjw
HOSTNAME=env-59bf48fb9d-cjsjw
HOSTNAME=env-canary-5dffdf4458-4vnn2
```
Tracing the general available release with Jaeger:
![ga-trace](https://github.com/stefanprodan/k8s-podinfo/blob/master/docs/screens/openfaas-istio-ga-trace.png)
Tracing the canary release:
![canary-trace](https://github.com/stefanprodan/k8s-podinfo/blob/master/docs/screens/openfaas-istio-canary-trace.png)
Monitor ga vs canary success rate and latency with Prometheus and Grafana:
![canary-prom](https://github.com/stefanprodan/k8s-podinfo/blob/master/docs/screens/openfaas-istio-canary-prom.png)

Binary file not shown.

Binary file not shown.

View File

@@ -3,9 +3,9 @@ entries:
ambassador:
- apiVersion: v1
appVersion: 0.29.0
created: 2018-08-17T18:44:47.405027259+03:00
created: 2018-09-11T22:17:53.254801678+03:00
description: A Helm chart for Datawire Ambassador
digest: a30c8cb38e696b09fda8269ad8465ce6fec6100cfc108ca85ecbc85913ca5c7f
digest: 5d8b6d4f129fed12c005a0f8ff022d4a1aead489498c244f4764e0b0c3104477
engine: gotpl
maintainers:
- email: stefanprodan@users.noreply.github.com
@@ -19,9 +19,9 @@ entries:
grafana:
- apiVersion: v1
appVersion: "1.0"
created: 2018-08-17T18:44:47.405800273+03:00
created: 2018-09-11T22:17:53.255623303+03:00
description: A Helm chart for Kubernetes
digest: abdcadc5cddcb7c015aa5bb64e59bfa246774ad9243b3eb3c2a814abb38f2776
digest: e5c4c01b886ca55c85adf3ff95cef5f6dd603833d7dae02e2c6da4235728da75
name: grafana
urls:
- https://stefanprodan.github.io/k8s-podinfo/grafana-0.1.0.tgz
@@ -29,9 +29,9 @@ entries:
loadtest:
- apiVersion: v1
appVersion: "1.0"
created: 2018-08-17T18:44:47.406037468+03:00
created: 2018-09-11T22:17:53.255836039+03:00
description: Hey load test Helm chart for Kubernetes
digest: fd3c3cd1eafa9d496250356aa52de1430f3467535fc2972eba5c5c392714eb1f
digest: 5a1bf6cba24ada79e10af0b3780dacf64e4c10283fff47165010ee868ed02482
name: loadtest
urls:
- https://stefanprodan.github.io/k8s-podinfo/loadtest-0.1.0.tgz
@@ -39,17 +39,65 @@ entries:
ngrok:
- apiVersion: v1
appVersion: "1.0"
created: 2018-08-17T18:44:47.406355266+03:00
created: 2018-09-11T22:17:53.256149474+03:00
description: A Ngrok Helm chart for Kubernetes
digest: 7bf5ed2ef63ccd5efb76bcd9a086b04816a162c51d6ab592bccf58c283acd2ea
digest: 989439d77088b2d3092b0ac0c65a4835335ec8194b9dbf36b2347c9a3d28944f
name: ngrok
urls:
- https://stefanprodan.github.io/k8s-podinfo/ngrok-0.1.0.tgz
version: 0.1.0
podinfo:
- apiVersion: v1
appVersion: 1.2.0
created: 2018-09-11T22:17:53.262775711+03:00
description: Podinfo Helm chart for Kubernetes
digest: 106e5c22922b6953381f9c73929d320128536c1a68c972323b860af5aa9182fe
engine: gotpl
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
- email: stefanprodan@users.noreply.github.com
name: stefanprodan
name: podinfo
sources:
- https://github.com/stefanprodan/k8s-podinfo
urls:
- https://stefanprodan.github.io/k8s-podinfo/podinfo-1.2.0.tgz
version: 1.2.0
- apiVersion: v1
appVersion: 1.1.0
created: 2018-09-11T22:17:53.261768578+03:00
description: Podinfo Helm chart for Kubernetes
digest: 973d60c629d7ae476776098ad6b654d78d91f91b3faa8bc016b94c73c42be094
engine: gotpl
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
- email: stefanprodan@users.noreply.github.com
name: stefanprodan
name: podinfo
sources:
- https://github.com/stefanprodan/k8s-podinfo
urls:
- https://stefanprodan.github.io/k8s-podinfo/podinfo-1.1.0.tgz
version: 1.1.0
- apiVersion: v1
appVersion: 1.0.0
created: 2018-09-11T22:17:53.260888709+03:00
description: Podinfo Helm chart for Kubernetes
digest: 82068727ba5b552341b14a980e954e27a8517f0ef76aab314c160b0f075e6de4
engine: gotpl
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
- email: stefanprodan@users.noreply.github.com
name: stefanprodan
name: podinfo
sources:
- https://github.com/stefanprodan/k8s-podinfo
urls:
- https://stefanprodan.github.io/k8s-podinfo/podinfo-1.0.0.tgz
version: 1.0.0
- apiVersion: v1
appVersion: 0.6.0
created: 2018-08-17T18:44:47.410049875+03:00
created: 2018-09-11T22:17:53.259861051+03:00
description: Podinfo Helm chart for Kubernetes
digest: bd25a710eddb3985d3bd921a11022b5c68a04d37cf93a1a4aab17eeda35aa2f8
engine: gotpl
@@ -65,7 +113,7 @@ entries:
version: 0.2.2
- apiVersion: v1
appVersion: 0.5.1
created: 2018-08-17T18:44:47.409648714+03:00
created: 2018-09-11T22:17:53.259045852+03:00
description: Podinfo Helm chart for Kubernetes
digest: 631ca3e2db5553541a50b625f538e6a1f2a103c13aa8148fdd38baf2519e5235
engine: gotpl
@@ -81,7 +129,7 @@ entries:
version: 0.2.1
- apiVersion: v1
appVersion: 0.5.0
created: 2018-08-17T18:44:47.408857715+03:00
created: 2018-09-11T22:17:53.258116794+03:00
description: Podinfo Helm chart for Kubernetes
digest: dfe7cf44aef0d170549918b00966422a07e7611f9d0081fb34f5b5beb0641c00
engine: gotpl
@@ -97,7 +145,7 @@ entries:
version: 0.2.0
- apiVersion: v1
appVersion: 0.3.0
created: 2018-08-17T18:44:47.407706617+03:00
created: 2018-09-11T22:17:53.257068714+03:00
description: Podinfo Helm chart for Kubernetes
digest: 4865a2d8b269cf453935cda9661c2efb82c16411471f8c11221a6d03d9bb58b1
engine: gotpl
@@ -113,10 +161,42 @@ entries:
version: 0.1.0
podinfo-istio:
- apiVersion: v1
appVersion: 0.6.0
created: 2018-08-17T18:44:47.410653895+03:00
appVersion: 1.2.0
created: 2018-09-11T22:17:53.265466221+03:00
description: Podinfo Helm chart for Istio
digest: 79d9cbe4ba8b83ced977d895e56c1223d1fcd88a15f2df1981365c39cf6f4de7
digest: 8115e72f232f82eb3e6da1965364cfede7c069f95a627dddac45cfbe6cb90dc4
engine: gotpl
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
- email: stefanprodan@users.noreply.github.com
name: stefanprodan
name: podinfo-istio
sources:
- https://github.com/stefanprodan/k8s-podinfo
urls:
- https://stefanprodan.github.io/k8s-podinfo/podinfo-istio-1.2.0.tgz
version: 1.2.0
- apiVersion: v1
appVersion: 1.1.0
created: 2018-09-11T22:17:53.264848234+03:00
description: Podinfo Helm chart for Istio
digest: bcceb63ff780a8f0ba0b30997040e4e82170f9cce17c26ec817648ed024c83f5
engine: gotpl
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
- email: stefanprodan@users.noreply.github.com
name: stefanprodan
name: podinfo-istio
sources:
- https://github.com/stefanprodan/k8s-podinfo
urls:
- https://stefanprodan.github.io/k8s-podinfo/podinfo-istio-0.2.0.tgz
version: 0.2.0
- apiVersion: v1
appVersion: 0.6.0
created: 2018-09-11T22:17:53.263900568+03:00
description: Podinfo Helm chart for Istio
digest: f12f8aa1eca1328e9eaa30bd757f6ed3ff97205e2bf016a47265bc2de6a63d8f
engine: gotpl
home: https://github.com/stefanprodan/k8s-podinfo
maintainers:
@@ -128,4 +208,4 @@ entries:
urls:
- https://stefanprodan.github.io/k8s-podinfo/podinfo-istio-0.1.0.tgz
version: 0.1.0
generated: 2018-08-17T18:44:47.404359545+03:00
generated: 2018-09-11T22:17:53.253846309+03:00

Binary file not shown.

Binary file not shown.

BIN
docs/podinfo-1.0.0.tgz Normal file

Binary file not shown.

BIN
docs/podinfo-1.1.0.tgz Normal file

Binary file not shown.

BIN
docs/podinfo-1.2.0.tgz Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -86,7 +86,7 @@
</v-card-title>
<v-card-text>
Instrumentation with Prometheus (RED method).
Structured logging with zerolog and Fluentd.
Structured logging with zap and Fluentd.
Tracing with Jaeger and Istio.
</v-card-text>
</v-card>

View File

@@ -77,15 +77,20 @@ func NewHighBiased(epsilon float64) *Stream {
// is guaranteed to be within (Quantile±Epsilon).
//
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error properties.
func NewTargeted(targets map[float64]float64) *Stream {
func NewTargeted(targetMap map[float64]float64) *Stream {
// Convert map to slice to avoid slow iterations on a map.
// ƒ is called on the hot path, so converting the map to a slice
// beforehand results in significant CPU savings.
targets := targetMapToSlice(targetMap)
ƒ := func(s *stream, r float64) float64 {
var m = math.MaxFloat64
var f float64
for quantile, epsilon := range targets {
if quantile*s.n <= r {
f = (2 * epsilon * r) / quantile
for _, t := range targets {
if t.quantile*s.n <= r {
f = (2 * t.epsilon * r) / t.quantile
} else {
f = (2 * epsilon * (s.n - r)) / (1 - quantile)
f = (2 * t.epsilon * (s.n - r)) / (1 - t.quantile)
}
if f < m {
m = f
@@ -96,6 +101,25 @@ func NewTargeted(targets map[float64]float64) *Stream {
return newStream(ƒ)
}
type target struct {
quantile float64
epsilon float64
}
func targetMapToSlice(targetMap map[float64]float64) []target {
targets := make([]target, 0, len(targetMap))
for quantile, epsilon := range targetMap {
t := target{
quantile: quantile,
epsilon: epsilon,
}
targets = append(targets, t)
}
return targets
}
// Stream computes quantiles for a stream of float64s. It is not thread-safe by
// design. Take care when using across multiple goroutines.
type Stream struct {

2
vendor/github.com/chzyer/readline/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
*.exe
*.tmp

10
vendor/github.com/chzyer/readline/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,10 @@
language: go
go:
- 1.5
before_install:
- go get golang.org/x/crypto/ssh/terminal
script:
- GOOS=windows go install github.com/chzyer/readline/example/...
- GOOS=linux go install github.com/chzyer/readline/example/...
- GOOS=darwin go install github.com/chzyer/readline/example/...
- go test -v

58
vendor/github.com/chzyer/readline/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,58 @@
# ChangeLog
### 1.4 - 2016-07-25
* [#60][60] Support dynamic autocompletion
* Fix ANSI parser on Windows
* Fix wrong column width in complete mode on Windows
* Remove dependent package "golang.org/x/crypto/ssh/terminal"
### 1.3 - 2016-05-09
* [#38][38] add SetChildren for prefix completer interface
* [#42][42] improve multiple lines compatibility
* [#43][43] remove sub-package(runes) for gopkg compatiblity
* [#46][46] Auto complete with space prefixed line
* [#48][48] support suspend process (ctrl+Z)
* [#49][49] fix bug that check equals with previous command
* [#53][53] Fix bug which causes integer divide by zero panicking when input buffer is empty
### 1.2 - 2016-03-05
* Add a demo for checking password strength [example/readline-pass-strength](https://github.com/chzyer/readline/blob/master/example/readline-pass-strength/readline-pass-strength.go), , written by [@sahib](https://github.com/sahib)
* [#23][23], support stdin remapping
* [#27][27], add a `UniqueEditLine` to `Config`, which will erase the editing line after user submited it, usually use in IM.
* Add a demo for multiline [example/readline-multiline](https://github.com/chzyer/readline/blob/master/example/readline-multiline/readline-multiline.go) which can submit one SQL by multiple lines.
* Supports performs even stdin/stdout is not a tty.
* Add a new simple apis for single instance, check by [here](https://github.com/chzyer/readline/blob/master/std.go). It need to save history manually if using this api.
* [#28][28], fixes the history is not working as expected.
* [#33][33], vim mode now support `c`, `d`, `x (delete character)`, `r (replace character)`
### 1.1 - 2015-11-20
* [#12][12] Add support for key `<Delete>`/`<Home>`/`<End>`
* Only enter raw mode as needed (calling `Readline()`), program will receive signal(e.g. Ctrl+C) if not interact with `readline`.
* Bugs fixed for `PrefixCompleter`
* Press `Ctrl+D` in empty line will cause `io.EOF` in error, Press `Ctrl+C` in anytime will cause `ErrInterrupt` instead of `io.EOF`, this will privodes a shell-like user experience.
* Customable Interrupt/EOF prompt in `Config`
* [#17][17] Change atomic package to use 32bit function to let it runnable on arm 32bit devices
* Provides a new password user experience(`readline.ReadPasswordEx()`).
### 1.0 - 2015-10-14
* Initial public release.
[12]: https://github.com/chzyer/readline/pull/12
[17]: https://github.com/chzyer/readline/pull/17
[23]: https://github.com/chzyer/readline/pull/23
[27]: https://github.com/chzyer/readline/pull/27
[28]: https://github.com/chzyer/readline/pull/28
[33]: https://github.com/chzyer/readline/pull/33
[38]: https://github.com/chzyer/readline/pull/38
[42]: https://github.com/chzyer/readline/pull/42
[43]: https://github.com/chzyer/readline/pull/43
[46]: https://github.com/chzyer/readline/pull/46
[48]: https://github.com/chzyer/readline/pull/48
[49]: https://github.com/chzyer/readline/pull/49
[53]: https://github.com/chzyer/readline/pull/53
[60]: https://github.com/chzyer/readline/pull/60

View File

@@ -1,6 +1,6 @@
MIT License
The MIT License (MIT)
Copyright (c) 2017 Olivier Poitrey
Copyright (c) 2015 Chzyer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

306
vendor/github.com/chzyer/readline/README.md generated vendored Normal file
View File

@@ -0,0 +1,306 @@
# readline
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md)
[![Build Status](https://travis-ci.org/chzyer/readline.svg?branch=master)](https://travis-ci.org/chzyer/readline)
[![GoDoc](https://godoc.org/github.com/chzyer/readline?status.svg)](https://godoc.org/github.com/chzyer/readline)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/chzyer/readline?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![OpenCollective](https://opencollective.com/readline/badge/backers.svg)](#backers)
[![OpenCollective](https://opencollective.com/readline/badge/sponsors.svg)](#sponsors)
Readline is A Pure Go Implementation of a libreadline-style Library.
The goal is to be a powerful alternater for GNU-Readline.
**WHY:**
Readline will support most of features which GNU Readline is supported, and provide a pure go environment and a MIT license.
It can also provides shell-like interactives by using [flagly](https://github.com/chzyer/flagly) (demo: [flagly-shell](https://github.com/chzyer/flagly/blob/master/demo/flagly-shell/flagly-shell.go))
# Demo
![demo](https://raw.githubusercontent.com/chzyer/readline/assets/demo.gif)
Also works fine in windows
![demo windows](https://raw.githubusercontent.com/chzyer/readline/assets/windows.gif)
* [example/readline-demo](https://github.com/chzyer/readline/blob/master/example/readline-demo/readline-demo.go) The source code about the demo above
* [example/readline-im](https://github.com/chzyer/readline/blob/master/example/readline-im/readline-im.go) Example for how to write a IM program.
* [example/readline-multiline](https://github.com/chzyer/readline/blob/master/example/readline-multiline/readline-multiline.go) Example for how to parse command which can submit by multiple time.
* [example/readline-pass-strength](https://github.com/chzyer/readline/blob/master/example/readline-pass-strength/readline-pass-strength.go) A example about checking password strength, written by [@sahib](https://github.com/sahib)
# Todo
* Vi Mode is not completely finish
* More funny examples
* Support dumb/eterm-color terminal in emacs
# Features
* Support emacs/vi mode, almost all basic features that GNU-Readline is supported
* zsh-style backward/forward history search
* zsh-style completion
* Readline auto refresh when others write to Stdout while editing (it needs specify the Stdout/Stderr provided by *readline.Instance to others).
* Support colourful prompt in all platforms.
# Usage
* Import package
```
go get gopkg.in/readline.v1
```
or
```
go get github.com/chzyer/readline
```
* Simplest example
```go
import "gopkg.in/readline.v1"
rl, err := readline.New("> ")
if err != nil {
panic(err)
}
defer rl.Close()
for {
line, err := rl.Readline()
if err != nil { // io.EOF, readline.ErrInterrupt
break
}
println(line)
}
```
* Example with durable history
```go
rl, err := readline.NewEx(&readline.Config{
Prompt: "> ",
HistoryFile: "/tmp/readline.tmp",
})
if err != nil {
panic(err)
}
defer rl.Close()
for {
line, err := rl.Readline()
if err != nil { // io.EOF, readline.ErrInterrupt
break
}
println(line)
}
```
* Example with auto completion
```go
import (
"gopkg.in/readline.v1"
)
var completer = readline.NewPrefixCompleter(
readline.PcItem("say",
readline.PcItem("hello"),
readline.PcItem("bye"),
),
readline.PcItem("help"),
)
rl, err := readline.NewEx(&readline.Config{
Prompt: "> ",
AutoComplete: completer,
})
if err != nil {
panic(err)
}
defer rl.Close()
for {
line, err := rl.Readline()
if err != nil { // io.EOF, readline.ErrInterrupt
break
}
println(line)
}
```
# Shortcut
`Meta`+`B` means press `Esc` and `n` separately.
Users can change that in terminal simulator(i.e. iTerm2) to `Alt`+`B`
Notice: `Meta`+`B` is equals with `Alt`+`B` in windows.
* Shortcut in normal mode
| Shortcut | Comment |
|--------------------|------------------------------------------|
| `Ctrl`+`A` | Beginning of line |
| `Ctrl`+`B` / `←` | Backward one character |
| `Meta`+`B` | Backward one word |
| `Ctrl`+`C` | Send io.EOF |
| `Ctrl`+`D` | Delete one character |
| `Meta`+`D` | Delete one word |
| `Ctrl`+`E` | End of line |
| `Ctrl`+`F` / `→` | Forward one character |
| `Meta`+`F` | Forward one word |
| `Ctrl`+`G` | Cancel |
| `Ctrl`+`H` | Delete previous character |
| `Ctrl`+`I` / `Tab` | Command line completion |
| `Ctrl`+`J` | Line feed |
| `Ctrl`+`K` | Cut text to the end of line |
| `Ctrl`+`L` | Clear screen |
| `Ctrl`+`M` | Same as Enter key |
| `Ctrl`+`N` / `↓` | Next line (in history) |
| `Ctrl`+`P` / `↑` | Prev line (in history) |
| `Ctrl`+`R` | Search backwards in history |
| `Ctrl`+`S` | Search forwards in history |
| `Ctrl`+`T` | Transpose characters |
| `Meta`+`T` | Transpose words (TODO) |
| `Ctrl`+`U` | Cut text to the beginning of line |
| `Ctrl`+`W` | Cut previous word |
| `Backspace` | Delete previous character |
| `Meta`+`Backspace` | Cut previous word |
| `Enter` | Line feed |
* Shortcut in Search Mode (`Ctrl`+`S` or `Ctrl`+`r` to enter this mode)
| Shortcut | Comment |
|-------------------------|---------------------------------------------|
| `Ctrl`+`S` | Search forwards in history |
| `Ctrl`+`R` | Search backwards in history |
| `Ctrl`+`C` / `Ctrl`+`G` | Exit Search Mode and revert the history |
| `Backspace` | Delete previous character |
| Other | Exit Search Mode |
* Shortcut in Complete Select Mode (double `Tab` to enter this mode)
| Shortcut | Comment |
|-------------------------|---------------------------------------------|
| `Ctrl`+`F` | Move Forward |
| `Ctrl`+`B` | Move Backward |
| `Ctrl`+`N` | Move to next line |
| `Ctrl`+`P` | Move to previous line |
| `Ctrl`+`A` | Move to the first candicate in current line |
| `Ctrl`+`E` | Move to the last candicate in current line |
| `Tab` / `Enter` | Use the word on cursor to complete |
| `Ctrl`+`C` / `Ctrl`+`G` | Exit Complete Select Mode |
| Other | Exit Complete Select Mode |
# Tested with
| Environment | $TERM |
|-------------------------------|--------|
| Mac OS X iTerm2 | xterm |
| Mac OS X default Terminal.app | xterm |
| Mac OS X iTerm2 Screen | screen |
| Mac OS X iTerm2 Tmux | screen |
| Ubuntu Server 14.04 LTS | linux |
| Centos 7 | linux |
| Windows 10 | - |
### Notice:
* `Ctrl`+`A` is not working in `screen` because it used as a control command by default
If you test it otherwhere, whether it works fine or not, please let me know!
## Who is using Readline
* [cockroachdb/cockroach](https://github.com/cockroachdb/cockroach)
* [youtube/doorman](https://github.com/youtube/doorman)
* [bom-d-van/harp](https://github.com/bom-d-van/harp)
* [abiosoft/ishell](https://github.com/abiosoft/ishell)
* [robertkrimen/otto](https://github.com/robertkrimen/otto)
* [Netflix/hal-9001](https://github.com/Netflix/hal-9001)
* [docker/go-p9p](https://github.com/docker/go-p9p)
# Feedback
If you have any questions, please submit a github issue and any pull requests is welcomed :)
* [https://twitter.com/chzyer](https://twitter.com/chzyer)
* [http://weibo.com/2145262190](http://weibo.com/2145262190)
# Backers
Love Readline? Help me keep it alive by donating funds to cover project expenses!<br />
[[Become a backer](https://opencollective.com/readline#backer)]
<a href="https://opencollective.com/readline/backer/0/website" target="_blank"><img src="https://opencollective.com/readline/backer/0/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/1/website" target="_blank"><img src="https://opencollective.com/readline/backer/1/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/2/website" target="_blank"><img src="https://opencollective.com/readline/backer/2/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/3/website" target="_blank"><img src="https://opencollective.com/readline/backer/3/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/4/website" target="_blank"><img src="https://opencollective.com/readline/backer/4/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/5/website" target="_blank"><img src="https://opencollective.com/readline/backer/5/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/6/website" target="_blank"><img src="https://opencollective.com/readline/backer/6/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/7/website" target="_blank"><img src="https://opencollective.com/readline/backer/7/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/8/website" target="_blank"><img src="https://opencollective.com/readline/backer/8/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/9/website" target="_blank"><img src="https://opencollective.com/readline/backer/9/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/10/website" target="_blank"><img src="https://opencollective.com/readline/backer/10/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/11/website" target="_blank"><img src="https://opencollective.com/readline/backer/11/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/12/website" target="_blank"><img src="https://opencollective.com/readline/backer/12/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/13/website" target="_blank"><img src="https://opencollective.com/readline/backer/13/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/14/website" target="_blank"><img src="https://opencollective.com/readline/backer/14/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/15/website" target="_blank"><img src="https://opencollective.com/readline/backer/15/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/16/website" target="_blank"><img src="https://opencollective.com/readline/backer/16/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/17/website" target="_blank"><img src="https://opencollective.com/readline/backer/17/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/18/website" target="_blank"><img src="https://opencollective.com/readline/backer/18/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/19/website" target="_blank"><img src="https://opencollective.com/readline/backer/19/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/20/website" target="_blank"><img src="https://opencollective.com/readline/backer/20/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/21/website" target="_blank"><img src="https://opencollective.com/readline/backer/21/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/22/website" target="_blank"><img src="https://opencollective.com/readline/backer/22/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/23/website" target="_blank"><img src="https://opencollective.com/readline/backer/23/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/24/website" target="_blank"><img src="https://opencollective.com/readline/backer/24/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/25/website" target="_blank"><img src="https://opencollective.com/readline/backer/25/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/26/website" target="_blank"><img src="https://opencollective.com/readline/backer/26/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/27/website" target="_blank"><img src="https://opencollective.com/readline/backer/27/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/28/website" target="_blank"><img src="https://opencollective.com/readline/backer/28/avatar.svg"></a>
<a href="https://opencollective.com/readline/backer/29/website" target="_blank"><img src="https://opencollective.com/readline/backer/29/avatar.svg"></a>
# Sponsors
Become a sponsor and get your logo here on our Github page. [[Become a sponsor](https://opencollective.com/readline#sponsor)]
<a href="https://opencollective.com/readline/sponsor/0/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/1/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/2/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/3/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/4/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/5/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/6/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/7/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/8/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/9/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/9/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/10/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/10/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/11/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/11/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/12/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/12/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/13/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/13/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/14/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/14/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/15/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/15/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/16/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/16/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/17/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/17/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/18/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/18/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/19/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/19/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/20/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/20/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/21/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/21/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/22/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/22/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/23/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/23/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/24/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/24/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/25/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/25/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/26/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/26/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/27/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/27/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/28/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/28/avatar.svg"></a>
<a href="https://opencollective.com/readline/sponsor/29/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/29/avatar.svg"></a>

246
vendor/github.com/chzyer/readline/ansi_windows.go generated vendored Normal file
View File

@@ -0,0 +1,246 @@
// +build windows
package readline
import (
"bufio"
"io"
"strconv"
"strings"
"sync"
"unicode/utf8"
"unsafe"
)
const (
_ = uint16(0)
COLOR_FBLUE = 0x0001
COLOR_FGREEN = 0x0002
COLOR_FRED = 0x0004
COLOR_FINTENSITY = 0x0008
COLOR_BBLUE = 0x0010
COLOR_BGREEN = 0x0020
COLOR_BRED = 0x0040
COLOR_BINTENSITY = 0x0080
COMMON_LVB_UNDERSCORE = 0x8000
)
var ColorTableFg = []word{
0, // 30: Black
COLOR_FRED, // 31: Red
COLOR_FGREEN, // 32: Green
COLOR_FRED | COLOR_FGREEN, // 33: Yellow
COLOR_FBLUE, // 34: Blue
COLOR_FRED | COLOR_FBLUE, // 35: Magenta
COLOR_FGREEN | COLOR_FBLUE, // 36: Cyan
COLOR_FRED | COLOR_FBLUE | COLOR_FGREEN, // 37: White
}
var ColorTableBg = []word{
0, // 40: Black
COLOR_BRED, // 41: Red
COLOR_BGREEN, // 42: Green
COLOR_BRED | COLOR_BGREEN, // 43: Yellow
COLOR_BBLUE, // 44: Blue
COLOR_BRED | COLOR_BBLUE, // 45: Magenta
COLOR_BGREEN | COLOR_BBLUE, // 46: Cyan
COLOR_BRED | COLOR_BBLUE | COLOR_BGREEN, // 47: White
}
type ANSIWriter struct {
target io.Writer
wg sync.WaitGroup
ctx *ANSIWriterCtx
sync.Mutex
}
func NewANSIWriter(w io.Writer) *ANSIWriter {
a := &ANSIWriter{
target: w,
ctx: NewANSIWriterCtx(w),
}
return a
}
func (a *ANSIWriter) Close() error {
a.wg.Wait()
return nil
}
type ANSIWriterCtx struct {
isEsc bool
isEscSeq bool
arg []string
target *bufio.Writer
wantFlush bool
}
func NewANSIWriterCtx(target io.Writer) *ANSIWriterCtx {
return &ANSIWriterCtx{
target: bufio.NewWriter(target),
}
}
func (a *ANSIWriterCtx) Flush() {
a.target.Flush()
}
func (a *ANSIWriterCtx) process(r rune) bool {
if a.wantFlush {
if r == 0 || r == CharEsc {
a.wantFlush = false
a.target.Flush()
}
}
if a.isEscSeq {
a.isEscSeq = a.ioloopEscSeq(a.target, r, &a.arg)
return true
}
switch r {
case CharEsc:
a.isEsc = true
case '[':
if a.isEsc {
a.arg = nil
a.isEscSeq = true
a.isEsc = false
break
}
fallthrough
default:
a.target.WriteRune(r)
a.wantFlush = true
}
return true
}
func (a *ANSIWriterCtx) ioloopEscSeq(w *bufio.Writer, r rune, argptr *[]string) bool {
arg := *argptr
var err error
if r >= 'A' && r <= 'D' {
count := short(GetInt(arg, 1))
info, err := GetConsoleScreenBufferInfo()
if err != nil {
return false
}
switch r {
case 'A': // up
info.dwCursorPosition.y -= count
case 'B': // down
info.dwCursorPosition.y += count
case 'C': // right
info.dwCursorPosition.x += count
case 'D': // left
info.dwCursorPosition.x -= count
}
SetConsoleCursorPosition(&info.dwCursorPosition)
return false
}
switch r {
case 'J':
killLines()
case 'K':
eraseLine()
case 'm':
color := word(0)
for _, item := range arg {
var c int
c, err = strconv.Atoi(item)
if err != nil {
w.WriteString("[" + strings.Join(arg, ";") + "m")
break
}
if c >= 30 && c < 40 {
color ^= COLOR_FINTENSITY
color |= ColorTableFg[c-30]
} else if c >= 40 && c < 50 {
color ^= COLOR_BINTENSITY
color |= ColorTableBg[c-40]
} else if c == 4 {
color |= COMMON_LVB_UNDERSCORE | ColorTableFg[7]
} else { // unknown code treat as reset
color = ColorTableFg[7]
}
}
if err != nil {
break
}
kernel.SetConsoleTextAttribute(stdout, uintptr(color))
case '\007': // set title
case ';':
if len(arg) == 0 || arg[len(arg)-1] != "" {
arg = append(arg, "")
*argptr = arg
}
return true
default:
if len(arg) == 0 {
arg = append(arg, "")
}
arg[len(arg)-1] += string(r)
*argptr = arg
return true
}
*argptr = nil
return false
}
func (a *ANSIWriter) Write(b []byte) (int, error) {
a.Lock()
defer a.Unlock()
off := 0
for len(b) > off {
r, size := utf8.DecodeRune(b[off:])
if size == 0 {
return off, io.ErrShortWrite
}
off += size
a.ctx.process(r)
}
a.ctx.Flush()
return off, nil
}
func killLines() error {
sbi, err := GetConsoleScreenBufferInfo()
if err != nil {
return err
}
size := (sbi.dwCursorPosition.y - sbi.dwSize.y) * sbi.dwSize.x
size += sbi.dwCursorPosition.x
var written int
kernel.FillConsoleOutputAttribute(stdout, uintptr(ColorTableFg[7]),
uintptr(size),
sbi.dwCursorPosition.ptr(),
uintptr(unsafe.Pointer(&written)),
)
return kernel.FillConsoleOutputCharacterW(stdout, uintptr(' '),
uintptr(size),
sbi.dwCursorPosition.ptr(),
uintptr(unsafe.Pointer(&written)),
)
}
func eraseLine() error {
sbi, err := GetConsoleScreenBufferInfo()
if err != nil {
return err
}
size := sbi.dwSize.x
sbi.dwCursorPosition.x = 0
var written int
return kernel.FillConsoleOutputCharacterW(stdout, uintptr(' '),
uintptr(size),
sbi.dwCursorPosition.ptr(),
uintptr(unsafe.Pointer(&written)),
)
}

36
vendor/github.com/chzyer/readline/char.go generated vendored Normal file
View File

@@ -0,0 +1,36 @@
package readline
const (
CharLineStart = 1
CharBackward = 2
CharInterrupt = 3
CharDelete = 4
CharLineEnd = 5
CharForward = 6
CharBell = 7
CharCtrlH = 8
CharTab = 9
CharCtrlJ = 10
CharKill = 11
CharCtrlL = 12
CharEnter = 13
CharNext = 14
CharPrev = 16
CharBckSearch = 18
CharFwdSearch = 19
CharTranspose = 20
CharCtrlU = 21
CharCtrlW = 23
CharCtrlZ = 26
CharEsc = 27
CharEscapeEx = 91
CharBackspace = 127
)
const (
MetaBackward rune = -iota - 1
MetaForward
MetaDelete
MetaBackspace
MetaTranspose
)

273
vendor/github.com/chzyer/readline/complete.go generated vendored Normal file
View File

@@ -0,0 +1,273 @@
package readline
import (
"bufio"
"bytes"
"fmt"
"io"
)
type AutoCompleter interface {
// Readline will pass the whole line and current offset to it
// Completer need to pass all the candidates, and how long they shared the same characters in line
// Example:
// [go, git, git-shell, grep]
// Do("g", 1) => ["o", "it", "it-shell", "rep"], 1
// Do("gi", 2) => ["t", "t-shell"], 2
// Do("git", 3) => ["", "-shell"], 3
Do(line []rune, pos int) (newLine [][]rune, length int)
}
type opCompleter struct {
w io.Writer
op *Operation
width int
inCompleteMode bool
inSelectMode bool
candidate [][]rune
candidateSource []rune
candidateOff int
candidateChoise int
candidateColNum int
}
func newOpCompleter(w io.Writer, op *Operation, width int) *opCompleter {
return &opCompleter{
w: w,
op: op,
width: width,
}
}
func (o *opCompleter) doSelect() {
if len(o.candidate) == 1 {
o.op.buf.WriteRunes(o.candidate[0])
o.ExitCompleteMode(false)
return
}
o.nextCandidate(1)
o.CompleteRefresh()
}
func (o *opCompleter) nextCandidate(i int) {
o.candidateChoise += i
o.candidateChoise = o.candidateChoise % len(o.candidate)
if o.candidateChoise < 0 {
o.candidateChoise = len(o.candidate) + o.candidateChoise
}
}
func (o *opCompleter) OnComplete() {
if o.IsInCompleteSelectMode() {
o.doSelect()
return
}
buf := o.op.buf
rs := buf.Runes()
if o.IsInCompleteMode() && o.candidateSource != nil && runes.Equal(rs, o.candidateSource) {
o.EnterCompleteSelectMode()
o.doSelect()
return
}
o.ExitCompleteSelectMode()
o.candidateSource = rs
newLines, offset := o.op.cfg.AutoComplete.Do(rs, buf.idx)
if len(newLines) == 0 {
o.ExitCompleteMode(false)
return
}
// only Aggregate candidates in non-complete mode
if !o.IsInCompleteMode() {
if len(newLines) == 1 {
buf.WriteRunes(newLines[0])
o.ExitCompleteMode(false)
return
}
same, size := runes.Aggregate(newLines)
if size > 0 {
buf.WriteRunes(same)
o.ExitCompleteMode(false)
return
}
}
o.EnterCompleteMode(offset, newLines)
}
func (o *opCompleter) IsInCompleteSelectMode() bool {
return o.inSelectMode
}
func (o *opCompleter) IsInCompleteMode() bool {
return o.inCompleteMode
}
func (o *opCompleter) HandleCompleteSelect(r rune) bool {
next := true
switch r {
case CharEnter, CharCtrlJ:
next = false
o.op.buf.WriteRunes(o.op.candidate[o.op.candidateChoise])
o.ExitCompleteMode(false)
case CharLineStart:
num := o.candidateChoise % o.candidateColNum
o.nextCandidate(-num)
case CharLineEnd:
num := o.candidateColNum - o.candidateChoise%o.candidateColNum - 1
o.candidateChoise += num
if o.candidateChoise >= len(o.candidate) {
o.candidateChoise = len(o.candidate) - 1
}
case CharBackspace:
o.ExitCompleteSelectMode()
next = false
case CharTab, CharForward:
o.doSelect()
case CharBell, CharInterrupt:
o.ExitCompleteMode(true)
next = false
case CharNext:
tmpChoise := o.candidateChoise + o.candidateColNum
if tmpChoise >= o.getMatrixSize() {
tmpChoise -= o.getMatrixSize()
} else if tmpChoise >= len(o.candidate) {
tmpChoise += o.candidateColNum
tmpChoise -= o.getMatrixSize()
}
o.candidateChoise = tmpChoise
case CharBackward:
o.nextCandidate(-1)
case CharPrev:
tmpChoise := o.candidateChoise - o.candidateColNum
if tmpChoise < 0 {
tmpChoise += o.getMatrixSize()
if tmpChoise >= len(o.candidate) {
tmpChoise -= o.candidateColNum
}
}
o.candidateChoise = tmpChoise
default:
next = false
o.ExitCompleteSelectMode()
}
if next {
o.CompleteRefresh()
return true
}
return false
}
func (o *opCompleter) getMatrixSize() int {
line := len(o.candidate) / o.candidateColNum
if len(o.candidate)%o.candidateColNum != 0 {
line++
}
return line * o.candidateColNum
}
func (o *opCompleter) OnWidthChange(newWidth int) {
o.width = newWidth
}
func (o *opCompleter) CompleteRefresh() {
if !o.inCompleteMode {
return
}
lineCnt := o.op.buf.CursorLineCount()
colWidth := 0
for _, c := range o.candidate {
w := runes.WidthAll(c)
if w > colWidth {
colWidth = w
}
}
colWidth += o.candidateOff + 1
same := o.op.buf.RuneSlice(-o.candidateOff)
// -1 to avoid reach the end of line
width := o.width - 1
colNum := width / colWidth
colWidth += (width - (colWidth * colNum)) / colNum
o.candidateColNum = colNum
buf := bufio.NewWriter(o.w)
buf.Write(bytes.Repeat([]byte("\n"), lineCnt))
colIdx := 0
lines := 1
buf.WriteString("\033[J")
for idx, c := range o.candidate {
inSelect := idx == o.candidateChoise && o.IsInCompleteSelectMode()
if inSelect {
buf.WriteString("\033[30;47m")
}
buf.WriteString(string(same))
buf.WriteString(string(c))
buf.Write(bytes.Repeat([]byte(" "), colWidth-len(c)))
if inSelect {
buf.WriteString("\033[0m")
}
colIdx++
if colIdx == colNum {
buf.WriteString("\n")
lines++
colIdx = 0
}
}
// move back
fmt.Fprintf(buf, "\033[%dA\r", lineCnt-1+lines)
fmt.Fprintf(buf, "\033[%dC", o.op.buf.idx+o.op.buf.PromptLen())
buf.Flush()
}
func (o *opCompleter) aggCandidate(candidate [][]rune) int {
offset := 0
for i := 0; i < len(candidate[0]); i++ {
for j := 0; j < len(candidate)-1; j++ {
if i > len(candidate[j]) {
goto aggregate
}
if candidate[j][i] != candidate[j+1][i] {
goto aggregate
}
}
offset = i
}
aggregate:
return offset
}
func (o *opCompleter) EnterCompleteSelectMode() {
o.inSelectMode = true
o.candidateChoise = -1
o.CompleteRefresh()
}
func (o *opCompleter) EnterCompleteMode(offset int, candidate [][]rune) {
o.inCompleteMode = true
o.candidate = candidate
o.candidateOff = offset
o.CompleteRefresh()
}
func (o *opCompleter) ExitCompleteSelectMode() {
o.inSelectMode = false
o.candidate = nil
o.candidateChoise = -1
o.candidateOff = -1
o.candidateSource = nil
}
func (o *opCompleter) ExitCompleteMode(revent bool) {
o.inCompleteMode = false
o.ExitCompleteSelectMode()
}

165
vendor/github.com/chzyer/readline/complete_helper.go generated vendored Normal file
View File

@@ -0,0 +1,165 @@
package readline
import (
"bytes"
"strings"
)
// Caller type for dynamic completion
type DynamicCompleteFunc func(string) []string
type PrefixCompleterInterface interface {
Print(prefix string, level int, buf *bytes.Buffer)
Do(line []rune, pos int) (newLine [][]rune, length int)
GetName() []rune
GetChildren() []PrefixCompleterInterface
SetChildren(children []PrefixCompleterInterface)
}
type DynamicPrefixCompleterInterface interface {
PrefixCompleterInterface
IsDynamic() bool
GetDynamicNames(line []rune) [][]rune
}
type PrefixCompleter struct {
Name []rune
Dynamic bool
Callback DynamicCompleteFunc
Children []PrefixCompleterInterface
}
func (p *PrefixCompleter) Tree(prefix string) string {
buf := bytes.NewBuffer(nil)
p.Print(prefix, 0, buf)
return buf.String()
}
func Print(p PrefixCompleterInterface, prefix string, level int, buf *bytes.Buffer) {
if strings.TrimSpace(string(p.GetName())) != "" {
buf.WriteString(prefix)
if level > 0 {
buf.WriteString("├")
buf.WriteString(strings.Repeat("─", (level*4)-2))
buf.WriteString(" ")
}
buf.WriteString(string(p.GetName()) + "\n")
level++
}
for _, ch := range p.GetChildren() {
ch.Print(prefix, level, buf)
}
}
func (p *PrefixCompleter) Print(prefix string, level int, buf *bytes.Buffer) {
Print(p, prefix, level, buf)
}
func (p *PrefixCompleter) IsDynamic() bool {
return p.Dynamic
}
func (p *PrefixCompleter) GetName() []rune {
return p.Name
}
func (p *PrefixCompleter) GetDynamicNames(line []rune) [][]rune {
var names = [][]rune{}
for _, name := range p.Callback(string(line)) {
names = append(names, []rune(name+" "))
}
return names
}
func (p *PrefixCompleter) GetChildren() []PrefixCompleterInterface {
return p.Children
}
func (p *PrefixCompleter) SetChildren(children []PrefixCompleterInterface) {
p.Children = children
}
func NewPrefixCompleter(pc ...PrefixCompleterInterface) *PrefixCompleter {
return PcItem("", pc...)
}
func PcItem(name string, pc ...PrefixCompleterInterface) *PrefixCompleter {
name += " "
return &PrefixCompleter{
Name: []rune(name),
Dynamic: false,
Children: pc,
}
}
func PcItemDynamic(callback DynamicCompleteFunc, pc ...PrefixCompleterInterface) *PrefixCompleter {
return &PrefixCompleter{
Callback: callback,
Dynamic: true,
Children: pc,
}
}
func (p *PrefixCompleter) Do(line []rune, pos int) (newLine [][]rune, offset int) {
return doInternal(p, line, pos, line)
}
func Do(p PrefixCompleterInterface, line []rune, pos int) (newLine [][]rune, offset int) {
return doInternal(p, line, pos, line)
}
func doInternal(p PrefixCompleterInterface, line []rune, pos int, origLine []rune) (newLine [][]rune, offset int) {
line = runes.TrimSpaceLeft(line[:pos])
goNext := false
var lineCompleter PrefixCompleterInterface
for _, child := range p.GetChildren() {
childNames := make([][]rune, 1)
childDynamic, ok := child.(DynamicPrefixCompleterInterface)
if ok && childDynamic.IsDynamic() {
childNames = childDynamic.GetDynamicNames(origLine)
} else {
childNames[0] = child.GetName()
}
for _, childName := range childNames {
if len(line) >= len(childName) {
if runes.HasPrefix(line, childName) {
if len(line) == len(childName) {
newLine = append(newLine, []rune{' '})
} else {
newLine = append(newLine, childName)
}
offset = len(childName)
lineCompleter = child
goNext = true
}
} else {
if runes.HasPrefix(childName, line) {
newLine = append(newLine, childName[len(line):])
offset = len(line)
lineCompleter = child
}
}
}
}
if len(newLine) != 1 {
return
}
tmpLine := make([]rune, 0, len(line))
for i := offset; i < len(line); i++ {
if line[i] == ' ' {
continue
}
tmpLine = append(tmpLine, line[i:]...)
return doInternal(lineCompleter, tmpLine, len(tmpLine), origLine)
}
if goNext {
return doInternal(lineCompleter, nil, 0, origLine)
}
return
}

82
vendor/github.com/chzyer/readline/complete_segment.go generated vendored Normal file
View File

@@ -0,0 +1,82 @@
package readline
type SegmentCompleter interface {
// a
// |- a1
// |--- a11
// |- a2
// b
// input:
// DoTree([], 0) [a, b]
// DoTree([a], 1) [a]
// DoTree([a, ], 0) [a1, a2]
// DoTree([a, a], 1) [a1, a2]
// DoTree([a, a1], 2) [a1]
// DoTree([a, a1, ], 0) [a11]
// DoTree([a, a1, a], 1) [a11]
DoSegment([][]rune, int) [][]rune
}
type dumpSegmentCompleter struct {
f func([][]rune, int) [][]rune
}
func (d *dumpSegmentCompleter) DoSegment(segment [][]rune, n int) [][]rune {
return d.f(segment, n)
}
func SegmentFunc(f func([][]rune, int) [][]rune) AutoCompleter {
return &SegmentComplete{&dumpSegmentCompleter{f}}
}
func SegmentAutoComplete(completer SegmentCompleter) *SegmentComplete {
return &SegmentComplete{
SegmentCompleter: completer,
}
}
type SegmentComplete struct {
SegmentCompleter
}
func RetSegment(segments [][]rune, cands [][]rune, idx int) ([][]rune, int) {
ret := make([][]rune, 0, len(cands))
lastSegment := segments[len(segments)-1]
for _, cand := range cands {
if !runes.HasPrefix(cand, lastSegment) {
continue
}
ret = append(ret, cand[len(lastSegment):])
}
return ret, idx
}
func SplitSegment(line []rune, pos int) ([][]rune, int) {
segs := [][]rune{}
lastIdx := -1
line = line[:pos]
pos = 0
for idx, l := range line {
if l == ' ' {
pos = 0
segs = append(segs, line[lastIdx+1:idx])
lastIdx = idx
} else {
pos++
}
}
segs = append(segs, line[lastIdx+1:])
return segs, pos
}
func (c *SegmentComplete) Do(line []rune, pos int) (newLine [][]rune, offset int) {
segment, idx := SplitSegment(line, pos)
cands := c.DoSegment(segment, idx)
newLine, offset = RetSegment(segment, cands, idx)
for idx := range newLine {
newLine[idx] = append(newLine[idx], ' ')
}
return newLine, offset
}

29
vendor/github.com/chzyer/readline/debug.go generated vendored Normal file
View File

@@ -0,0 +1,29 @@
package readline
import (
"container/list"
"fmt"
"os"
"time"
)
func sleep(n int) {
Debug(n)
time.Sleep(2000 * time.Millisecond)
}
// print a linked list to Debug()
func debugList(l *list.List) {
idx := 0
for e := l.Front(); e != nil; e = e.Next() {
Debug(idx, fmt.Sprintf("%+v", e.Value))
idx++
}
}
// append log info to another file
func Debug(o ...interface{}) {
f, _ := os.OpenFile("debug.tmp", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
fmt.Fprintln(f, o...)
f.Close()
}

20
vendor/github.com/chzyer/readline/doc.go generated vendored Normal file
View File

@@ -0,0 +1,20 @@
// Readline is a pure go implementation for GNU-Readline kind library.
//
// WHY: Readline will support most of features which GNU Readline is supported, and provide a pure go environment and a MIT license.
//
// example:
// rl, err := readline.New("> ")
// if err != nil {
// panic(err)
// }
// defer rl.Close()
//
// for {
// line, err := rl.Readline()
// if err != nil { // io.EOF
// break
// }
// println(line)
// }
//
package readline

296
vendor/github.com/chzyer/readline/history.go generated vendored Normal file
View File

@@ -0,0 +1,296 @@
package readline
import (
"bufio"
"container/list"
"fmt"
"os"
"strings"
)
type hisItem struct {
Source []rune
Version int64
Tmp []rune
}
func (h *hisItem) Clean() {
h.Source = nil
h.Tmp = nil
}
type opHistory struct {
cfg *Config
history *list.List
historyVer int64
current *list.Element
fd *os.File
}
func newOpHistory(cfg *Config) (o *opHistory) {
o = &opHistory{
cfg: cfg,
history: list.New(),
}
return o
}
func (o *opHistory) Reset() {
o.history = list.New()
o.current = nil
}
func (o *opHistory) IsHistoryClosed() bool {
return o.fd.Fd() == ^(uintptr(0))
}
func (o *opHistory) Init() {
if o.IsHistoryClosed() {
o.initHistory()
}
}
func (o *opHistory) initHistory() {
if o.cfg.HistoryFile != "" {
o.historyUpdatePath(o.cfg.HistoryFile)
}
}
// only called by newOpHistory
func (o *opHistory) historyUpdatePath(path string) {
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
if err != nil {
return
}
o.fd = f
r := bufio.NewReader(o.fd)
total := 0
for ; ; total++ {
line, err := r.ReadString('\n')
if err != nil {
break
}
// ignore the empty line
line = strings.TrimSpace(line)
if len(line) == 0 {
continue
}
o.Push([]rune(line))
o.Compact()
}
if total > o.cfg.HistoryLimit {
o.Rewrite()
}
o.historyVer++
o.Push(nil)
return
}
func (o *opHistory) Compact() {
for o.history.Len() > o.cfg.HistoryLimit && o.history.Len() > 0 {
o.history.Remove(o.history.Front())
}
}
func (o *opHistory) Rewrite() {
if o.cfg.HistoryFile == "" {
return
}
tmpFile := o.cfg.HistoryFile + ".tmp"
fd, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_APPEND, 0666)
if err != nil {
return
}
buf := bufio.NewWriter(fd)
for elem := o.history.Front(); elem != nil; elem = elem.Next() {
buf.WriteString(string(elem.Value.(*hisItem).Source))
}
buf.Flush()
// replace history file
if err = os.Rename(tmpFile, o.cfg.HistoryFile); err != nil {
fd.Close()
return
}
if o.fd != nil {
o.fd.Close()
}
// fd is write only, just satisfy what we need.
o.fd = fd
}
func (o *opHistory) Close() {
if o.fd != nil {
o.fd.Close()
}
}
func (o *opHistory) FindBck(isNewSearch bool, rs []rune, start int) (int, *list.Element) {
for elem := o.current; elem != nil; elem = elem.Prev() {
item := o.showItem(elem.Value)
if isNewSearch {
start += len(rs)
}
if elem == o.current {
if len(item) >= start {
item = item[:start]
}
}
idx := runes.IndexAllBck(item, rs)
if idx < 0 {
continue
}
return idx, elem
}
return -1, nil
}
func (o *opHistory) FindFwd(isNewSearch bool, rs []rune, start int) (int, *list.Element) {
for elem := o.current; elem != nil; elem = elem.Next() {
item := o.showItem(elem.Value)
if isNewSearch {
start -= len(rs)
if start < 0 {
start = 0
}
}
if elem == o.current {
if len(item)-1 >= start {
item = item[start:]
} else {
continue
}
}
idx := runes.IndexAll(item, rs)
if idx < 0 {
continue
}
if elem == o.current {
idx += start
}
return idx, elem
}
return -1, nil
}
func (o *opHistory) showItem(obj interface{}) []rune {
item := obj.(*hisItem)
if item.Version == o.historyVer {
return item.Tmp
}
return item.Source
}
func (o *opHistory) Prev() []rune {
if o.current == nil {
return nil
}
current := o.current.Prev()
if current == nil {
return nil
}
o.current = current
return runes.Copy(o.showItem(current.Value))
}
func (o *opHistory) Next() ([]rune, bool) {
if o.current == nil {
return nil, false
}
current := o.current.Next()
if current == nil {
return nil, false
}
o.current = current
return runes.Copy(o.showItem(current.Value)), true
}
func (o *opHistory) debug() {
Debug("-------")
for item := o.history.Front(); item != nil; item = item.Next() {
Debug(fmt.Sprintf("%+v", item.Value))
}
}
// save history
func (o *opHistory) New(current []rune) (err error) {
current = runes.Copy(current)
// if just use last command without modify
// just clean lastest history
if back := o.history.Back(); back != nil {
prev := back.Prev()
if prev != nil {
if runes.Equal(current, prev.Value.(*hisItem).Source) {
o.current = o.history.Back()
o.current.Value.(*hisItem).Clean()
o.historyVer++
return nil
}
}
}
if len(current) == 0 {
o.current = o.history.Back()
if o.current != nil {
o.current.Value.(*hisItem).Clean()
o.historyVer++
return nil
}
}
if o.current != o.history.Back() {
// move history item to current command
currentItem := o.current.Value.(*hisItem)
// set current to last item
o.current = o.history.Back()
current = runes.Copy(currentItem.Tmp)
}
// err only can be a IO error, just report
err = o.Update(current, true)
// push a new one to commit current command
o.historyVer++
o.Push(nil)
return
}
func (o *opHistory) Revert() {
o.historyVer++
o.current = o.history.Back()
}
func (o *opHistory) Update(s []rune, commit bool) (err error) {
s = runes.Copy(s)
if o.current == nil {
o.Push(s)
o.Compact()
return
}
r := o.current.Value.(*hisItem)
r.Version = o.historyVer
if commit {
r.Source = s
if o.fd != nil {
// just report the error
_, err = o.fd.Write([]byte(string(r.Source) + "\n"))
}
} else {
r.Tmp = append(r.Tmp[:0], s...)
}
o.current.Value = r
o.Compact()
return
}
func (o *opHistory) Push(s []rune) {
s = runes.Copy(s)
elem := o.history.PushBack(&hisItem{Source: s})
o.current = elem
}

479
vendor/github.com/chzyer/readline/operation.go generated vendored Normal file
View File

@@ -0,0 +1,479 @@
package readline
import (
"errors"
"io"
)
var (
ErrInterrupt = errors.New("Interrupt")
)
type InterruptError struct {
Line []rune
}
func (*InterruptError) Error() string {
return "Interrupted"
}
type Operation struct {
cfg *Config
t *Terminal
buf *RuneBuffer
outchan chan []rune
errchan chan error
w io.Writer
history *opHistory
*opSearch
*opCompleter
*opPassword
*opVim
}
type wrapWriter struct {
r *Operation
t *Terminal
target io.Writer
}
func (w *wrapWriter) Write(b []byte) (int, error) {
if !w.t.IsReading() {
return w.target.Write(b)
}
var (
n int
err error
)
w.r.buf.Refresh(func() {
n, err = w.target.Write(b)
})
if w.r.IsSearchMode() {
w.r.SearchRefresh(-1)
}
if w.r.IsInCompleteMode() {
w.r.CompleteRefresh()
}
return n, err
}
func NewOperation(t *Terminal, cfg *Config) *Operation {
width := cfg.FuncGetWidth()
op := &Operation{
t: t,
buf: NewRuneBuffer(t, cfg.Prompt, cfg, width),
outchan: make(chan []rune),
errchan: make(chan error),
}
op.w = op.buf.w
op.SetConfig(cfg)
op.opVim = newVimMode(op)
op.opCompleter = newOpCompleter(op.buf.w, op, width)
op.opPassword = newOpPassword(op)
op.cfg.FuncOnWidthChanged(func() {
newWidth := cfg.FuncGetWidth()
op.opCompleter.OnWidthChange(newWidth)
op.opSearch.OnWidthChange(newWidth)
op.buf.OnWidthChange(newWidth)
})
go op.ioloop()
return op
}
func (o *Operation) SetPrompt(s string) {
o.buf.SetPrompt(s)
}
func (o *Operation) SetMaskRune(r rune) {
o.buf.SetMask(r)
}
func (o *Operation) ioloop() {
for {
keepInSearchMode := false
keepInCompleteMode := false
r := o.t.ReadRune()
if r == 0 { // io.EOF
if o.buf.Len() == 0 {
o.buf.Clean()
select {
case o.errchan <- io.EOF:
}
break
} else {
// if stdin got io.EOF and there is something left in buffer,
// let's flush them by sending CharEnter.
// And we will got io.EOF int next loop.
r = CharEnter
}
}
isUpdateHistory := true
if o.IsInCompleteSelectMode() {
keepInCompleteMode = o.HandleCompleteSelect(r)
if keepInCompleteMode {
continue
}
o.buf.Refresh(nil)
switch r {
case CharEnter, CharCtrlJ:
o.history.Update(o.buf.Runes(), false)
fallthrough
case CharInterrupt:
o.t.KickRead()
fallthrough
case CharBell:
continue
}
}
if o.IsEnableVimMode() {
r = o.HandleVim(r, o.t.ReadRune)
if r == 0 {
continue
}
}
switch r {
case CharBell:
if o.IsSearchMode() {
o.ExitSearchMode(true)
o.buf.Refresh(nil)
}
if o.IsInCompleteMode() {
o.ExitCompleteMode(true)
o.buf.Refresh(nil)
}
case CharTab:
if o.cfg.AutoComplete == nil {
o.t.Bell()
break
}
o.OnComplete()
keepInCompleteMode = true
case CharBckSearch:
o.SearchMode(S_DIR_BCK)
keepInSearchMode = true
case CharCtrlU:
o.buf.KillFront()
case CharFwdSearch:
o.SearchMode(S_DIR_FWD)
keepInSearchMode = true
case CharKill:
o.buf.Kill()
keepInCompleteMode = true
case MetaForward:
o.buf.MoveToNextWord()
case CharTranspose:
o.buf.Transpose()
case MetaBackward:
o.buf.MoveToPrevWord()
case MetaDelete:
o.buf.DeleteWord()
case CharLineStart:
o.buf.MoveToLineStart()
case CharLineEnd:
o.buf.MoveToLineEnd()
case CharBackspace, CharCtrlH:
if o.IsSearchMode() {
o.SearchBackspace()
keepInSearchMode = true
break
}
if o.buf.Len() == 0 {
o.t.Bell()
break
}
o.buf.Backspace()
if o.IsInCompleteMode() {
o.OnComplete()
}
case CharCtrlZ:
o.buf.Clean()
o.t.SleepToResume()
o.Refresh()
case CharCtrlL:
ClearScreen(o.w)
o.Refresh()
case MetaBackspace, CharCtrlW:
o.buf.BackEscapeWord()
case CharEnter, CharCtrlJ:
if o.IsSearchMode() {
o.ExitSearchMode(false)
}
o.buf.MoveToLineEnd()
var data []rune
if !o.cfg.UniqueEditLine {
o.buf.WriteRune('\n')
data = o.buf.Reset()
data = data[:len(data)-1] // trim \n
} else {
o.buf.Clean()
data = o.buf.Reset()
}
o.outchan <- data
if !o.cfg.DisableAutoSaveHistory {
// ignore IO error
_ = o.history.New(data)
} else {
isUpdateHistory = false
}
case CharBackward:
o.buf.MoveBackward()
case CharForward:
o.buf.MoveForward()
case CharPrev:
buf := o.history.Prev()
if buf != nil {
o.buf.Set(buf)
} else {
o.t.Bell()
}
case CharNext:
buf, ok := o.history.Next()
if ok {
o.buf.Set(buf)
} else {
o.t.Bell()
}
case CharDelete:
if o.buf.Len() > 0 || !o.IsNormalMode() {
o.t.KickRead()
if !o.buf.Delete() {
o.t.Bell()
}
break
}
// treat as EOF
if !o.cfg.UniqueEditLine {
o.buf.WriteString(o.cfg.EOFPrompt + "\n")
}
o.buf.Reset()
isUpdateHistory = false
o.history.Revert()
o.errchan <- io.EOF
if o.cfg.UniqueEditLine {
o.buf.Clean()
}
case CharInterrupt:
if o.IsSearchMode() {
o.t.KickRead()
o.ExitSearchMode(true)
break
}
if o.IsInCompleteMode() {
o.t.KickRead()
o.ExitCompleteMode(true)
o.buf.Refresh(nil)
break
}
o.buf.MoveToLineEnd()
o.buf.Refresh(nil)
hint := o.cfg.InterruptPrompt + "\n"
if !o.cfg.UniqueEditLine {
o.buf.WriteString(hint)
}
remain := o.buf.Reset()
if !o.cfg.UniqueEditLine {
remain = remain[:len(remain)-len([]rune(hint))]
}
isUpdateHistory = false
o.history.Revert()
o.errchan <- &InterruptError{remain}
default:
if o.IsSearchMode() {
o.SearchChar(r)
keepInSearchMode = true
break
}
o.buf.WriteRune(r)
if o.IsInCompleteMode() {
o.OnComplete()
keepInCompleteMode = true
}
}
if o.cfg.Listener != nil {
newLine, newPos, ok := o.cfg.Listener.OnChange(o.buf.Runes(), o.buf.Pos(), r)
if ok {
o.buf.SetWithIdx(newPos, newLine)
}
}
if !keepInSearchMode && o.IsSearchMode() {
o.ExitSearchMode(false)
o.buf.Refresh(nil)
} else if o.IsInCompleteMode() {
if !keepInCompleteMode {
o.ExitCompleteMode(false)
o.Refresh()
} else {
o.buf.Refresh(nil)
o.CompleteRefresh()
}
}
if isUpdateHistory && !o.IsSearchMode() {
// it will cause null history
o.history.Update(o.buf.Runes(), false)
}
}
}
func (o *Operation) Stderr() io.Writer {
return &wrapWriter{target: o.cfg.Stderr, r: o, t: o.t}
}
func (o *Operation) Stdout() io.Writer {
return &wrapWriter{target: o.cfg.Stdout, r: o, t: o.t}
}
func (o *Operation) String() (string, error) {
r, err := o.Runes()
return string(r), err
}
func (o *Operation) Runes() ([]rune, error) {
o.t.EnterRawMode()
defer o.t.ExitRawMode()
if o.cfg.Listener != nil {
o.cfg.Listener.OnChange(nil, 0, 0)
}
o.buf.Refresh(nil) // print prompt
o.t.KickRead()
select {
case r := <-o.outchan:
return r, nil
case err := <-o.errchan:
if e, ok := err.(*InterruptError); ok {
return e.Line, ErrInterrupt
}
return nil, err
}
}
func (o *Operation) PasswordEx(prompt string, l Listener) ([]byte, error) {
cfg := o.GenPasswordConfig()
cfg.Prompt = prompt
cfg.Listener = l
return o.PasswordWithConfig(cfg)
}
func (o *Operation) GenPasswordConfig() *Config {
return o.opPassword.PasswordConfig()
}
func (o *Operation) PasswordWithConfig(cfg *Config) ([]byte, error) {
if err := o.opPassword.EnterPasswordMode(cfg); err != nil {
return nil, err
}
defer o.opPassword.ExitPasswordMode()
return o.Slice()
}
func (o *Operation) Password(prompt string) ([]byte, error) {
return o.PasswordEx(prompt, nil)
}
func (o *Operation) SetTitle(t string) {
o.w.Write([]byte("\033[2;" + t + "\007"))
}
func (o *Operation) Slice() ([]byte, error) {
r, err := o.Runes()
if err != nil {
return nil, err
}
return []byte(string(r)), nil
}
func (o *Operation) Close() {
o.history.Close()
}
func (o *Operation) SetHistoryPath(path string) {
if o.history != nil {
o.history.Close()
}
o.cfg.HistoryFile = path
o.history = newOpHistory(o.cfg)
}
func (o *Operation) IsNormalMode() bool {
return !o.IsInCompleteMode() && !o.IsSearchMode()
}
func (op *Operation) SetConfig(cfg *Config) (*Config, error) {
if op.cfg == cfg {
return op.cfg, nil
}
if err := cfg.Init(); err != nil {
return op.cfg, err
}
old := op.cfg
op.cfg = cfg
op.SetPrompt(cfg.Prompt)
op.SetMaskRune(cfg.MaskRune)
op.buf.SetConfig(cfg)
width := op.cfg.FuncGetWidth()
if cfg.opHistory == nil {
op.SetHistoryPath(cfg.HistoryFile)
cfg.opHistory = op.history
cfg.opSearch = newOpSearch(op.buf.w, op.buf, op.history, cfg, width)
}
op.history = cfg.opHistory
// SetHistoryPath will close opHistory which already exists
// so if we use it next time, we need to reopen it by `InitHistory()`
op.history.Init()
if op.cfg.AutoComplete != nil {
op.opCompleter = newOpCompleter(op.buf.w, op, width)
}
op.opSearch = cfg.opSearch
return old, nil
}
func (o *Operation) ResetHistory() {
o.history.Reset()
}
// if err is not nil, it just mean it fail to write to file
// other things goes fine.
func (o *Operation) SaveHistory(content string) error {
return o.history.New([]rune(content))
}
func (o *Operation) Refresh() {
if o.t.IsReading() {
o.buf.Refresh(nil)
}
}
func (o *Operation) Clean() {
o.buf.Clean()
}
func FuncListener(f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)) Listener {
return &DumpListener{f: f}
}
type DumpListener struct {
f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)
}
func (d *DumpListener) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
return d.f(line, pos, key)
}
type Listener interface {
OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)
}

32
vendor/github.com/chzyer/readline/password.go generated vendored Normal file
View File

@@ -0,0 +1,32 @@
package readline
type opPassword struct {
o *Operation
backupCfg *Config
}
func newOpPassword(o *Operation) *opPassword {
return &opPassword{o: o}
}
func (o *opPassword) ExitPasswordMode() {
o.o.SetConfig(o.backupCfg)
o.backupCfg = nil
}
func (o *opPassword) EnterPasswordMode(cfg *Config) (err error) {
o.backupCfg, err = o.o.SetConfig(cfg)
return
}
func (o *opPassword) PasswordConfig() *Config {
return &Config{
EnableMask: true,
InterruptPrompt: "\n",
EOFPrompt: "\n",
HistoryLimit: -1,
Stdout: o.o.cfg.Stdout,
Stderr: o.o.cfg.Stderr,
}
}

125
vendor/github.com/chzyer/readline/rawreader_windows.go generated vendored Normal file
View File

@@ -0,0 +1,125 @@
// +build windows
package readline
import "unsafe"
const (
VK_CANCEL = 0x03
VK_BACK = 0x08
VK_TAB = 0x09
VK_RETURN = 0x0D
VK_SHIFT = 0x10
VK_CONTROL = 0x11
VK_MENU = 0x12
VK_ESCAPE = 0x1B
VK_LEFT = 0x25
VK_UP = 0x26
VK_RIGHT = 0x27
VK_DOWN = 0x28
VK_DELETE = 0x2E
VK_LSHIFT = 0xA0
VK_RSHIFT = 0xA1
VK_LCONTROL = 0xA2
VK_RCONTROL = 0xA3
)
// RawReader translate input record to ANSI escape sequence.
// To provides same behavior as unix terminal.
type RawReader struct {
ctrlKey bool
altKey bool
}
func NewRawReader() *RawReader {
r := new(RawReader)
return r
}
// only process one action in one read
func (r *RawReader) Read(buf []byte) (int, error) {
ir := new(_INPUT_RECORD)
var read int
var err error
next:
err = kernel.ReadConsoleInputW(stdin,
uintptr(unsafe.Pointer(ir)),
1,
uintptr(unsafe.Pointer(&read)),
)
if err != nil {
return 0, err
}
if ir.EventType != EVENT_KEY {
goto next
}
ker := (*_KEY_EVENT_RECORD)(unsafe.Pointer(&ir.Event[0]))
if ker.bKeyDown == 0 { // keyup
if r.ctrlKey || r.altKey {
switch ker.wVirtualKeyCode {
case VK_RCONTROL, VK_LCONTROL:
r.ctrlKey = false
case VK_MENU: //alt
r.altKey = false
}
}
goto next
}
if ker.unicodeChar == 0 {
var target rune
switch ker.wVirtualKeyCode {
case VK_RCONTROL, VK_LCONTROL:
r.ctrlKey = true
case VK_MENU: //alt
r.altKey = true
case VK_LEFT:
target = CharBackward
case VK_RIGHT:
target = CharForward
case VK_UP:
target = CharPrev
case VK_DOWN:
target = CharNext
}
if target != 0 {
return r.write(buf, target)
}
goto next
}
char := rune(ker.unicodeChar)
if r.ctrlKey {
switch char {
case 'A':
char = CharLineStart
case 'E':
char = CharLineEnd
case 'R':
char = CharBckSearch
case 'S':
char = CharFwdSearch
}
} else if r.altKey {
switch char {
case VK_BACK:
char = CharBackspace
}
return r.writeEsc(buf, char)
}
return r.write(buf, char)
}
func (r *RawReader) writeEsc(b []byte, char rune) (int, error) {
b[0] = '\033'
n := copy(b[1:], []byte(string(char)))
return n + 1, nil
}
func (r *RawReader) write(b []byte, char rune) (int, error) {
n := copy(b, []byte(string(char)))
return n, nil
}
func (r *RawReader) Close() error {
return nil
}

257
vendor/github.com/chzyer/readline/readline.go generated vendored Normal file
View File

@@ -0,0 +1,257 @@
package readline
import "io"
type Instance struct {
Config *Config
Terminal *Terminal
Operation *Operation
}
type Config struct {
// prompt supports ANSI escape sequence, so we can color some characters even in windows
Prompt string
// readline will persist historys to file where HistoryFile specified
HistoryFile string
// specify the max length of historys, it's 500 by default, set it to -1 to disable history
HistoryLimit int
DisableAutoSaveHistory bool
// AutoCompleter will called once user press TAB
AutoComplete AutoCompleter
// Any key press will pass to Listener
// NOTE: Listener will be triggered by (nil, 0, 0) immediately
Listener Listener
// If VimMode is true, readline will in vim.insert mode by default
VimMode bool
InterruptPrompt string
EOFPrompt string
FuncGetWidth func() int
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
EnableMask bool
MaskRune rune
// erase the editing line after user submited it
// it use in IM usually.
UniqueEditLine bool
// force use interactive even stdout is not a tty
FuncIsTerminal func() bool
FuncMakeRaw func() error
FuncExitRaw func() error
FuncOnWidthChanged func(func())
ForceUseInteractive bool
// private fields
inited bool
opHistory *opHistory
opSearch *opSearch
}
func (c *Config) useInteractive() bool {
if c.ForceUseInteractive {
return true
}
return c.FuncIsTerminal()
}
func (c *Config) Init() error {
if c.inited {
return nil
}
c.inited = true
if c.Stdin == nil {
c.Stdin = Stdin
}
if c.Stdout == nil {
c.Stdout = Stdout
}
if c.Stderr == nil {
c.Stderr = Stderr
}
if c.HistoryLimit == 0 {
c.HistoryLimit = 500
}
if c.InterruptPrompt == "" {
c.InterruptPrompt = "^C"
} else if c.InterruptPrompt == "\n" {
c.InterruptPrompt = ""
}
if c.EOFPrompt == "" {
c.EOFPrompt = "^D"
} else if c.EOFPrompt == "\n" {
c.EOFPrompt = ""
}
if c.FuncGetWidth == nil {
c.FuncGetWidth = GetScreenWidth
}
if c.FuncIsTerminal == nil {
c.FuncIsTerminal = DefaultIsTerminal
}
rm := new(RawMode)
if c.FuncMakeRaw == nil {
c.FuncMakeRaw = rm.Enter
}
if c.FuncExitRaw == nil {
c.FuncExitRaw = rm.Exit
}
if c.FuncOnWidthChanged == nil {
c.FuncOnWidthChanged = DefaultOnWidthChanged
}
return nil
}
func (c Config) Clone() *Config {
c.opHistory = nil
c.opSearch = nil
return &c
}
func (c *Config) SetListener(f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)) {
c.Listener = FuncListener(f)
}
func NewEx(cfg *Config) (*Instance, error) {
t, err := NewTerminal(cfg)
if err != nil {
return nil, err
}
rl := t.Readline()
return &Instance{
Config: cfg,
Terminal: t,
Operation: rl,
}, nil
}
func New(prompt string) (*Instance, error) {
return NewEx(&Config{Prompt: prompt})
}
func (i *Instance) ResetHistory() {
i.Operation.ResetHistory()
}
func (i *Instance) SetPrompt(s string) {
i.Operation.SetPrompt(s)
}
func (i *Instance) SetMaskRune(r rune) {
i.Operation.SetMaskRune(r)
}
// change history persistence in runtime
func (i *Instance) SetHistoryPath(p string) {
i.Operation.SetHistoryPath(p)
}
// readline will refresh automatic when write through Stdout()
func (i *Instance) Stdout() io.Writer {
return i.Operation.Stdout()
}
// readline will refresh automatic when write through Stdout()
func (i *Instance) Stderr() io.Writer {
return i.Operation.Stderr()
}
// switch VimMode in runtime
func (i *Instance) SetVimMode(on bool) {
i.Operation.SetVimMode(on)
}
func (i *Instance) IsVimMode() bool {
return i.Operation.IsEnableVimMode()
}
func (i *Instance) GenPasswordConfig() *Config {
return i.Operation.GenPasswordConfig()
}
// we can generate a config by `i.GenPasswordConfig()`
func (i *Instance) ReadPasswordWithConfig(cfg *Config) ([]byte, error) {
return i.Operation.PasswordWithConfig(cfg)
}
func (i *Instance) ReadPasswordEx(prompt string, l Listener) ([]byte, error) {
return i.Operation.PasswordEx(prompt, l)
}
func (i *Instance) ReadPassword(prompt string) ([]byte, error) {
return i.Operation.Password(prompt)
}
type Result struct {
Line string
Error error
}
func (l *Result) CanContinue() bool {
return len(l.Line) != 0 && l.Error == ErrInterrupt
}
func (l *Result) CanBreak() bool {
return !l.CanContinue() && l.Error != nil
}
func (i *Instance) Line() *Result {
ret, err := i.Readline()
return &Result{ret, err}
}
// err is one of (nil, io.EOF, readline.ErrInterrupt)
func (i *Instance) Readline() (string, error) {
return i.Operation.String()
}
func (i *Instance) SaveHistory(content string) error {
return i.Operation.SaveHistory(content)
}
// same as readline
func (i *Instance) ReadSlice() ([]byte, error) {
return i.Operation.Slice()
}
// we must make sure that call Close() before process exit.
func (i *Instance) Close() error {
if err := i.Terminal.Close(); err != nil {
return err
}
i.Operation.Close()
return nil
}
func (i *Instance) Clean() {
i.Operation.Clean()
}
func (i *Instance) Write(b []byte) (int, error) {
return i.Stdout().Write(b)
}
func (i *Instance) SetConfig(cfg *Config) *Config {
if i.Config == cfg {
return cfg
}
old := i.Config
i.Config = cfg
i.Operation.SetConfig(cfg)
i.Terminal.SetConfig(cfg)
return old
}
func (i *Instance) Refresh() {
i.Operation.Refresh()
}

474
vendor/github.com/chzyer/readline/remote.go generated vendored Normal file
View File

@@ -0,0 +1,474 @@
package readline
import (
"bufio"
"bytes"
"encoding/binary"
"fmt"
"io"
"net"
"os"
"sync"
"sync/atomic"
)
type MsgType int16
const (
T_DATA = MsgType(iota)
T_WIDTH
T_WIDTH_REPORT
T_ISTTY_REPORT
T_RAW
T_ERAW // exit raw
T_EOF
)
type RemoteSvr struct {
eof int32
closed int32
width int32
reciveChan chan struct{}
writeChan chan *writeCtx
conn net.Conn
isTerminal bool
funcWidthChan func()
stopChan chan struct{}
dataBufM sync.Mutex
dataBuf bytes.Buffer
}
type writeReply struct {
n int
err error
}
type writeCtx struct {
msg *Message
reply chan *writeReply
}
func newWriteCtx(msg *Message) *writeCtx {
return &writeCtx{
msg: msg,
reply: make(chan *writeReply),
}
}
func NewRemoteSvr(conn net.Conn) (*RemoteSvr, error) {
rs := &RemoteSvr{
width: -1,
conn: conn,
writeChan: make(chan *writeCtx),
reciveChan: make(chan struct{}),
stopChan: make(chan struct{}),
}
buf := bufio.NewReader(rs.conn)
if err := rs.init(buf); err != nil {
return nil, err
}
go rs.readLoop(buf)
go rs.writeLoop()
return rs, nil
}
func (r *RemoteSvr) init(buf *bufio.Reader) error {
m, err := ReadMessage(buf)
if err != nil {
return err
}
// receive isTerminal
if m.Type != T_ISTTY_REPORT {
return fmt.Errorf("unexpected init message")
}
r.GotIsTerminal(m.Data)
// receive width
m, err = ReadMessage(buf)
if err != nil {
return err
}
if m.Type != T_WIDTH_REPORT {
return fmt.Errorf("unexpected init message")
}
r.GotReportWidth(m.Data)
return nil
}
func (r *RemoteSvr) HandleConfig(cfg *Config) {
cfg.Stderr = r
cfg.Stdout = r
cfg.Stdin = r
cfg.FuncExitRaw = r.ExitRawMode
cfg.FuncIsTerminal = r.IsTerminal
cfg.FuncMakeRaw = r.EnterRawMode
cfg.FuncExitRaw = r.ExitRawMode
cfg.FuncGetWidth = r.GetWidth
cfg.FuncOnWidthChanged = func(f func()) {
r.funcWidthChan = f
}
}
func (r *RemoteSvr) IsTerminal() bool {
return r.isTerminal
}
func (r *RemoteSvr) checkEOF() error {
if atomic.LoadInt32(&r.eof) == 1 {
return io.EOF
}
return nil
}
func (r *RemoteSvr) Read(b []byte) (int, error) {
r.dataBufM.Lock()
n, err := r.dataBuf.Read(b)
r.dataBufM.Unlock()
if n == 0 {
if err := r.checkEOF(); err != nil {
return 0, err
}
}
if n == 0 && err == io.EOF {
<-r.reciveChan
r.dataBufM.Lock()
n, err = r.dataBuf.Read(b)
r.dataBufM.Unlock()
}
if n == 0 {
if err := r.checkEOF(); err != nil {
return 0, err
}
}
return n, err
}
func (r *RemoteSvr) writeMsg(m *Message) error {
ctx := newWriteCtx(m)
r.writeChan <- ctx
reply := <-ctx.reply
return reply.err
}
func (r *RemoteSvr) Write(b []byte) (int, error) {
ctx := newWriteCtx(NewMessage(T_DATA, b))
r.writeChan <- ctx
reply := <-ctx.reply
return reply.n, reply.err
}
func (r *RemoteSvr) EnterRawMode() error {
return r.writeMsg(NewMessage(T_RAW, nil))
}
func (r *RemoteSvr) ExitRawMode() error {
return r.writeMsg(NewMessage(T_ERAW, nil))
}
func (r *RemoteSvr) writeLoop() {
defer r.Close()
loop:
for {
select {
case ctx, ok := <-r.writeChan:
if !ok {
break
}
n, err := ctx.msg.WriteTo(r.conn)
ctx.reply <- &writeReply{n, err}
case <-r.stopChan:
break loop
}
}
}
func (r *RemoteSvr) Close() {
if atomic.CompareAndSwapInt32(&r.closed, 0, 1) {
close(r.stopChan)
r.conn.Close()
}
}
func (r *RemoteSvr) readLoop(buf *bufio.Reader) {
defer r.Close()
for {
m, err := ReadMessage(buf)
if err != nil {
break
}
switch m.Type {
case T_EOF:
atomic.StoreInt32(&r.eof, 1)
select {
case r.reciveChan <- struct{}{}:
default:
}
case T_DATA:
r.dataBufM.Lock()
r.dataBuf.Write(m.Data)
r.dataBufM.Unlock()
select {
case r.reciveChan <- struct{}{}:
default:
}
case T_WIDTH_REPORT:
r.GotReportWidth(m.Data)
case T_ISTTY_REPORT:
r.GotIsTerminal(m.Data)
}
}
}
func (r *RemoteSvr) GotIsTerminal(data []byte) {
if binary.BigEndian.Uint16(data) == 0 {
r.isTerminal = false
} else {
r.isTerminal = true
}
}
func (r *RemoteSvr) GotReportWidth(data []byte) {
atomic.StoreInt32(&r.width, int32(binary.BigEndian.Uint16(data)))
if r.funcWidthChan != nil {
r.funcWidthChan()
}
}
func (r *RemoteSvr) GetWidth() int {
return int(atomic.LoadInt32(&r.width))
}
// -----------------------------------------------------------------------------
type Message struct {
Type MsgType
Data []byte
}
func ReadMessage(r io.Reader) (*Message, error) {
m := new(Message)
var length int32
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &m.Type); err != nil {
return nil, err
}
m.Data = make([]byte, int(length)-2)
if _, err := io.ReadFull(r, m.Data); err != nil {
return nil, err
}
return m, nil
}
func NewMessage(t MsgType, data []byte) *Message {
return &Message{t, data}
}
func (m *Message) WriteTo(w io.Writer) (int, error) {
buf := bytes.NewBuffer(make([]byte, 0, len(m.Data)+2+4))
binary.Write(buf, binary.BigEndian, int32(len(m.Data)+2))
binary.Write(buf, binary.BigEndian, m.Type)
buf.Write(m.Data)
n, err := buf.WriteTo(w)
return int(n), err
}
// -----------------------------------------------------------------------------
type RemoteCli struct {
conn net.Conn
raw RawMode
receiveChan chan struct{}
inited int32
isTerminal *bool
data bytes.Buffer
dataM sync.Mutex
}
func NewRemoteCli(conn net.Conn) (*RemoteCli, error) {
r := &RemoteCli{
conn: conn,
receiveChan: make(chan struct{}),
}
return r, nil
}
func (r *RemoteCli) MarkIsTerminal(is bool) {
r.isTerminal = &is
}
func (r *RemoteCli) init() error {
if !atomic.CompareAndSwapInt32(&r.inited, 0, 1) {
return nil
}
if err := r.reportIsTerminal(); err != nil {
return err
}
if err := r.reportWidth(); err != nil {
return err
}
// register sig for width changed
DefaultOnWidthChanged(func() {
r.reportWidth()
})
return nil
}
func (r *RemoteCli) writeMsg(m *Message) error {
r.dataM.Lock()
_, err := m.WriteTo(r.conn)
r.dataM.Unlock()
return err
}
func (r *RemoteCli) Write(b []byte) (int, error) {
m := NewMessage(T_DATA, b)
r.dataM.Lock()
_, err := m.WriteTo(r.conn)
r.dataM.Unlock()
return len(b), err
}
func (r *RemoteCli) reportWidth() error {
screenWidth := GetScreenWidth()
data := make([]byte, 2)
binary.BigEndian.PutUint16(data, uint16(screenWidth))
msg := NewMessage(T_WIDTH_REPORT, data)
if err := r.writeMsg(msg); err != nil {
return err
}
return nil
}
func (r *RemoteCli) reportIsTerminal() error {
var isTerminal bool
if r.isTerminal != nil {
isTerminal = *r.isTerminal
} else {
isTerminal = DefaultIsTerminal()
}
data := make([]byte, 2)
if isTerminal {
binary.BigEndian.PutUint16(data, 1)
} else {
binary.BigEndian.PutUint16(data, 0)
}
msg := NewMessage(T_ISTTY_REPORT, data)
if err := r.writeMsg(msg); err != nil {
return err
}
return nil
}
func (r *RemoteCli) readLoop() {
buf := bufio.NewReader(r.conn)
for {
msg, err := ReadMessage(buf)
if err != nil {
break
}
switch msg.Type {
case T_ERAW:
r.raw.Exit()
case T_RAW:
r.raw.Enter()
case T_DATA:
os.Stdout.Write(msg.Data)
}
}
}
func (r *RemoteCli) ServeBy(source io.Reader) error {
if err := r.init(); err != nil {
return err
}
go func() {
defer r.Close()
for {
n, _ := io.Copy(r, source)
if n == 0 {
break
}
}
}()
defer r.raw.Exit()
r.readLoop()
return nil
}
func (r *RemoteCli) Close() {
r.writeMsg(NewMessage(T_EOF, nil))
}
func (r *RemoteCli) Serve() error {
return r.ServeBy(os.Stdin)
}
func ListenRemote(n, addr string, cfg *Config, h func(*Instance), onListen ...func(net.Listener) error) error {
ln, err := net.Listen(n, addr)
if err != nil {
return err
}
if len(onListen) > 0 {
if err := onListen[0](ln); err != nil {
return err
}
}
for {
conn, err := ln.Accept()
if err != nil {
break
}
go func() {
defer conn.Close()
rl, err := HandleConn(*cfg, conn)
if err != nil {
return
}
h(rl)
}()
}
return nil
}
func HandleConn(cfg Config, conn net.Conn) (*Instance, error) {
r, err := NewRemoteSvr(conn)
if err != nil {
return nil, err
}
r.HandleConfig(&cfg)
rl, err := NewEx(&cfg)
if err != nil {
return nil, err
}
return rl, nil
}
func DialRemote(n, addr string) error {
conn, err := net.Dial(n, addr)
if err != nil {
return err
}
defer conn.Close()
cli, err := NewRemoteCli(conn)
if err != nil {
return err
}
return cli.Serve()
}

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