mirror of
https://github.com/stefanprodan/podinfo.git
synced 2026-04-09 12:36:50 +00:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02e5f233d0 | ||
|
|
b89f46ac04 | ||
|
|
59cd692141 | ||
|
|
bcd61428d1 | ||
|
|
f8ec9c0947 | ||
|
|
6c98fbf1f4 | ||
|
|
54f6d9f74d | ||
|
|
1d35304d9d | ||
|
|
457a56f71a | ||
|
|
fbcab6cf56 | ||
|
|
0126282669 | ||
|
|
ff1fb39f43 | ||
|
|
84f0e1c9e2 | ||
|
|
3eb4cc90f9 | ||
|
|
b6c3d36bde | ||
|
|
a8a85e6aae | ||
|
|
79b2d784bf | ||
|
|
bfd35f6cc0 | ||
|
|
f1775ba090 | ||
|
|
7a2d59de8e | ||
|
|
8191871761 | ||
|
|
36bb719b1c | ||
|
|
ecd204b15e | ||
|
|
979fd669df | ||
|
|
feac686e60 | ||
|
|
d362dc5f81 | ||
|
|
593ccaa0cd | ||
|
|
0f098cf0f1 | ||
|
|
2ddbc03371 | ||
|
|
f2d95bbf80 | ||
|
|
7d18ec68b3 | ||
|
|
774d34c1dd | ||
|
|
f13d006993 | ||
|
|
aeeb146c2a | ||
|
|
11bd74eff2 | ||
|
|
af6d11fd33 | ||
|
|
49746fe2fb | ||
|
|
da24d729bb | ||
|
|
449fcca3a9 | ||
|
|
2b0a742974 | ||
|
|
153f4dce45 | ||
|
|
4c8d11cc3e | ||
|
|
08415ce2ce | ||
|
|
d26b7a96d9 | ||
|
|
3c897b8bd7 | ||
|
|
511ab87a18 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,3 +18,4 @@
|
||||
release/
|
||||
build/
|
||||
gcloud/
|
||||
dist/
|
||||
|
||||
21
.goreleaser.yml
Normal file
21
.goreleaser.yml
Normal 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*
|
||||
18
.travis.yml
18
.travis.yml
@@ -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
|
||||
|
||||
@@ -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
281
Gopkg.lock
generated
@@ -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
|
||||
|
||||
33
Gopkg.toml
33
Gopkg.toml
@@ -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
|
||||
|
||||
10
Makefile
10
Makefile
@@ -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:
|
||||
|
||||
32
README.md
32
README.md
@@ -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
|
||||
|
||||
|
||||
1636
charts/grafana/dashboards/istio-bluegreen.json
Normal file
1636
charts/grafana/dashboards/istio-bluegreen.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
245
cmd/podcli/check.go
Normal 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
39
cmd/podcli/main.go
Normal 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
21
cmd/podcli/version.go
Normal 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
143
cmd/podcli/ws.go
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
19
deploy/istio-system/openfaas-canary-virtual-service.yaml
Normal file
19
deploy/istio-system/openfaas-canary-virtual-service.yaml
Normal 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
|
||||
|
||||
51
deploy/istio-system/openfaas-mtls.yaml
Normal file
51
deploy/istio-system/openfaas-mtls.yaml
Normal 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
|
||||
13
deploy/istio-system/openfaas-ns.yaml
Normal file
13
deploy/istio-system/openfaas-ns.yaml
Normal 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
|
||||
55
deploy/istio-system/openfaas-policies.yaml
Normal file
55
deploy/istio-system/openfaas-policies.yaml
Normal 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 ]
|
||||
17
deploy/istio-system/openfaas-virtual-service.yaml
Normal file
17
deploy/istio-system/openfaas-virtual-service.yaml
Normal 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
478
docs/8-istio-openfaas.md
Normal 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:
|
||||
|
||||

|
||||
|
||||
Tracing the canary release:
|
||||
|
||||

|
||||
|
||||
Monitor ga vs canary success rate and latency with Prometheus and Grafana:
|
||||
|
||||

|
||||
|
||||
Binary file not shown.
Binary file not shown.
112
docs/index.yaml
112
docs/index.yaml
@@ -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
BIN
docs/podinfo-1.0.0.tgz
Normal file
Binary file not shown.
BIN
docs/podinfo-1.1.0.tgz
Normal file
BIN
docs/podinfo-1.1.0.tgz
Normal file
Binary file not shown.
BIN
docs/podinfo-1.2.0.tgz
Normal file
BIN
docs/podinfo-1.2.0.tgz
Normal file
Binary file not shown.
Binary file not shown.
BIN
docs/podinfo-istio-0.2.0.tgz
Normal file
BIN
docs/podinfo-istio-0.2.0.tgz
Normal file
Binary file not shown.
BIN
docs/podinfo-istio-1.2.0.tgz
Normal file
BIN
docs/podinfo-istio-1.2.0.tgz
Normal file
Binary file not shown.
BIN
docs/screens/openfaas-istio-canary-prom.png
Normal file
BIN
docs/screens/openfaas-istio-canary-prom.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 387 KiB |
BIN
docs/screens/openfaas-istio-canary-trace.png
Normal file
BIN
docs/screens/openfaas-istio-canary-trace.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 527 KiB |
BIN
docs/screens/openfaas-istio-ga-trace.png
Normal file
BIN
docs/screens/openfaas-istio-ga-trace.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 490 KiB |
15
pkg/api/configs.go
Normal file
15
pkg/api/configs.go
Normal 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
23
pkg/api/delay.go
Normal 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
35
pkg/api/delay_test.go
Normal 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
79
pkg/api/echo.go
Normal 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
34
pkg/api/echo_test.go
Normal 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
82
pkg/api/echows.go
Normal 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
11
pkg/api/env.go
Normal 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
35
pkg/api/env_test.go
Normal 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
10
pkg/api/headers.go
Normal 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
36
pkg/api/headers_test.go
Normal 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
32
pkg/api/health.go
Normal 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
45
pkg/api/health_test.go
Normal 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
122
pkg/api/http.go
Normal 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
26
pkg/api/index.go
Normal 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
38
pkg/api/info.go
Normal 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
35
pkg/api/info_test.go
Normal 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
31
pkg/api/logging.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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
32
pkg/api/mock.go
Normal 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
10
pkg/api/panic.go
Normal 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
184
pkg/api/server.go
Normal 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
20
pkg/api/status.go
Normal 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
26
pkg/api/status_test.go
Normal 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
48
pkg/api/store.go
Normal 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
102
pkg/api/token.go
Normal 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
15
pkg/api/version.go
Normal 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
36
pkg/api/version_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package version
|
||||
|
||||
var VERSION = "0.6.1"
|
||||
var GITCOMMIT = "unknown"
|
||||
var VERSION = "1.2.0"
|
||||
var REVISION = "unknown"
|
||||
|
||||
@@ -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>
|
||||
|
||||
34
vendor/github.com/beorn7/perks/quantile/stream.go
generated
vendored
34
vendor/github.com/beorn7/perks/quantile/stream.go
generated
vendored
@@ -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
2
vendor/github.com/chzyer/readline/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.exe
|
||||
*.tmp
|
||||
10
vendor/github.com/chzyer/readline/.travis.yml
generated
vendored
Normal file
10
vendor/github.com/chzyer/readline/.travis.yml
generated
vendored
Normal 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
58
vendor/github.com/chzyer/readline/CHANGELOG.md
generated
vendored
Normal 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
|
||||
5
vendor/github.com/rs/zerolog/LICENSE → vendor/github.com/chzyer/readline/LICENSE
generated
vendored
5
vendor/github.com/rs/zerolog/LICENSE → vendor/github.com/chzyer/readline/LICENSE
generated
vendored
@@ -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
306
vendor/github.com/chzyer/readline/README.md
generated
vendored
Normal file
@@ -0,0 +1,306 @@
|
||||
# readline
|
||||
|
||||
[](LICENSE.md)
|
||||
[](https://travis-ci.org/chzyer/readline)
|
||||
[](https://godoc.org/github.com/chzyer/readline)
|
||||
[](https://gitter.im/chzyer/readline?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](#backers)
|
||||
[](#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
|
||||
|
||||

|
||||
|
||||
Also works fine in windows
|
||||
|
||||

|
||||
|
||||
|
||||
* [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
246
vendor/github.com/chzyer/readline/ansi_windows.go
generated
vendored
Normal 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
36
vendor/github.com/chzyer/readline/char.go
generated
vendored
Normal 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
273
vendor/github.com/chzyer/readline/complete.go
generated
vendored
Normal 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
165
vendor/github.com/chzyer/readline/complete_helper.go
generated
vendored
Normal 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
82
vendor/github.com/chzyer/readline/complete_segment.go
generated
vendored
Normal 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
29
vendor/github.com/chzyer/readline/debug.go
generated
vendored
Normal 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
20
vendor/github.com/chzyer/readline/doc.go
generated
vendored
Normal 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
296
vendor/github.com/chzyer/readline/history.go
generated
vendored
Normal 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
479
vendor/github.com/chzyer/readline/operation.go
generated
vendored
Normal 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
32
vendor/github.com/chzyer/readline/password.go
generated
vendored
Normal 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
125
vendor/github.com/chzyer/readline/rawreader_windows.go
generated
vendored
Normal 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
257
vendor/github.com/chzyer/readline/readline.go
generated
vendored
Normal 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
474
vendor/github.com/chzyer/readline/remote.go
generated
vendored
Normal 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
Reference in New Issue
Block a user