From baa297550c9c341902a9caaabd66a2e7444b6307 Mon Sep 17 00:00:00 2001 From: Aylei Date: Wed, 17 Apr 2019 19:57:36 +0800 Subject: [PATCH] Support debug and ctop commands in CLI (#387) * Abandon vendor and refresh go.sum Signed-off-by: Aylei * Format .gitignore Signed-off-by: Aylei * CLI skeleton, update kuberetes dependecies to 1.12.5 Signed-off-by: Aylei * Add use command for tkc Signed-off-by: Aylei * Support info command, support list command Signed-off-by: Aylei * Remoave un-intentionally added files Signed-off-by: Aylei * Support get command in tkc Signed-off-by: Aylei * Lint formats Signed-off-by: Aylei * Support debug and ctop commands in CLI Signed-off-by: Aylei * Bump golang to 1.12 in tidb-debug image Signed-off-by: Aylei * Change checksum accordingly Signed-off-by: Aylei --- Makefile | 22 +- ci/release_cli_binary_and_debug_image.groovy | 60 +++++ go.mod | 13 +- go.sum | 18 ++ misc/cmd/debug-launcher/main.go | 34 +++ misc/images/debug-launcher/Dockerfile | 7 + misc/images/debug-launcher/entry-point.sh | 4 + misc/images/tidb-control/Dockerfile | 14 ++ misc/images/tidb-control/README.md | 48 ++++ misc/images/tidb-control/banner | 9 + misc/images/tidb-control/profile | 18 ++ misc/images/tidb-debug/Dockerfile | 54 +++++ misc/images/tidb-debug/README.md | 9 + misc/images/tidb-debug/banner | 9 + misc/images/tidb-debug/profile | 18 ++ misc/images/tidb-debug/run_flamegraph.sh | 9 + pkg/tkctl/cmd/cmd.go | 2 - pkg/tkctl/cmd/ctop/ctop.go | 173 ++++++++++++++- pkg/tkctl/cmd/debug/debug.go | 222 ++++++++++++++++--- pkg/tkctl/config/config.go | 2 +- pkg/tkctl/debug/hijack.go | 216 ++++++++++++++++++ pkg/tkctl/debug/launcher.go | 185 ++++++++++++++++ pkg/tkctl/executor/executor.go | 143 ++++++++++++ pkg/tkctl/streams/in.go | 69 ++++++ pkg/tkctl/streams/out.go | 63 ++++++ pkg/tkctl/streams/stream.go | 47 ++++ pkg/tkctl/util/util.go | 25 ++- pkg/tkctl/util/util_test.go | 56 +++++ 28 files changed, 1499 insertions(+), 50 deletions(-) create mode 100644 ci/release_cli_binary_and_debug_image.groovy create mode 100644 misc/cmd/debug-launcher/main.go create mode 100644 misc/images/debug-launcher/Dockerfile create mode 100755 misc/images/debug-launcher/entry-point.sh create mode 100644 misc/images/tidb-control/Dockerfile create mode 100644 misc/images/tidb-control/README.md create mode 100644 misc/images/tidb-control/banner create mode 100644 misc/images/tidb-control/profile create mode 100644 misc/images/tidb-debug/Dockerfile create mode 100644 misc/images/tidb-debug/README.md create mode 100644 misc/images/tidb-debug/banner create mode 100644 misc/images/tidb-debug/profile create mode 100644 misc/images/tidb-debug/run_flamegraph.sh create mode 100644 pkg/tkctl/debug/hijack.go create mode 100644 pkg/tkctl/debug/launcher.go create mode 100644 pkg/tkctl/executor/executor.go create mode 100644 pkg/tkctl/streams/in.go create mode 100644 pkg/tkctl/streams/out.go create mode 100644 pkg/tkctl/streams/stream.go create mode 100644 pkg/tkctl/util/util_test.go diff --git a/Makefile b/Makefile index 4f360a190e0..2f5a2f4b383 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,9 @@ ifeq ($(GO111), 1) $(error Please upgrade your Go compiler to 1.11 or higher version) endif -GOENV := GO15VENDOREXPERIMENT="1" GO111MODULE=on CGO_ENABLED=0 GOOS=linux GOARCH=amd64 +GOOS := $(if $(GOOS),$(GOOS),linux) +GOARCH := $(if $(GOARCH),$(GOARCH),amd64) +GOENV := GO15VENDOREXPERIMENT="1" GO111MODULE=on CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) GO := $(GOENV) go build GOTEST := CGO_ENABLED=0 GO111MODULE=on go test -v -cover @@ -123,4 +125,20 @@ check-gosec: @echo "security checking" CGO_ENABLED=0 retool do gosec $$($(PACKAGE_DIRECTORIES)) -.PHONY: check check-setup check-all build e2e-build +cli: + $(GO) -ldflags '$(LDFLAGS)' -o tkc cmd/tkctl/main.go + +debug-docker-push: debug-build-docker + docker push "${DOCKER_REGISTRY}/pingcap/debug-launcher:latest" + docker push "${DOCKER_REGISTRY}/pingcap/tidb-control:latest" + docker push "${DOCKER_REGISTRY}/pingcap/tidb-debug:latest" + +debug-build-docker: debug-build + docker build -t "${DOCKER_REGISTRY}/pingcap/debug-launcher:latest" misc/images/debug-launcher + docker build -t "${DOCKER_REGISTRY}/pingcap/tidb-control:latest" misc/images/tidb-control + docker build -t "${DOCKER_REGISTRY}/pingcap/tidb-debug:latest" misc/images/tidb-debug + +debug-build: + $(GO) -ldflags '$(LDFLAGS)' -o misc/images/debug-launcher/bin/debug-launcher misc/cmd/debug-launcher/main.go + +.PHONY: check check-setup check-all build e2e-build debug-build cli diff --git a/ci/release_cli_binary_and_debug_image.groovy b/ci/release_cli_binary_and_debug_image.groovy new file mode 100644 index 00000000000..b2c3e0e0699 --- /dev/null +++ b/ci/release_cli_binary_and_debug_image.groovy @@ -0,0 +1,60 @@ +def call(BUILD_BRANCH, RELEASE_TAG) { + + env.GOPATH = "/go" + env.GOROOT = "/usr/local/go" + env.PATH = "${env.GOROOT}/bin:${env.GOPATH}/bin:/bin:${env.PATH}:/home/jenkins/bin" + + def GITHASH + def TKC_CLI_PACKAGE = "tkc-${GOOS}-${GOARCH}-${RELEASE_TAG}" + + catchError { + node('k8s_centos7_build') { + def WORKSPACE = pwd() + + dir("${WORKSPACE}/operator"){ + stage('Build and release CLI to qiniu'){ + checkout([$class: 'GitSCM', branches: [[name: "${BUILD_BRANCH}"]], userRemoteConfigs:[[url: "${BUILD_URL}", credentialsId: "${CREDENTIALS_ID}"]]]) + GITHASH = sh(returnStdout: true, script: "git rev-parse HEAD").trim() + def GOARCH = "amd64" + ["linux", "darwin", "windows"].each { + sh """ + GOOS=${it} GOARCH=${GOARCH} make cli + tar -zcf ${TKC_CLI_PACKAGE}.tgz tkc + sha256sum ${TKC_CLI_PACKAGE}.tgz > ${TKC_CLI_PACKAGE}.sha256 + + upload.py ${TKC_CLI_PACKAGE}.tgz ${TKC_CLI_PACKAGE}.tgz + upload.py ${TKC_CLI_PACKAGE}.sha256 ${TKC_CLI_PACKAGE}.sha256 + """ + } + } + + stage('Build and push debug images'){ + withDockerServer([uri: "${env.DOCKER_HOST}"]) { + DOCKER_REGISTRY="" make debug-docker-push + DOCKER_REGISTRY="uhub.service.ucloud.cn" make debug-docker-push + } + } + } + } + currentBuild.result = "SUCCESS" + } + stage('Summary') { + echo("echo summary info ########") + def DURATION = ((System.currentTimeMillis() - currentBuild.startTimeInMillis) / 1000 / 60).setScale(2, BigDecimal.ROUND_HALF_UP) + def slackmsg = "[${env.JOB_NAME.replaceAll('%2F','/')}-${env.BUILD_NUMBER}] `${currentBuild.result}`" + "\n" + + "Elapsed Time: `${DURATION}` Mins" + "\n" + + "tidb-operator Branch: `${BUILD_BRANCH}`, Githash: `${GITHASH.take(7)}`" + "\n" + + "Display URL:" + "\n" + + "${env.RUN_DISPLAY_URL}" + + if(currentBuild.result != "SUCCESS"){ + slackSend channel: '#cloud_jenkins', color: 'danger', teamDomain: 'pingcap', tokenCredentialId: 'slack-pingcap-token', message: "${slackmsg}" + } else { + slackmsg = "${slackmsg}" + "\n" + + "tkc cli tool build and debug image build failed for BRANCH:${BUILD_BRANCH} and TAG:${RELEASE_TAG}`" + slackSend channel: '#cloud_jenkins', color: 'good', teamDomain: 'pingcap', tokenCredentialId: 'slack-pingcap-token', message: "${slackmsg}" + } + } +} + +return this diff --git a/go.mod b/go.mod index 3df29ee1619..6b3ce809832 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,19 @@ module github.com/pingcap/tidb-operator require ( github.com/BurntSushi/toml v0.3.1 // indirect github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect + github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 // indirect github.com/coreos/bbolt v1.3.1-coreos.6 // indirect github.com/coreos/etcd v0.0.0-20180530235116-2b3aa7e1d49d // indirect github.com/coreos/go-semver v0.2.0 // indirect github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 // indirect github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea // indirect + github.com/daviddengcn/go-colortext v0.0.0-20180409174941-186a3d44e920 // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/dnephin/govet v0.0.0-20171012192244-4a96d43e39d3 github.com/docker/distribution v2.7.1+incompatible // indirect - github.com/docker/docker v1.13.1 // indirect + github.com/docker/docker v0.7.3-0.20171023200535-7848b8beb9d3 github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.3.3 // indirect github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect @@ -53,9 +56,11 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/onsi/ginkgo v1.6.0 github.com/onsi/gomega v1.4.1 github.com/opencontainers/go-digest v1.0.0-rc1 // indirect + github.com/opencontainers/image-spec v1.0.1 // indirect github.com/pborman/uuid v1.2.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pingcap/check v0.0.0-20171206051426-1c287c953996 // indirect @@ -67,9 +72,10 @@ require ( github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 // indirect github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e // indirect github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273 // indirect + github.com/renstrom/dedent v1.1.0 // indirect github.com/russross/blackfriday v1.5.2+incompatible // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - github.com/sirupsen/logrus v1.0.6 // indirect + github.com/sirupsen/logrus v1.0.6 github.com/soheilhy/cmux v0.1.4 // indirect github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.3 @@ -106,7 +112,10 @@ require ( k8s.io/klog v0.2.0 // indirect k8s.io/kube-openapi v0.0.0-20190320154901-5e45bb682580 // indirect k8s.io/kubernetes v1.12.5 + k8s.io/metrics v0.0.0-20190118124808-33c1aed8dc65 // indirect k8s.io/utils v0.0.0-20190308190857-21c4ce38f2a7 // indirect sigs.k8s.io/yaml v1.1.0 // indirect vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 // indirect ) + +replace github.com/renstrom/dedent => github.com/lithammer/dedent v1.1.0 diff --git a/go.sum b/go.sum index 2855c68feac..8626eb0c4c3 100644 --- a/go.sum +++ b/go.sum @@ -3,12 +3,16 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e h1:eb0Pzkt15Bm7f2FFYv7sjY7NPFi3cPkS3tv1CcrFBWA= github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 h1:HD4PLRzjuCVW79mQ0/pdsalOLHJ+FaEoqJLxfltpb2U= +github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= github.com/coreos/bbolt v1.3.1-coreos.6 h1:uTXKg9gY70s9jMAKdfljFQcuh4e/BXOM+V+d00KFj3A= github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v0.0.0-20180530235116-2b3aa7e1d49d h1:X15ii/EYFV9j7Hgj8VwtpSe65vGy7RYrX9oIDgXvRHk= @@ -23,12 +27,18 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/daviddengcn/go-colortext v0.0.0-20180409174941-186a3d44e920 h1:d/cVoZOrJPJHKH1NdeUjyVAWKp4OpOT+Q+6T1sH7jeU= +github.com/daviddengcn/go-colortext v0.0.0-20180409174941-186a3d44e920/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dnephin/govet v0.0.0-20171012192244-4a96d43e39d3 h1:LrNBULdC8dhFb7VeeIfuLAVq2IOFtVD9zIYh838jWfM= github.com/dnephin/govet v0.0.0-20171012192244-4a96d43e39d3/go.mod h1:pPTX0MEEoAnfbrAGFj4nSVNhl6YbugRj6eardUZdtGo= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v0.0.0-20170601211448-f5ec1e2936dc h1:S8H7eaOGNNOZ83UGSgpgv4FlCtoBTJxG6GzFNkwJr5Q= +github.com/docker/docker v0.0.0-20170601211448-f5ec1e2936dc/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v0.7.3-0.20171023200535-7848b8beb9d3 h1:zP6PLsIZzP7GvGgoXb3fKlp0hoMdcskl9Je1GaHzFJM= +github.com/docker/docker v0.7.3-0.20171023200535-7848b8beb9d3/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo= github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= @@ -113,6 +123,8 @@ github.com/juju/loggo v0.0.0-20180524022052-584905176618 h1:MK144iBQF9hTSwBW/9eJ github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073 h1:WQM1NildKThwdP7qWrNAFGzp4ijNLw8RlgENkaI4MJs= github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= +github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -125,12 +137,16 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLD github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= @@ -309,6 +325,8 @@ k8s.io/kubernetes v1.12.5 h1:pdQvCJZPGRNVS3CaajKuoPCZKreQaglbRcXwkDwR598= k8s.io/kubernetes v1.12.5/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/kubernetes v1.12.6 h1:Uh8wsnpa6khE5ylCoLBP0UomxD5h0FK8OWV9MLiSKjI= k8s.io/kubernetes v1.12.6/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +k8s.io/metrics v0.0.0-20190118124808-33c1aed8dc65 h1:0VelqHP6rojigdeX7EfWJ26OCw7PSvCGz7xGg7ukN8U= +k8s.io/metrics v0.0.0-20190118124808-33c1aed8dc65/go.mod h1:a25VAbm3QT3xiVl1jtoF1ueAKQM149UdZ+L93ePfV3M= k8s.io/utils v0.0.0-20190308190857-21c4ce38f2a7 h1:8r+l4bNWjRlsFYlQJnKJ2p7s1YQPj4XyXiJVqDHRx7c= k8s.io/utils v0.0.0-20190308190857-21c4ce38f2a7/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= diff --git a/misc/cmd/debug-launcher/main.go b/misc/cmd/debug-launcher/main.go new file mode 100644 index 00000000000..550b9c853c6 --- /dev/null +++ b/misc/cmd/debug-launcher/main.go @@ -0,0 +1,34 @@ +// Copyright 2019. PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/pingcap/tidb-operator/pkg/tkctl/debug" + "log" + "os" +) + +func main() { + + iostreams := debug.IOStreams{ + In: os.Stdin, + Out: os.Stdout, + ErrOut: os.Stderr, + } + cmd := debug.NewLauncherCmd(iostreams) + if err := cmd.Execute(); err != nil { + log.Fatal(err) + os.Exit(1) + } +} diff --git a/misc/images/debug-launcher/Dockerfile b/misc/images/debug-launcher/Dockerfile new file mode 100644 index 00000000000..7f0f92226ce --- /dev/null +++ b/misc/images/debug-launcher/Dockerfile @@ -0,0 +1,7 @@ +FROM bash +ENV TERM=linux + +ADD bin/debug-launcher /debug-launcher +ADD entry-point.sh /entry-point.sh +ENTRYPOINT ["/entry-point.sh"] + diff --git a/misc/images/debug-launcher/entry-point.sh b/misc/images/debug-launcher/entry-point.sh new file mode 100755 index 00000000000..20b09c254ed --- /dev/null +++ b/misc/images/debug-launcher/entry-point.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +# launch container under bash to get proper tty +/debug-launcher $@ \ No newline at end of file diff --git a/misc/images/tidb-control/Dockerfile b/misc/images/tidb-control/Dockerfile new file mode 100644 index 00000000000..bb60e584daa --- /dev/null +++ b/misc/images/tidb-control/Dockerfile @@ -0,0 +1,14 @@ +FROM bash:4.3.48 +RUN wget -q http://download.pingcap.org/tidb-latest-linux-amd64.tar.gz \ + && tar xzf tidb-latest-linux-amd64.tar.gz \ + && mv tidb-latest-linux-amd64/bin/pd-ctl \ + tidb-latest-linux-amd64/bin/tidb-ctl \ + /usr/local/bin/ \ + && rm -rf tidb-latest-linux-amd64.tar.gz tidb-latest-linux-amd64 + +ADD banner /etc/banner +ADD profile /etc/profile + +CMD ["/usr/local/bin/bash", "-l"] + + diff --git a/misc/images/tidb-control/README.md b/misc/images/tidb-control/README.md new file mode 100644 index 00000000000..12c6edb20f9 --- /dev/null +++ b/misc/images/tidb-control/README.md @@ -0,0 +1,48 @@ +# Tidb Cluster Control Image + +`tidb-control` is a docker image containing the main control tools for tidb cluster: `pd-ctl` and `tidb-ctl`. + +# Usage + +```shell +$ docker run -it --rm pingcap/tidb-control:latest + +# enter docker shell +→ / tidb-ctl +Usage: + tidb-ctl [flags] + tidb-ctl [command] + +Available Commands: + base64decode decode base64 value + decoder decode key + etcd control the info about etcd by grpc_gateway + help Help about any command + mvcc MVCC Information + region Region information + schema Schema Information + table Table information + +Flags: + -h, --help help for tidb-ctl + --host ip TiDB server host (default 127.0.0.1) + --pdhost ip PD server host (default 127.0.0.1) + --pdport uint16 PD server port (default 2379) + --port uint16 TiDB server port (default 10080) + +Use "tidb-ctl [command] --help" for more information about a command. +→ / pd-ctl --help +Usage of pd-ctl: + --cacert string path of file that contains list of trusted SSL CAs. + --cert string path of file that contains X509 certificate in PEM format. + -d, --detach Run pdctl without readline + --key string path of file that contains X509 key in PEM format. + -u, --pd string The pd address (default "http://127.0.0.1:2379") + -V, --version print version information and exit +pflag: help requested + +# mount CA certificate files if necessary +$ docker run -v $(pwd)/ca.pem:$(pwd)/ca.pem -v $(pwd)/client.pem:$(pwd)/client.pem -it --rm pingcap/tidb-control:latest +``` + +PS: `library/bash` is a `busybox` based image, so some dynamic linked binaries like `tikv-ctl` cannot work in this minimal image. If you are looking for `tikv-ctl` or more tools, take a look at [tidb-debug](../tidb-debug/). diff --git a/misc/images/tidb-control/banner b/misc/images/tidb-control/banner new file mode 100644 index 00000000000..5db14df2e90 --- /dev/null +++ b/misc/images/tidb-control/banner @@ -0,0 +1,9 @@ + +████████╗██╗██████╗ ██████╗ ██████╗████████╗██╗ +╚══██╔══╝╚═╝██╔══██╗██╔══██╗ ██╔════╝╚══██╔══╝██║ + ██║ ██╗██║ ██║██████╔╝ ██║ ██║ ██║ + ██║ ██║██║ ██║██╔══██╗ ██║ ██║ ██║ + ██║ ██║██████╔╝██████╔╝ ╚██████╗ ██║ ███████╗ + ╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝ + +Welcome to tidb cluster control! (https://github.com/pingcap/tidb) diff --git a/misc/images/tidb-control/profile b/misc/images/tidb-control/profile new file mode 100644 index 00000000000..010f4d4e261 --- /dev/null +++ b/misc/images/tidb-control/profile @@ -0,0 +1,18 @@ +cat /etc/banner + +export CLICOLOR=1 +export LSCOLORS=GxFxCxDxBxegedabagaced +export LS_OPTIONS='--color=auto' + +# aliases +alias ls='ls $LS_OPTIONS' + +function prompt { + local GREENBOLD="\[\033[1;32m\]" + local RESETCOLOR="\[\e[00m\]" + + export PS1="$GREENBOLD→ \w $RESETCOLOR" + export PS2=" | → $RESETCOLOR " +} + +prompt diff --git a/misc/images/tidb-debug/Dockerfile b/misc/images/tidb-debug/Dockerfile new file mode 100644 index 00000000000..be150e5dc8d --- /dev/null +++ b/misc/images/tidb-debug/Dockerfile @@ -0,0 +1,54 @@ +FROM centos:7 + +RUN yum update -y && yum install -y \ + curl \ + file \ + gdb \ + git \ + iotop \ + linux-perf \ + mysql \ + net-tools \ + perf \ + perl \ + procps-ng \ + psmisc \ + strace \ + sysstat \ + tree \ + tcpdump \ + unzip \ + vim \ + wget \ + which \ + netstat \ + && yum clean all \ + && rm -rf /var/cache/yum/* + +RUN wget -q http://download.pingcap.org/tidb-latest-linux-amd64.tar.gz \ + && tar xzf tidb-latest-linux-amd64.tar.gz \ + && mv tidb-latest-linux-amd64/bin/* /usr/local/bin/ \ + && rm -rf tidb-latest-linux-amd64.tar.gz tidb-latest-linux-amd64 + +RUN wget https://github.com/brendangregg/FlameGraph/archive/master.zip \ + && unzip master.zip \ + && mv FlameGraph-master /opt/FlameGraph \ + && rm master.zip +ADD run_flamegraph.sh /run_flamegraph.sh + +# used for go pprof +ENV GOLANG_VERSION 1.12.4 +ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go$GOLANG_VERSION.linux-amd64.tar.gz +ENV GOLANG_DOWNLOAD_SHA256 d7d1f1f88ddfe55840712dc1747f37a790cbcaa448f6c9cf51bbe10aa65442f5 +RUN curl -fsSL "$GOLANG_DOWNLOAD_URL" -o golang.tar.gz \ + && echo "$GOLANG_DOWNLOAD_SHA256 golang.tar.gz" | sha256sum -c - \ + && tar -C /usr/local -xzf golang.tar.gz \ + && rm golang.tar.gz +ENV GOPATH /go +ENV GOROOT /usr/local/go +ENV PATH $GOPATH/bin:$GOROOT/bin:$PATH + +ADD banner /etc/banner +ADD profile /etc/profile + +ENTRYPOINT ["/bin/bash", "-l"] \ No newline at end of file diff --git a/misc/images/tidb-debug/README.md b/misc/images/tidb-debug/README.md new file mode 100644 index 00000000000..4cb7d632efc --- /dev/null +++ b/misc/images/tidb-debug/README.md @@ -0,0 +1,9 @@ +# TiDB cluster debug toolkit + +TiDB cluster debug toolkit is a docker image contains various troubleshooting tools for TiDB cluster. + +# Usage + +```shell +$ docker run -it --rm pingcap/tidb-debug:latest +``` diff --git a/misc/images/tidb-debug/banner b/misc/images/tidb-debug/banner new file mode 100644 index 00000000000..6c0846ddae0 --- /dev/null +++ b/misc/images/tidb-debug/banner @@ -0,0 +1,9 @@ + +████████╗██╗██████╗ ██████╗ ██████╗ ███████╗██████╗ ██╗ ██╗ ██████╗ +╚══██╔══╝╚═╝██╔══██╗██╔══██╗ ██╔══██╗██╔════╝██╔══██╗██║ ██║██╔════╝ + ██║ ██╗██║ ██║██████╔╝ ██║ ██║█████╗ ██████╔╝██║ ██║██║ ███╗ + ██║ ██║██║ ██║██╔══██╗ ██║ ██║██╔══╝ ██╔══██╗██║ ██║██║ ██║ + ██║ ██║██████╔╝██████╔╝ ██████╔╝███████╗██████╔╝╚██████╔╝╚██████╔╝ + ╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═════╝ ╚═════╝ ╚═════╝ + +Welcome to tidb debug toolkit! (https://github.com/pingcap/tidb) diff --git a/misc/images/tidb-debug/profile b/misc/images/tidb-debug/profile new file mode 100644 index 00000000000..010f4d4e261 --- /dev/null +++ b/misc/images/tidb-debug/profile @@ -0,0 +1,18 @@ +cat /etc/banner + +export CLICOLOR=1 +export LSCOLORS=GxFxCxDxBxegedabagaced +export LS_OPTIONS='--color=auto' + +# aliases +alias ls='ls $LS_OPTIONS' + +function prompt { + local GREENBOLD="\[\033[1;32m\]" + local RESETCOLOR="\[\e[00m\]" + + export PS1="$GREENBOLD→ \w $RESETCOLOR" + export PS2=" | → $RESETCOLOR " +} + +prompt diff --git a/misc/images/tidb-debug/run_flamegraph.sh b/misc/images/tidb-debug/run_flamegraph.sh new file mode 100644 index 00000000000..fd6a2a964aa --- /dev/null +++ b/misc/images/tidb-debug/run_flamegraph.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +perf record -F 99 -p $1 -g -- sleep 60 +perf script > out.perf +/opt/FlameGraph/stackcollapse-perf.pl out.perf > out.folded +/opt/FlameGraph/flamegraph.pl out.folded > kernel.svg +curl --upload-file ./kernel.svg https://transfer.sh/kernel.svg \ No newline at end of file diff --git a/pkg/tkctl/cmd/cmd.go b/pkg/tkctl/cmd/cmd.go index 66d17a6ab4d..1be7f701aaf 100644 --- a/pkg/tkctl/cmd/cmd.go +++ b/pkg/tkctl/cmd/cmd.go @@ -20,7 +20,6 @@ import ( "github.com/pingcap/tidb-operator/pkg/tkctl/cmd/get" "github.com/pingcap/tidb-operator/pkg/tkctl/cmd/info" "github.com/pingcap/tidb-operator/pkg/tkctl/cmd/list" - "github.com/pingcap/tidb-operator/pkg/tkctl/cmd/pdctl" "github.com/pingcap/tidb-operator/pkg/tkctl/cmd/use" "github.com/pingcap/tidb-operator/pkg/tkctl/config" "github.com/spf13/cobra" @@ -72,7 +71,6 @@ func NewTkcCommand(streams genericclioptions.IOStreams) *cobra.Command { Message: "Troubleshooting Commands:", Commands: []*cobra.Command{ debug.NewCmdDebug(tkcContext, streams), - pdctl.NewCmdPdctl(tkcContext, streams), ctop.NewCmdCtop(tkcContext, streams), }, }, diff --git a/pkg/tkctl/cmd/ctop/ctop.go b/pkg/tkctl/cmd/ctop/ctop.go index 00225acd3d7..db042722a6f 100644 --- a/pkg/tkctl/cmd/ctop/ctop.go +++ b/pkg/tkctl/cmd/ctop/ctop.go @@ -16,20 +16,177 @@ package ctop import ( "fmt" "github.com/pingcap/tidb-operator/pkg/tkctl/config" + "github.com/pingcap/tidb-operator/pkg/tkctl/executor" + "github.com/pingcap/tidb-operator/pkg/tkctl/util" "github.com/spf13/cobra" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/cli-runtime/pkg/genericclioptions" - "os" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "strings" ) -// TODO: implementation +const ( + ctopExample = ` + # ctop the specified pod + tkc ctop POD_NAME + + # ctop the specified node + tkc ctop node/NODE_NAME +` + ctopUsage = "expected 'ctop POD_NAME' or 'ctop node/NODE_NAME' for the ctop command" + defaultImage = "quay.io/vektorlab/ctop:0.7.2" +) + +type CtopKind string + +const ( + CtopPod CtopKind = "pod" + CtopNode CtopKind = "node" +) + +// CtopOptions specify the target resource stats to show +type CtopOptions struct { + Target string + Kind CtopKind + Namespace string + Image string + HostDockerSocket string + + KubeCli *kubernetes.Clientset + + RestConfig *rest.Config + + genericclioptions.IOStreams +} + +// NewCtopOptions create a ctop options +func NewCtopOptions(iostreams genericclioptions.IOStreams) *CtopOptions { + return &CtopOptions{ + Kind: CtopPod, + Image: defaultImage, + HostDockerSocket: util.DockerSocket, + + IOStreams: iostreams, + } +} + // NewCmdTop creates the ctop subcommand func NewCmdCtop(tkcContext *config.TkcContext, streams genericclioptions.IOStreams) *cobra.Command { - // create pod in target host, bind docker.sock or runc sock in RO mode, filter the target container - return &cobra.Command{ - Use: "ctop", - Short: "Not implemented", - Run: func(_ *cobra.Command, args []string) { - fmt.Fprint(os.Stdout, "not implemented") + options := NewCtopOptions(streams) + + cmd := &cobra.Command{ + Use: "ctop", + Example: ctopExample, + Short: "ctop shows top-like container stats for given pod", + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(options.Complete(cmd, tkcContext, args)) + cmdutil.CheckErr(options.Run()) }, } + cmd.Flags().StringVar(&options.HostDockerSocket, "docker-socketl", options.HostDockerSocket, + "docker socket path of kubernetes node") + cmd.Flags().StringVar(&options.Image, "image", options.Image, + "Container Image to run the debug container") + return cmd +} + +func (o *CtopOptions) Complete(cmd *cobra.Command, tkcContext *config.TkcContext, args []string) error { + if len(args) < 1 { + return cmdutil.UsageErrorf(cmd, ctopUsage) + } + o.Kind, o.Target = parseTarget(args[0]) + + clientConfig, err := tkcContext.ToTkcClientConfig() + if err != nil { + return err + } + ns, _, err := clientConfig.Namespace() + if err != nil { + return err + } + o.Namespace = ns + restConfig, err := clientConfig.RestConfig() + if err != nil { + return err + } + kubeCli, err := kubernetes.NewForConfig(restConfig) + o.RestConfig = restConfig + if err != nil { + return err + } + o.KubeCli = kubeCli + return nil +} + +func (o *CtopOptions) Run() error { + + var nodeName string + var filter string + switch o.Kind { + case CtopPod: + pod, err := o.KubeCli.CoreV1().Pods(o.Namespace).Get(o.Target, metav1.GetOptions{}) + if err != nil { + return err + } + nodeName = pod.Spec.NodeName + filter = pod.Name + case CtopNode: + nodeName = o.Target + default: + return fmt.Errorf("unknown resource type %s", string(o.Kind)) + } + pod := o.makeCtopPod(nodeName, filter) + podExecutor := executor.NewPodExecutor(o.KubeCli, pod, o.RestConfig, o.IOStreams) + return podExecutor.Execute() +} + +func (o *CtopOptions) makeCtopPod(nodeName, filter string) *v1.Pod { + args := []string{"-a"} + if len(filter) > 0 { + args = []string{ + "-f", + filter, + } + } + volume, mount := util.MakeDockerSocketMount(o.HostDockerSocket, true) + ctopPod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("ctop-%s", string(uuid.NewUUID())), + Namespace: o.Namespace, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "ctop", + Image: o.Image, + Args: args, + Stdin: true, + StdinOnce: true, + TTY: true, + VolumeMounts: []v1.VolumeMount{mount}, + ImagePullPolicy: v1.PullIfNotPresent, + }, + }, + Volumes: []v1.Volume{volume}, + NodeName: nodeName, + RestartPolicy: v1.RestartPolicyNever, + }, + } + return ctopPod +} + +func parseTarget(arg string) (kind CtopKind, target string) { + splits := strings.SplitN(arg, "/", 2) + if len(splits) == 1 { + kind = CtopPod + target = splits[0] + } else { + kind = CtopKind(splits[0]) + target = splits[1] + } + return } diff --git a/pkg/tkctl/cmd/debug/debug.go b/pkg/tkctl/cmd/debug/debug.go index 0cc701a9ffc..72b6dcc95ab 100644 --- a/pkg/tkctl/cmd/debug/debug.go +++ b/pkg/tkctl/cmd/debug/debug.go @@ -16,43 +16,50 @@ package debug import ( "fmt" "github.com/pingcap/tidb-operator/pkg/tkctl/config" + "github.com/pingcap/tidb-operator/pkg/tkctl/executor" + "github.com/pingcap/tidb-operator/pkg/tkctl/util" "github.com/spf13/cobra" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/cli-runtime/pkg/genericclioptions" - "os" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" ) const ( - example = ` + debugExample = ` # debug a container in the running pod, the first container will be picked by default - kubectl debug POD_NAME + tkc debug POD_NAME # specify namespace or container - kubectl debug --namespace foo POD_NAME -c CONTAINER_NAME + tkc debug --namespace foo POD_NAME -c CONTAINER_NAME # override the default troubleshooting image - kubectl debug POD_NAME --image aylei/debug-jvm + tkc debug POD_NAME --image aylei/debug-jvm # override entrypoint of debug container - kubectl debug POD_NAME --image aylei/debug-jvm /bin/bash + tkc debug POD_NAME --image aylei/debug-jvm /bin/bash ` - longDesc = ` + debugLongDesc = ` Run a container in a running pod, this container will join the namespaces of an existing container of the pod. + +Under the hood, 'debug' create a agent in target node, the agent is responsible for launching the actual debug +container which will join linux namespaces of the target container and proxying I/O connection. When debug session +ends, the agent destroy and cleanup the debug container and exited with code 0, 'tkc' will clean the finished agent +in the defer manner if it is not interrupted by user. ` - defaultImage = "nicolaka/netshoot:latest" + debugUsage = "expected 'debug POD_NAME' for the debug command" + defaultImage = "pingcap/tidb-debug:latest" + launcherImage = "pingcap/debug-launcher:latest" + launcherName = "debug-launcher" ) -// TODO: implementation -// NewCmdDebug creates the debug subcommand which helps container debugging -func NewCmdDebug(tkcContext *config.TkcContext, streams genericclioptions.IOStreams) *cobra.Command { - return &cobra.Command{ - Use: "debug", - Short: "Not implemented", - Run: func(_ *cobra.Command, args []string) { - fmt.Fprint(os.Stdout, "not implemented") - }, - } -} +var ( + defaultCommand = []string{"bash", "-l"} +) // DebugOptions specify how to run debug container in a running pod type DebugOptions struct { @@ -62,31 +69,178 @@ type DebugOptions struct { PodName string // Debug options - Image string - ContainerName string - Command []string + Image string + ContainerName string + Command []string + HostDockerSocket string + LauncherImage string + + KubeCli *kubernetes.Clientset + + RestConfig *rest.Config + + genericclioptions.IOStreams +} + +func NewDebugOptions(iostreams genericclioptions.IOStreams) *DebugOptions { + return &DebugOptions{ + Image: defaultImage, + HostDockerSocket: util.DockerSocket, + LauncherImage: launcherImage, + + IOStreams: iostreams, + } +} + +// NewCmdDebug creates the debug subcommand which helps container debugging +func NewCmdDebug(tkcContext *config.TkcContext, streams genericclioptions.IOStreams) *cobra.Command { + options := NewDebugOptions(streams) + + cmd := &cobra.Command{ + Use: "debug", + Short: "Run a debug container", + Long: debugLongDesc, + Example: debugExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(options.Complete(tkcContext, cmd, args)) + cmdutil.CheckErr(options.Run()) + }, + } + cmd.Flags().StringVar(&options.Image, "image", options.Image, + "Container Image to run the debug container") + cmd.Flags().StringVarP(&options.ContainerName, "container", "c", "", + "Target container to debug, default to the first container in pod") + cmd.Flags().StringVar(&options.HostDockerSocket, "docker-socketl", options.HostDockerSocket, + "docker socket path of kubernetes node") + cmd.Flags().StringVar(&options.LauncherImage, "launcher-image", options.LauncherImage, + "image for launcher pod which is responsible to launch the debug container") + + return cmd } // Complete populate default values for DebugOptions -func (p *DebugOptions) Complete(configFlags *genericclioptions.ConfigFlags, cmd *cobra.Command, argsIn []string, argsLenAtDash int) error { +func (o *DebugOptions) Complete(tkcContext *config.TkcContext, cmd *cobra.Command, argsIn []string) error { // select one pod to debug (required) + if len(argsIn) == 0 { + return cmdutil.UsageErrorf(cmd, debugUsage) + } + o.PodName = argsIn[0] - // we have to define kv,db,pd selector here (tidb-context aware) - - // override default image, container, command + o.Command = argsIn[1:] + if len(o.Command) < 1 { + o.Command = defaultCommand + } + clientConfig, err := tkcContext.ToTkcClientConfig() + if err != nil { + return err + } + ns, _, err := clientConfig.Namespace() + if err != nil { + return err + } + o.Namespace = ns + restConfig, err := clientConfig.RestConfig() + if err != nil { + return err + } + kubeCli, err := kubernetes.NewForConfig(restConfig) + o.RestConfig = restConfig + if err != nil { + return err + } + o.KubeCli = kubeCli return nil } -func (p *DebugOptions) Validate() error { - // validate required fields are populated - return nil +func (o *DebugOptions) Run() error { + + // 0.Prepare debug context: get Pod and verify state + pod, err := o.KubeCli.CoreV1().Pods(o.Namespace).Get(o.PodName, metav1.GetOptions{}) + if err != nil { + return err + } + if pod.Status.Phase == v1.PodSucceeded || pod.Status.Phase == v1.PodFailed { + return fmt.Errorf("cannot debug in a completed pod; current phase is %s", pod.Status.Phase) + } + containerName := o.ContainerName + if len(containerName) == 0 { + if len(pod.Spec.Containers) > 1 { + usageString := fmt.Sprintf("Defaulting container name to %s.", pod.Spec.Containers[0].Name) + fmt.Fprintf(o.ErrOut, "%s\n\r", usageString) + } + containerName = pod.Spec.Containers[0].Name + } + + nodeName := pod.Spec.NodeName + targetContainerID, err := o.getContainerIDByName(pod, containerName) + if err != nil { + return err + } + + launcher := o.makeLauncherPod(nodeName, targetContainerID, o.Command) + podExecutor := executor.NewPodExecutor(o.KubeCli, launcher, o.RestConfig, o.IOStreams) + return podExecutor.Execute() } -func (p *DebugOptions) Run() error { - // Get Pod and verify state +func (o *DebugOptions) makeLauncherPod(nodeName, containerID string, command []string) *v1.Pod { + + volume, mount := util.MakeDockerSocketMount(o.HostDockerSocket, false) + // we always mount docker socket to default path despite the host docker socket path + launchArgs := []string{ + "--target-container", + containerID, + "--image", + o.Image, + "--docker-socket", + fmt.Sprintf("unix://%s", util.DockerSocket), + "--", + } + launchArgs = append(launchArgs, command...) + return &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-%s", launcherName, string(uuid.NewUUID())), + Namespace: o.Namespace, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: launcherName, + Image: o.LauncherImage, + Args: launchArgs, + Stdin: true, + TTY: true, + VolumeMounts: []v1.VolumeMount{mount}, + ImagePullPolicy: v1.PullAlways, + }, + }, + Volumes: []v1.Volume{volume}, + NodeName: nodeName, + RestartPolicy: v1.RestartPolicyNever, + }, + } +} - // Create launch pod, wait remote shell attached and running +func (o *DebugOptions) getContainerIDByName(pod *v1.Pod, containerName string) (string, error) { + for _, containerStatus := range pod.Status.ContainerStatuses { + if containerStatus.Name != containerName { + continue + } + if !containerStatus.Ready { + return "", fmt.Errorf("container [%s] not ready", containerName) + } + return containerStatus.ContainerID, nil + } - // Hold and cleanup - return nil + // also search init containers + for _, initContainerStatus := range pod.Status.InitContainerStatuses { + if initContainerStatus.Name != containerName { + continue + } + if initContainerStatus.State.Running == nil { + return "", fmt.Errorf("init container [%s] is not running", containerName) + } + return initContainerStatus.ContainerID, nil + } + + return "", fmt.Errorf("cannot find specified container %s", containerName) } diff --git a/pkg/tkctl/config/config.go b/pkg/tkctl/config/config.go index fd059419668..a6a5ff28795 100644 --- a/pkg/tkctl/config/config.go +++ b/pkg/tkctl/config/config.go @@ -206,7 +206,7 @@ func (c *TkcContext) collectOverrides() *clientcmd.ConfigOverrides { } else if c.TidbClusterConfig != nil && len(c.TidbClusterConfig.KubeContext) > 0 { overrides.CurrentContext = c.TidbClusterConfig.KubeContext } - if c.Namespace != nil { + if c.Namespace != nil && len(*c.Namespace) > 0 { overrides.Context.Namespace = *c.Namespace } else if c.TidbClusterConfig != nil && len(c.TidbClusterConfig.Namespace) > 0 { overrides.Context.Namespace = c.TidbClusterConfig.Namespace diff --git a/pkg/tkctl/debug/hijack.go b/pkg/tkctl/debug/hijack.go new file mode 100644 index 00000000000..6e1a4bc746e --- /dev/null +++ b/pkg/tkctl/debug/hijack.go @@ -0,0 +1,216 @@ +// Copyright 2019. PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package debug + +import ( + "context" + "fmt" + "github.com/pingcap/tidb-operator/pkg/tkctl/streams" + "io" + "runtime" + "sync" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/stdcopy" + "github.com/docker/docker/pkg/term" + "github.com/sirupsen/logrus" +) + +type Streams struct { + In *streams.In + Out *streams.Out + Err io.Writer +} + +// A hijackedIOStreamer handles copying input to and output from streams to the connection. +// hijack logic is modified from docker-cli (docker/cli/command/container/hijack.go), +// which properly handled tty and control sequence +type hijackedIOStreamer struct { + streams Streams + inputStream io.ReadCloser + outputStream io.Writer + errorStream io.Writer + + resp types.HijackedResponse + + tty bool +} + +func newHijackedIOStreamer(ioStreams IOStreams, resp types.HijackedResponse) *hijackedIOStreamer { + return &hijackedIOStreamer{ + streams: Streams{ + In: streams.NewIn(ioStreams.In), + Out: streams.NewOut(ioStreams.Out), + Err: ioStreams.ErrOut, + }, + + inputStream: ioStreams.In, + outputStream: ioStreams.Out, + errorStream: ioStreams.ErrOut, + + resp: resp, + + tty: true, + } +} + +// stream handles setting up the IO and then begins streaming stdin/stdout +// to/from the hijacked connection, blocking until it is either done reading +// output, the user inputs the detach key sequence when in TTY mode, or when +// the given context is cancelled. +func (h *hijackedIOStreamer) stream(ctx context.Context) error { + restoreInput, err := h.setupInput() + if err != nil { + return fmt.Errorf("unable to setup input stream: %s", err) + } + + defer restoreInput() + + outputDone := h.beginOutputStream(restoreInput) + inputDone, detached := h.beginInputStream(restoreInput) + + select { + case err := <-outputDone: + return err + case <-inputDone: + // Input stream has closed. + if h.outputStream != nil || h.errorStream != nil { + // Wait for output to complete streaming. + select { + case err := <-outputDone: + return err + case <-ctx.Done(): + return ctx.Err() + } + } + return nil + case err := <-detached: + // Got a detach key sequence. + return err + case <-ctx.Done(): + return ctx.Err() + } +} + +func (h *hijackedIOStreamer) setupInput() (restore func(), err error) { + if h.inputStream == nil || !h.tty { + // No need to setup input TTY. + // The restore func is a nop. + return func() {}, nil + } + + if err := setRawTerminal(h.streams); err != nil { + return nil, fmt.Errorf("unable to set IO streams as raw terminal: %s", err) + } + + // Use sync.Once so we may call restore multiple times but ensure we + // only restore the terminal once. + var restoreOnce sync.Once + restore = func() { + restoreOnce.Do(func() { + restoreTerminal(h.streams, h.inputStream) + }) + } + + h.inputStream = ioutils.NewReadCloserWrapper(h.inputStream, h.inputStream.Close) + + return restore, nil +} + +func (h *hijackedIOStreamer) beginOutputStream(restoreInput func()) <-chan error { + if h.outputStream == nil && h.errorStream == nil { + // There is no need to copy output. + return nil + } + + outputDone := make(chan error) + go func() { + var err error + + // When TTY is ON, use regular copy + if h.outputStream != nil && h.tty { + _, err = io.Copy(h.outputStream, h.resp.Reader) + // We should restore the terminal as soon as possible + // once the connection ends so any following print + // messages will be in normal type. + restoreInput() + } else { + _, err = stdcopy.StdCopy(h.outputStream, h.errorStream, h.resp.Reader) + } + + logrus.Debug("[hijack] End of stdout") + + if err != nil { + logrus.Debugf("Error receiveStdout: %s", err) + } + + outputDone <- err + }() + + return outputDone +} + +func (h *hijackedIOStreamer) beginInputStream(restoreInput func()) (doneC <-chan struct{}, detachedC <-chan error) { + inputDone := make(chan struct{}) + detached := make(chan error) + + go func() { + if h.inputStream != nil { + _, err := io.Copy(h.resp.Conn, h.inputStream) + // We should restore the terminal as soon as possible + // once the connection ends so any following print + // messages will be in normal type. + restoreInput() + + logrus.Debug("[hijack] End of stdin") + + if _, ok := err.(term.EscapeError); ok { + detached <- err + return + } + + if err != nil { + // This error will also occur on the receive + // side (from stdout) where it will be + // propagated back to the caller. + logrus.Debugf("Error sendStdin: %s", err) + } + } + + if err := h.resp.CloseWrite(); err != nil { + logrus.Debugf("Couldn't send EOF: %s", err) + } + + close(inputDone) + }() + + return inputDone, detached +} + +func setRawTerminal(streams Streams) error { + if err := streams.In.SetRawTerminal(); err != nil { + return err + } + return streams.Out.SetRawTerminal() +} + +func restoreTerminal(streams Streams, in io.Closer) error { + streams.In.RestoreTerminal() + streams.Out.RestoreTerminal() + if in != nil && runtime.GOOS != "darwin" && runtime.GOOS != "windows" { + return in.Close() + } + return nil +} diff --git a/pkg/tkctl/debug/launcher.go b/pkg/tkctl/debug/launcher.go new file mode 100644 index 00000000000..747ad9a6713 --- /dev/null +++ b/pkg/tkctl/debug/launcher.go @@ -0,0 +1,185 @@ +// Copyright 2019. PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package debug + +import ( + "context" + "fmt" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/strslice" + dockerclient "github.com/docker/docker/client" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/spf13/cobra" + "io" + "strings" +) + +const ( + defaultDockerSocket = "unix:///var/run/docker.sock" + dockerContainerPrefix = "docker://" +) + +type IOStreams struct { + In io.ReadCloser + Out io.WriteCloser + ErrOut io.WriteCloser +} + +// Launcher is responsible for launching debug container +type Launcher struct { + IOStreams + + targetContainerID string + image string + dockerSocket string + ctx context.Context + + client *dockerclient.Client +} + +// NewLauncher create a launcher instance +func NewLauncher(streams IOStreams) *Launcher { + return &Launcher{ + dockerSocket: defaultDockerSocket, + ctx: context.Background(), + + IOStreams: streams, + } +} + +// NewLauncherCmd create the launcher command +func NewLauncherCmd(streams IOStreams) *cobra.Command { + launcher := NewLauncher(streams) + cmd := &cobra.Command{ + Use: "debug-launcher --target-container=CONTAINER_ID --image=IMAGE -- COMMAND", + RunE: func(c *cobra.Command, args []string) error { + return launcher.Run(args) + }, + } + cmd.Flags().StringVar(&launcher.targetContainerID, "target-container", launcher.targetContainerID, + "target container id") + cmd.Flags().StringVar(&launcher.image, "image", launcher.image, + "debug container image") + cmd.Flags().StringVar(&launcher.dockerSocket, "docker-socket", launcher.dockerSocket, + "docker socket to bind") + return cmd +} + +// Run launches the debug container and attach it. +// We could alternatively just run docker exec in command line, but this brings shell and docker client to the +// image, which is unwanted. +func (l *Launcher) Run(args []string) error { + client, err := dockerclient.NewClient(l.dockerSocket, "", nil, nil) + if err != nil { + return err + } + l.client = client + err = l.pullImage() + if err != nil { + return err + } + resp, err := l.createContainer(args) + if err != nil { + return err + } + containerID := resp.ID + fmt.Fprintf(l.Out, "starting debug container...\n") + err = l.startContainer(containerID) + if err != nil { + return err + } + defer l.cleanContainer(containerID) + err = l.attachToContainer(containerID) + if err != nil { + return err + } + return nil +} + +func (l *Launcher) pullImage() error { + out, err := l.client.ImagePull(l.ctx, l.image, types.ImagePullOptions{}) + if err != nil { + return err + } + defer out.Close() + // write pull progress to user + jsonmessage.DisplayJSONMessagesStream(out, l.Out, 1, true, nil) + return nil +} + +func (l *Launcher) createContainer(command []string) (*container.ContainerCreateCreatedBody, error) { + if !strings.HasPrefix(l.targetContainerID, dockerContainerPrefix) { + return nil, fmt.Errorf("Only docker containers are supported now") + } + dockerContainerID := l.targetContainerID[len(dockerContainerPrefix):] + config := &container.Config{ + Entrypoint: strslice.StrSlice(command), + Image: l.image, + Tty: true, + OpenStdin: true, + StdinOnce: true, + } + hostConfig := &container.HostConfig{ + NetworkMode: container.NetworkMode(containerMode(dockerContainerID)), + UsernsMode: container.UsernsMode(containerMode(dockerContainerID)), + IpcMode: container.IpcMode(containerMode(dockerContainerID)), + PidMode: container.PidMode(containerMode(dockerContainerID)), + } + body, err := l.client.ContainerCreate(l.ctx, config, hostConfig, nil, "") + if err != nil { + return nil, err + } + return &body, nil +} + +func (l *Launcher) startContainer(id string) error { + err := l.client.ContainerStart(l.ctx, id, types.ContainerStartOptions{}) + if err != nil { + return err + } + return nil +} + +func (l *Launcher) attachToContainer(id string) error { + opts := types.ContainerAttachOptions{ + Stream: true, + Stdin: true, + Stdout: true, + Stderr: true, + } + resp, err := l.client.ContainerAttach(l.ctx, id, opts) + if err != nil { + return err + } + streamer := newHijackedIOStreamer(l.IOStreams, resp) + defer resp.Close() + return streamer.stream(l.ctx) +} + +func (l *Launcher) cleanContainer(id string) error { + // once attach complete, the debug container is considered to be exited, so its safe to rm --force + err := l.client.ContainerRemove(l.ctx, id, + types.ContainerRemoveOptions{ + Force: true, + }) + if err != nil { + return err + } + return nil +} + +func containerMode(id string) string { + return fmt.Sprintf("container:%s", id) +} diff --git a/pkg/tkctl/executor/executor.go b/pkg/tkctl/executor/executor.go new file mode 100644 index 00000000000..11805dc4ff6 --- /dev/null +++ b/pkg/tkctl/executor/executor.go @@ -0,0 +1,143 @@ +// Copyright 2019. PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package executor + +import ( + "context" + "fmt" + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + watchapi "k8s.io/apimachinery/pkg/watch" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/watch" + "k8s.io/kubernetes/pkg/kubectl" + "k8s.io/kubernetes/pkg/kubectl/cmd" + "k8s.io/kubernetes/pkg/util/interrupt" + "time" +) + +const ( + defaultWaitTimeOutSeconds = 5 * 60 +) + +// PodExecutor run pod once, attach to it, and clean pod when session complete +type PodExecutor struct { + Pod *v1.Pod + KubeCli *kubernetes.Clientset + + RestConfig *rest.Config + + genericclioptions.IOStreams +} + +func NewPodExecutor(kubeCli *kubernetes.Clientset, pod *v1.Pod, restConfig *rest.Config, streams genericclioptions.IOStreams) *PodExecutor { + return &PodExecutor{ + KubeCli: kubeCli, + Pod: pod, + + RestConfig: restConfig, + + IOStreams: streams, + } +} + +func (t *PodExecutor) Execute() error { + pod, err := t.KubeCli.CoreV1().Pods(t.Pod.Namespace).Create(t.Pod) + if err != nil { + return err + } + + defer t.removePod(pod) + return t.attachPod(pod) +} + +func (t *PodExecutor) attachPod(pod *v1.Pod) error { + + // TODO: currently, if a pod stuck in ImagePullBackoff state, watch thinks it still has chance to start so will + // keep waiting, but most likely the pod cannot really start. For better experience, We should periodically + // print pod current state, so that user can interrupt waiting when seeing 'ImagePullBackoff' or other unexpected states. + fmt.Fprintf(t.Out, "waiting for pod %s running...\n", pod.Name) + watched, err := t.waitForPod(pod, defaultWaitTimeOutSeconds, kubectl.PodRunningAndReady) + if err != nil { + return err + } + if watched.Status.Phase == v1.PodSucceeded || watched.Status.Phase == v1.PodFailed { + return fmt.Errorf("pod unexpcetedly exits, status: %s", watched.Status.Phase) + } + // reuse `kubectl attach` facility + attachOpts := cmd.NewAttachOptions(t.IOStreams) + attachOpts.TTY = true + attachOpts.Stdin = true + attachOpts.Pod = watched + attachOpts.PodName = watched.Name + attachOpts.Namespace = watched.Namespace + attachOpts.Config = t.RestConfig + setKubernetesDefaults(attachOpts.Config) + + if err := attachOpts.Run(); err != nil { + return err + } + return nil +} + +func (t *PodExecutor) removePod(pod *v1.Pod) error { + return t.KubeCli.CoreV1(). + Pods(pod.Namespace). + Delete(pod.Name, &metav1.DeleteOptions{}) +} + +func (t *PodExecutor) waitForPod(pod *v1.Pod, timeoutSeconds int64, exitCondition watch.ConditionFunc) (*v1.Pod, error) { + w, err := t.KubeCli.CoreV1().Pods(pod.Namespace).Watch(metav1.SingleObject(metav1.ObjectMeta{Name: pod.Name})) + if err != nil { + return nil, err + } + ctx, cancel := watch.ContextWithOptionalTimeout(context.Background(), time.Duration(timeoutSeconds)*time.Second) + defer cancel() + intr := interrupt.New(nil, cancel) + var result *v1.Pod + err = intr.Run(func() error { + event, err := watch.UntilWithoutRetry(ctx, w, func(event watchapi.Event) (bool, error) { + return exitCondition(event) + }) + if event != nil { + result = event.Object.(*v1.Pod) + } + return err + }) + if err != nil && errors.IsNotFound(err) { + err = errors.NewNotFound(v1.Resource("pods"), pod.Name) + } + return result, err +} + +func setKubernetesDefaults(config *rest.Config) error { + config.GroupVersion = &schema.GroupVersion{Group: "", Version: "v1"} + + if config.APIPath == "" { + config.APIPath = "/api" + } + if config.NegotiatedSerializer == nil { + // This codec factory ensures the resources are not converted. Therefore, resources + // will not be round-tripped through internal versions. Defaulting does not happen + // on the client. + config.NegotiatedSerializer = &serializer.DirectCodecFactory{CodecFactory: scheme.Codecs} + } + return rest.SetKubernetesDefaults(config) +} diff --git a/pkg/tkctl/streams/in.go b/pkg/tkctl/streams/in.go new file mode 100644 index 00000000000..9f12781985d --- /dev/null +++ b/pkg/tkctl/streams/in.go @@ -0,0 +1,69 @@ +// Copyright 2019. PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package streams + +import ( + "errors" + "io" + "os" + "runtime" + + "github.com/docker/docker/pkg/term" +) + +// In is an input stream used by the Cli tool to read user input +type In struct { + commonStream + in io.ReadCloser +} + +func (i *In) Read(p []byte) (int, error) { + return i.in.Read(p) +} + +// Close implements the Closer interface +func (i *In) Close() error { + return i.in.Close() +} + +// SetRawTerminal sets raw mode on the input terminal +func (i *In) SetRawTerminal() (err error) { + if os.Getenv("NORAW") != "" || !i.commonStream.isTerminal { + return nil + } + i.commonStream.state, err = term.SetRawTerminal(i.commonStream.fd) + return err +} + +// CheckTty checks if we are trying to attach to a container tty +// from a non-tty client input stream, and if so, returns an error. +func (i *In) CheckTty(attachStdin, ttyMode bool) error { + // In order to attach to a container tty, input stream for the client must + // be a tty itself: redirecting or piping the client standard input is + // incompatible with `docker run -t`, `docker exec -t` or `docker attach`. + if ttyMode && attachStdin && !i.isTerminal { + eText := "the input device is not a TTY" + if runtime.GOOS == "windows" { + return errors.New(eText + ". If you are using mintty, try prefixing the command with 'winpty'") + } + return errors.New(eText) + } + return nil +} + +// NewIn returns a new In object from a ReadCloser +func NewIn(in io.ReadCloser) *In { + fd, isTerminal := term.GetFdInfo(in) + return &In{commonStream: commonStream{fd: fd, isTerminal: isTerminal}, in: in} +} diff --git a/pkg/tkctl/streams/out.go b/pkg/tkctl/streams/out.go new file mode 100644 index 00000000000..3c265433201 --- /dev/null +++ b/pkg/tkctl/streams/out.go @@ -0,0 +1,63 @@ +// Copyright 2019. PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package streams + +import ( + "io" + "os" + + "github.com/docker/docker/pkg/term" + "github.com/sirupsen/logrus" +) + +// Out is an output stream used by the Cli tool to write normal program +// output. +type Out struct { + commonStream + out io.Writer +} + +func (o *Out) Write(p []byte) (int, error) { + return o.out.Write(p) +} + +// SetRawTerminal sets raw mode on the input terminal +func (o *Out) SetRawTerminal() (err error) { + if os.Getenv("NORAW") != "" || !o.commonStream.isTerminal { + return nil + } + o.commonStream.state, err = term.SetRawTerminalOutput(o.commonStream.fd) + return err +} + +// GetTtySize returns the height and width in characters of the tty +func (o *Out) GetTtySize() (uint, uint) { + if !o.isTerminal { + return 0, 0 + } + ws, err := term.GetWinsize(o.fd) + if err != nil { + logrus.Debugf("Error getting size: %s", err) + if ws == nil { + return 0, 0 + } + } + return uint(ws.Height), uint(ws.Width) +} + +// NewOut returns a new Out object from a Writer +func NewOut(out io.Writer) *Out { + fd, isTerminal := term.GetFdInfo(out) + return &Out{commonStream: commonStream{fd: fd, isTerminal: isTerminal}, out: out} +} diff --git a/pkg/tkctl/streams/stream.go b/pkg/tkctl/streams/stream.go new file mode 100644 index 00000000000..caac671d2b0 --- /dev/null +++ b/pkg/tkctl/streams/stream.go @@ -0,0 +1,47 @@ +// Copyright 2019. PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package streams + +import ( + "github.com/docker/docker/pkg/term" +) + +// commonStream is an input stream used by the Cli tool to read user input +type commonStream struct { + fd uintptr + isTerminal bool + state *term.State +} + +// FD returns the file descriptor number for this stream +func (s *commonStream) FD() uintptr { + return s.fd +} + +// IsTerminal returns true if this stream is connected to a terminal +func (s *commonStream) IsTerminal() bool { + return s.isTerminal +} + +// RestoreTerminal restores normal mode to the terminal +func (s *commonStream) RestoreTerminal() { + if s.state != nil { + term.RestoreTerminal(s.fd, s.state) + } +} + +// SetIsTerminal sets the boolean used for isTerminal +func (s *commonStream) SetIsTerminal(isTerminal bool) { + s.isTerminal = isTerminal +} diff --git a/pkg/tkctl/util/util.go b/pkg/tkctl/util/util.go index c40f7d83155..8444fa0866b 100644 --- a/pkg/tkctl/util/util.go +++ b/pkg/tkctl/util/util.go @@ -13,8 +13,31 @@ package util -// TODO: fix unsafe name infer after resources being managed by CRD +import "k8s.io/api/core/v1" + +const ( + DockerSocket = "/var/run/docker.sock" +) +// MakeDockerSocketMount create the volume and corresponding mount for docker socket +func MakeDockerSocketMount(hostDockerSocket string, readOnly bool) (volume v1.Volume, mount v1.VolumeMount) { + mount = v1.VolumeMount{ + Name: "docker", + ReadOnly: readOnly, + MountPath: DockerSocket, + } + volume = v1.Volume{ + Name: "docker", + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{ + Path: hostDockerSocket, + }, + }, + } + return +} + +// TODO: fix unsafe name infer after resources being managed by CRD // GetTidbServiceName infers tidb service name from tidb cluster name func GetTidbServiceName(tc string) string { return tc + "-tidb" diff --git a/pkg/tkctl/util/util_test.go b/pkg/tkctl/util/util_test.go new file mode 100644 index 00000000000..f026cf0b34a --- /dev/null +++ b/pkg/tkctl/util/util_test.go @@ -0,0 +1,56 @@ +// Copyright 2019. PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + . "github.com/onsi/gomega" + "testing" +) + +func TestMakeDockerSocketMount(t *testing.T) { + g := NewGomegaWithT(t) + type testCase struct { + socket string + readOnly bool + } + tests := []testCase{ + { + socket: "/var/run/docker.sock", + readOnly: false, + }, + { + socket: "/test.sock", + readOnly: true, + }, + { + socket: "/test.sock", + readOnly: false, + }, + } + testFn := func(test testCase, g *GomegaWithT) { + volume, mount := MakeDockerSocketMount(test.socket, test.readOnly) + g.Expect(volume.HostPath).NotTo(BeNil()) + g.Expect(volume.HostPath.Path).To(Equal(test.socket)) + g.Expect(mount.MountPath).To(Equal(DockerSocket)) + g.Expect(mount.ReadOnly).To(Equal(test.readOnly)) + } + for i := range tests { + testFn(tests[i], g) + } +} + +func TestGetTidbServiceName(t *testing.T) { + g := NewGomegaWithT(t) + g.Expect(GetTidbServiceName("demo")).To(Equal("demo-tidb")) +}