Compare commits

..

57 Commits

Author SHA1 Message Date
Bo-Yi Wu f9cc37282c update golang to 1.11
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2018-09-26 15:42:19 +08:00
Bo-Yi Wu 6e431b0c53 test: add TestCommandScriptStop (#124) 2018-09-26 15:35:57 +08:00
Marco Vito Moscaritolo 3499506089 exit after first error (#123)
Closes #121 

WIP
2018-09-26 15:23:31 +08:00
Josh Komoroske 6c0b475c15 Customization of logger output destination (#117)
* Customization of logger output destination

* Tests to verify output correctness
2018-02-28 14:52:15 +08:00
Josh Komoroske 60993a71e2 Preservation of forwarded environment (#113)
* Tests for omitting unset variables

* Preservation of forwarded environment
2018-02-28 13:44:44 +08:00
Bo-Yi Wu 8bfc58f9d0 test: Add escapeArg testing (#116) 2018-02-27 14:58:43 +08:00
Damian Kaczmarek 7f4cb1c1d0 improve: shell escaping, allow for whitespace and single quotes (#108) 2018-02-27 14:48:38 +08:00
Josh Komoroske f92f762c9d Load PLUGIN_ENV_FILE before app is run (#112) 2018-02-24 19:47:07 +08:00
Bo-Yi Wu 84cb184039 refactor(drone): replace facebook with discord. 2017-12-18 09:34:36 +08:00
Bo-Yi Wu 31c084fd3e remove unused doc.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-12-18 09:30:13 +08:00
Bo-Yi Wu 69b3a40978 update drone-docker plugin. (#107)
* update drone-docker plugin.

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* add testing

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* add testing

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* add testing

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-11-09 23:32:01 -06:00
Damian Kaczmarek 4d443c40f2 fix: ssh process error not resulting in pipeline error (#105)
* fix: ssh process error not resulting in pipeline error

* Update main.go
2017-11-09 20:47:15 -06:00
Bo-Yi Wu 9dd4b8db8d add arm arm64 and amd64 build. (#106)
* add arm arm64 and amd64 build.

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* add release folder to ignore list.

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-11-09 20:12:30 -06:00
Damian Kaczmarek 45f43d7ffd fix: escaping special characters when passing env to ssh (#104) 2017-11-09 19:01:28 -06:00
Bo-Yi Wu 7220c94832 Add sync mode. (#101)
* Add sync mode.

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* close channel in sync mode.

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* close channel in sync mode.

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-10-29 21:31:51 -05:00
Bo-Yi Wu 2d5668ff17 Update document for mount key path. (#100)
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-10-29 20:46:16 -05:00
Bo-Yi Wu 6f1ace35bf add build number for drone.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-09-04 17:22:52 +08:00
Bo-Yi Wu 05ebe5b663 update docs
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-09-04 14:40:20 +08:00
Bo-Yi Wu e331f975ad update docs.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-09-04 14:38:04 +08:00
Bo-Yi Wu f943ff7179 update dockerfile. (#99)
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-08-19 21:48:30 +08:00
Bo-Yi Wu 65e15c4aab update gopath (#98)
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-08-19 21:45:07 +08:00
Bo-Yi Wu 83273b5669 add cloc command
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-08-19 21:35:53 +08:00
Bo-Yi Wu a8392b5f22 fix typo
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-08-15 15:01:51 +08:00
Bo-Yi Wu e057a699a4 refactor: add group build for drone. (#97)
* refactor: add group build for drone.

* remove codecov_token

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* fix format

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* fix path

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-08-15 14:52:35 +08:00
Bo-Yi Wu 14fddbbba5 feat: add multiple stage build for docker. (#96)
* feat: add multiple stage build for docker.

* remove unused target

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-08-15 14:23:25 +08:00
Bo-Yi Wu 5fbd22f265 refactor(Makefile): allow overriding default go program (#95)
* refactor(Makefile): allow overriding default go program

* fix typo

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-08-15 14:05:11 +08:00
Bo-Yi Wu bf269615ce update docs (#94)
* update docs

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* [ci skip] add username secret.

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-08-13 23:06:31 +08:00
Bo-Yi Wu 538a5a6ce5 remove unsed space.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-08-09 22:25:26 +08:00
DINESH S 78f4f15754 Update doc with custom secrets example (#93) fix #19 2017-08-09 22:24:08 +08:00
Bo-Yi Wu 40323f23e5 update testing
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-08-01 17:24:38 +08:00
Bo-Yi Wu ed83305de8 add debug mode. (#92)
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-08-01 16:54:34 +08:00
Bo-Yi Wu 4e625fa760 feat: add editor config. 2017-08-01 16:04:55 +08:00
Bo-Yi Wu c79b44dca2 update env key.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-08-01 12:51:36 +08:00
Bo-Yi Wu c86c472904 fix: env to ToUpper
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-08-01 12:40:35 +08:00
Bo-Yi Wu ecfaecd46d feat(env): pass secret to remote server. (#91)
* feat(env): pass secret to remote server.

* add testing

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-08-01 12:01:37 +08:00
Bo-Yi Wu e6d4fa77d1 add notify
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-06-24 22:58:13 +08:00
Bo-Yi Wu 9651a4eb6c feat: add check unused package. (#85) 2017-06-03 01:13:19 -05:00
Bo-Yi Wu b5b13e8b72 upgrade easyssh to 1.1.6 (#81)
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-16 15:45:00 +08:00
Bo-Yi Wu 26b3d47ee2 add exit code example.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-13 13:47:00 +08:00
Bo-Yi Wu 0a78278313 add return error code command example.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-12 14:21:33 +08:00
Bo-Yi Wu a7c37e0936 fix panic from easyssh-proxy
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-11 15:14:28 +08:00
Bo-Yi Wu 699d9148d8 update readme.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-10 21:55:56 +08:00
Bo-Yi Wu ceec42efdd bump easyssh package.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-10 21:37:48 +08:00
Bo-Yi Wu 88b5394dac update output
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-10 21:26:34 +08:00
Bo-Yi Wu 1637772e0b hide domain if only single host in config. (#80)
* hide domain if only single host in config.

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* fix host

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* refactor

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-10 21:17:31 +08:00
Bo-Yi Wu efdac217bd update log format.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-10 16:42:37 +08:00
Bo-Yi Wu f81056261d add demo gif
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-10 16:21:22 +08:00
Bo-Yi Wu 3fffe80a14 remove timestamp in log
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-10 15:36:20 +08:00
Bo-Yi Wu 2d568d1fde Support stream output logs. (#79)
* Support stream output logs.

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* fix block channel.

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-10 11:34:00 +08:00
Bo-Yi Wu f26bd7f7f7 update makefile
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-09 15:13:50 +08:00
Bo-Yi Wu 95427edbba update makefile
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-09 15:11:17 +08:00
Bo-Yi Wu 7f168bd1cb update makefile
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-09 15:07:57 +08:00
Bo-Yi Wu b6c973ef1e fix: get exit code from ssh run command. (#78) 2017-05-09 09:38:31 +08:00
Bo-Yi Wu 356b2ae6cc feat: support group build (#76) 2017-05-08 09:06:35 +08:00
Bo-Yi Wu b698d56d60 add some secrets.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-02 22:03:26 +08:00
Bo-Yi Wu 06f4f77ebc update drone to 0.6
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-05-02 21:51:15 +08:00
Bo-Yi Wu b63f275e9e refactor: show errors if set password and key at the same time (#72) 2017-04-23 11:49:40 +08:00
16 changed files with 934 additions and 231 deletions
+64 -36
View File
@@ -1,68 +1,96 @@
workspace:
base: /srv/app
path: src/github.com/appleboy/drone-ssh
base: /go/src
path: github.com/appleboy/drone-ssh
clone:
git:
image: plugins/git
depth: 50
tags: true
pipeline:
clone:
image: plugins/git
tags: true
lint:
image: golang:1.11
pull: true
group: golang
commands:
- make vet
- make lint
- make test-vendor
linux_amd64:
image: golang:1.11
pull: true
group: golang
commands:
- make linux_amd64
linux_arm64:
image: golang:1.11
pull: true
group: golang
commands:
- make linux_arm64
linux_arm:
image: golang:1.11
pull: true
group: golang
commands:
- make linux_arm
test:
image: appleboy/golang-testing
pull: true
environment:
TAGS: netgo
GOPATH: /srv/app
group: golang
commands:
- make ssh-server
- make vet
- make lint
# - make test
- coverage all
- make test
- make coverage
- make build
# build binary for docker image
- make static_build
when:
event: [ push, tag, pull_request ]
release:
image: appleboy/golang-testing
image: golang:1.11
pull: true
environment:
TAGS: netgo
GOPATH: /srv/app
commands:
- make release
when:
event: [ tag ]
branch: [ refs/tags/* ]
local: false
publish_tag:
image: plugins/docker
repo: ${DRONE_REPO}
tags: [ '${DRONE_TAG}' ]
codecov:
image: robertstettner/drone-codecov
secrets: [ codecov_token ]
files:
- .cover/coverage.txt
when:
event: [ tag ]
branch: [ refs/tags/* ]
local: false
event: [ push, pull_request ]
status: [ success ]
publish_latest:
publish:
image: plugins/docker
pull: true
repo: ${DRONE_REPO}
tags: [ 'latest' ]
default_tags: true
secrets: [ docker_username, docker_password ]
group: release
when:
event: [ push ]
branch: [ master ]
event: [ push, tag ]
local: false
release:
release_tag:
image: plugins/github-release
api_key: ${GITHUB_RELEASE_API_KEY}
pull: true
secrets: [ github_release_api_key ]
group: release
files:
- dist/release/*
when:
event: [ tag ]
branch: [ refs/tags/* ]
local: false
discord:
image: appleboy/drone-discord
pull: true
secrets: [ discord_webhook_id, discord_webhook_token ]
when:
status: [ changed, failure ]
+42
View File
@@ -0,0 +1,42 @@
# unifying the coding style for different editors and IDEs => editorconfig.org
; indicate this is the root of the project
root = true
###########################################################
; common
###########################################################
[*]
charset = utf-8
end_of_line = LF
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
###########################################################
; make
###########################################################
[Makefile]
indent_style = tab
[makefile]
indent_style = tab
###########################################################
; markdown
###########################################################
[*.md]
trim_trailing_whitespace = false
###########################################################
; golang
###########################################################
[*.go]
indent_style = tab
+1
View File
@@ -25,5 +25,6 @@ _testmain.go
.env
coverage.txt
release
drone-ssh
.cover
+59 -56
View File
@@ -40,38 +40,6 @@ pipeline:
- echo world
```
Example configuration for login with user private key:
```diff
pipeline:
ssh:
image: appleboy/drone-ssh
host: foo.com
username: root
- password: 1234
+ key: ${DEPLOY_KEY}
port: 22
script:
- echo hello
- echo world
```
Example configuration for login with file path of user private key:
```diff
pipeline:
ssh:
image: appleboy/drone-ssh
host: foo.com
username: root
- password: 1234
+ key_path: ./deploy/key.pem
port: 22
script:
- echo hello
- echo world
```
Example configuration for command timeout (unit: second), default value is 60 seconds:
```diff
@@ -82,7 +50,7 @@ pipeline:
username: root
password: 1234
port: 22
+ command_timeout: 10
+ command_timeout: 120
script:
- echo hello
- echo world
@@ -96,18 +64,49 @@ pipeline:
image: appleboy/drone-ssh
host: foo.com
username: root
password: 1234
port: 22
key: ${DEPLOY_KEY}
script:
- echo hello
- echo world
+ proxy_host: 10.130.33.145
+ proxy_user: ubuntu
+ proxy_port: 22
+ proxy_key: ${PROXY_KEY}
+ proxy_password: 1234
```
Example configuration for success build:
Example configuration using password from secrets:
```diff
pipeline:
ssh:
image: appleboy/drone-ssh
host: foo.com
username: root
- password: 1234
port: 22
+ secrets: [ ssh_password ]
script:
- echo hello
- echo world
```
Example configuration using ssh key from secrets:
```diff
pipeline:
ssh:
image: appleboy/drone-ssh
host: foo.com
username: root
port: 22
+ secrets: [ ssh_key ]
script:
- echo hello
- echo world
```
Example configuration for exporting custom secrets:
```diff
pipeline:
@@ -117,30 +116,31 @@ pipeline:
username: root
password: 1234
port: 22
+ secrets: [ aws_access_key_id ]
+ envs: [ aws_access_key_id ]
script:
- echo hello
- echo world
+ when:
+ status: success
- export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
```
Example configuration for tag event:
# Secret Reference
```diff
pipeline:
ssh:
image: appleboy/drone-ssh
host: foo.com
username: root
password: 1234
port: 22
script:
- echo hello
- echo world
+ when:
+ status: success
+ event: tag
```
ssh_username
: account for target host user
ssh_password
: password for target host user
ssh_key
: plain text of user private key
proxy_ssh_username
: account for user of proxy server
proxy_ssh_password
: password for user of proxy server
proxy_ssh_key
: plain text of user private key for proxy server
# Parameter Reference
@@ -162,6 +162,9 @@ key
key_path
: key path of user private key
envs
: custom secrets which are made available in the script section
script
: execute commands on a remote server
+10 -4
View File
@@ -1,10 +1,16 @@
FROM alpine:3.4
RUN apk update && \
apk add \
ca-certificates \
openssh-client && \
apk add -U --no-cache \
ca-certificates \
openssh-client && \
rm -rf /var/cache/apk/*
ADD drone-ssh /bin/
LABEL org.label-schema.version=latest
LABEL org.label-schema.vcs-url="https://github.com/appleboy/drone-ssh.git"
LABEL org.label-schema.name="drone-ssh"
LABEL org.label-schema.vendor="Bo-Yi Wu"
LABEL org.label-schema.schema-version="1.0"
ADD release/linux/amd64/drone-ssh /bin/
ENTRYPOINT ["/bin/drone-ssh"]
+51 -27
View File
@@ -2,6 +2,7 @@
DIST := dist
EXECUTABLE := drone-ssh
GO ?= go
# for dockerhub
DEPLOY_ACCOUNT := appleboy
@@ -9,11 +10,12 @@ DEPLOY_IMAGE := $(EXECUTABLE)
GOFMT ?= gofmt "-s"
TARGETS ?= linux darwin windows
PACKAGES ?= $(shell go list ./... | grep -v /vendor/)
GOFILES := find . -name "*.go" -type f -not -path "./vendor/*"
PACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/)
GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*")
SOURCES ?= $(shell find . -name "*.go" -type f)
TAGS ?=
LDFLAGS ?= -X 'main.Version=$(VERSION)'
LDFLAGS ?= -X 'main.Version=$(VERSION)' -X 'main.build=$(NUMBER)'
TMPDIR := $(shell mktemp -d 2>/dev/null || mktemp -d -t 'tempdir')
ifneq ($(shell uname), Darwin)
EXTLDFLAGS = -extldflags "-static" $(null)
@@ -23,6 +25,10 @@ endif
ifneq ($(DRONE_TAG),)
VERSION ?= $(DRONE_TAG)
endif
ifneq ($(DRONE_BUILD_NUMBER),)
NUMBER ?= $(DRONE_BUILD_NUMBER)
else
VERSION ?= $(shell git describe --tags --always || git rev-parse --short HEAD)
endif
@@ -31,50 +37,63 @@ all: build
.PHONY: fmt-check
fmt-check:
# get all go files and run go fmt on them
@files=$$($(GOFILES) | xargs $(GOFMT) -l); if [ -n "$$files" ]; then \
@diff=$$($(GOFMT) -d $(GOFILES)); \
if [ -n "$$diff" ]; then \
echo "Please run 'make fmt' and commit the result:"; \
echo "$${files}"; \
echo "$${diff}"; \
exit 1; \
fi;
fi;
.PHONY: test-vendor
test-vendor:
@hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/kardianos/govendor; \
fi
govendor list +unused | tee "$(TMPDIR)/wc-gitea-unused"
[ $$(cat "$(TMPDIR)/wc-gitea-unused" | wc -l) -eq 0 ] || echo "Warning: /!\\ Some vendor are not used /!\\"
govendor list +outside | tee "$(TMPDIR)/wc-gitea-outside"
[ $$(cat "$(TMPDIR)/wc-gitea-outside" | wc -l) -eq 0 ] || exit 1
govendor status || exit 1
fmt:
$(GOFILES) | xargs $(GOFMT) -w
$(GOFMT) -w $(GOFILES)
vet:
go vet $(PACKAGES)
$(GO) vet $(PACKAGES)
errcheck:
@hash errcheck > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/kisielk/errcheck; \
$(GO) get -u github.com/kisielk/errcheck; \
fi
errcheck $(PACKAGES)
lint:
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/golang/lint/golint; \
$(GO) get -u github.com/golang/lint/golint; \
fi
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
unconvert:
@hash unconvert > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/mdempsky/unconvert; \
$(GO) get -u github.com/mdempsky/unconvert; \
fi
for PKG in $(PACKAGES); do unconvert -v $$PKG || exit 1; done;
test: fmt-check
for PKG in $(PACKAGES); do go test -v -cover -coverprofile $$GOPATH/src/$$PKG/coverage.txt $$PKG || exit 1; done;
for PKG in $(PACKAGES); do $(GO) test -v -cover -coverprofile $$GOPATH/src/$$PKG/coverage.txt $$PKG || exit 1; done;
html:
go tool cover -html=coverage.txt
$(GO) tool cover -html=coverage.txt
install: $(SOURCES)
go install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
$(GO) install -v -tags '$(TAGS)' -ldflags "$(EXTLDFLAGS)-s -w $(LDFLAGS)"
build: $(EXECUTABLE)
$(EXECUTABLE): $(SOURCES)
go build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $@
$(GO) build -v -tags '$(TAGS)' -ldflags "$(EXTLDFLAGS)-s -w $(LDFLAGS)" -o $@
release: release-dirs release-build release-copy release-check
@@ -83,7 +102,7 @@ release-dirs:
release-build:
@hash gox > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/mitchellh/gox; \
$(GO) get -u github.com/mitchellh/gox; \
fi
gox -os="$(TARGETS)" -arch="amd64 386" -tags="$(TAGS)" -ldflags="-s -w $(LDFLAGS)" -output="$(DIST)/binaries/$(EXECUTABLE)-$(VERSION)-{{.OS}}-{{.Arch}}"
@@ -93,15 +112,18 @@ release-copy:
release-check:
cd $(DIST)/release; $(foreach file,$(wildcard $(DIST)/release/$(EXECUTABLE)-*),sha256sum $(notdir $(file)) > $(notdir $(file)).sha256;)
# for docker.
static_build:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $(DEPLOY_IMAGE)
linux_amd64:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -a -tags '$(TAGS)' -ldflags "$(EXTLDFLAGS)-s -w $(LDFLAGS)" -o release/linux/amd64/$(EXECUTABLE)
linux_arm64:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -a -tags '$(TAGS)' -ldflags "$(EXTLDFLAGS)-s -w $(LDFLAGS)" -o release/linux/arm64/$(EXECUTABLE)
linux_arm:
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 $(GO) build -a -tags '$(TAGS)' -ldflags "$(EXTLDFLAGS)-s -w $(LDFLAGS)" -o release/arm/amd64/$(EXECUTABLE)
docker_image:
docker build -t $(DEPLOY_ACCOUNT)/$(DEPLOY_IMAGE) .
docker: static_build docker_image
docker_deploy:
ifeq ($(tag),)
@echo "Usage: make $@ tag=<tag>"
@@ -112,13 +134,10 @@ endif
docker push $(DEPLOY_ACCOUNT)/$(DEPLOY_IMAGE):$(tag)
coverage:
sed -i '/main.go/d' .cover/coverage.txt
curl -s https://codecov.io/bash > .codecov && \
chmod +x .codecov && \
./.codecov -f .cover/coverage.txt
sed -i '/main.go/d' coverage.txt
clean:
go clean -x -i ./...
$(GO) clean -x -i ./...
rm -rf coverage.txt $(EXECUTABLE) $(DIST) vendor
ssh-server:
@@ -133,5 +152,10 @@ ssh-server:
rm -rf /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_dsa_key
./tests/entrypoint.sh /usr/sbin/sshd -D &
# Show source statistics.
cloc:
@cloc -exclude-dir=vendor,node_modules .
.PHONY: cloc
version:
@echo $(VERSION)
+24
View File
@@ -9,6 +9,7 @@ information and a listing of the available options please take a look at [the do
**Note: Please update your image config path to `appleboy/drone-ssh` for drone. `plugins/ssh` is no longer maintained.**
![demo](./screenshot/demo2017.05.10.gif)
## Build
Build the binary with the following commands:
@@ -49,3 +50,26 @@ docker run --rm \
-w $(pwd) \
appleboy/drone-ssh
```
## Mount key from file path
Please make sure that enable the `trusted` mode in project setting.
![trusted mode](./screenshot/trust.png)
Mount private key in `volumes` setting of `.drone.yml` config
```diff
pipeline:
ssh:
image: appleboy/drone-ssh
host: xxxxx.com
username: deploy
+ volumes:
+ - /root/drone_rsa:/root/ssh/drone_rsa
key_path: /root/ssh/drone_rsa
script:
- echo "test ssh"
```
See the detail of [issue comment](https://github.com/appleboy/drone-ssh/issues/51#issuecomment-336732928).
+48 -9
View File
@@ -1,6 +1,7 @@
package main
import (
"fmt"
"os"
"github.com/appleboy/easyssh-proxy"
@@ -9,10 +10,22 @@ import (
"github.com/urfave/cli"
)
// build number set at compile-time
var build = "0"
// Version set at compile-time
var Version = "v1.1.0-dev"
var Version string
func main() {
if Version == "" {
Version = fmt.Sprintf("1.3.1+%s", build)
}
// Load env-file if it exists first
if filename, found := os.LookupEnv("PLUGIN_ENV_FILE"); found {
_ = godotenv.Load(filename)
}
app := cli.NewApp()
app.Name = "Drone SSH"
app.Usage = "Executing remote ssh commands"
@@ -58,6 +71,11 @@ func main() {
EnvVar: "PLUGIN_PORT,SSH_PORT",
Value: 22,
},
cli.BoolFlag{
Name: "sync",
Usage: "sync mode",
EnvVar: "PLUGIN_SYNC",
},
cli.DurationFlag{
Name: "timeout,t",
Usage: "connection timeout",
@@ -74,9 +92,10 @@ func main() {
Usage: "execute commands",
EnvVar: "PLUGIN_SCRIPT,SSH_SCRIPT",
},
cli.StringFlag{
Name: "env-file",
Usage: "source env file",
cli.BoolFlag{
Name: "script.stop",
Usage: "stop script after first failure",
EnvVar: "PLUGIN_SCRIPT_STOP",
},
cli.StringFlag{
Name: "proxy.ssh-key",
@@ -115,6 +134,21 @@ func main() {
Usage: "proxy connection timeout",
EnvVar: "PLUGIN_PROXY_TIMEOUT,PROXY_SSH_TIMEOUT",
},
cli.StringSliceFlag{
Name: "secrets",
Usage: "plugin secret",
EnvVar: "PLUGIN_SECRETS",
},
cli.StringSliceFlag{
Name: "envs",
Usage: "Pass envs",
EnvVar: "PLUGIN_ENVS",
},
cli.BoolFlag{
Name: "debug",
Usage: "debug mode",
EnvVar: "PLUGIN_DEBUG",
},
}
// Override a template
@@ -150,14 +184,13 @@ REPOSITORY:
Github: https://github.com/appleboy/drone-ssh
`
app.Run(os.Args)
if err := app.Run(os.Args); err != nil {
fmt.Println("drone-ssh error: ", err)
os.Exit(1)
}
}
func run(c *cli.Context) error {
if c.String("env-file") != "" {
_ = godotenv.Load(c.String("env-file"))
}
plugin := Plugin{
Config: Config{
Key: c.String("ssh-key"),
@@ -169,6 +202,11 @@ func run(c *cli.Context) error {
Timeout: c.Duration("timeout"),
CommandTimeout: c.Int("command.timeout"),
Script: c.StringSlice("script"),
ScriptStop: c.Bool("script.stop"),
Secrets: c.StringSlice("secrets"),
Envs: c.StringSlice("envs"),
Debug: c.Bool("debug"),
Sync: c.Bool("sync"),
Proxy: easyssh.DefaultConfig{
Key: c.String("proxy.ssh-key"),
KeyPath: c.String("proxy.key-path"),
@@ -179,6 +217,7 @@ func run(c *cli.Context) error {
Timeout: c.Duration("proxy.timeout"),
},
},
Writer: os.Stdout,
}
return plugin.Exec()
+129 -46
View File
@@ -2,7 +2,8 @@ package main
import (
"fmt"
"log"
"io"
"os"
"strconv"
"strings"
"sync"
@@ -11,12 +12,11 @@ import (
"github.com/appleboy/easyssh-proxy"
)
var wg sync.WaitGroup
const (
missingHostOrUser = "Error: missing server host or user"
missingPasswordOrKey = "Error: can't connect without a private SSH key or password"
commandTimeOut = "Error: command timeout"
setPasswordandKey = "can't set password and key at the same time"
)
type (
@@ -31,71 +31,134 @@ type (
Timeout time.Duration
CommandTimeout int
Script []string
ScriptStop bool
Secrets []string
Envs []string
Proxy easyssh.DefaultConfig
Debug bool
Sync bool
}
// Plugin structure
Plugin struct {
Config Config
Writer io.Writer
}
)
func escapeArg(arg string) string {
return "'" + strings.Replace(arg, "'", `'\''`, -1) + "'"
}
func (p Plugin) exec(host string, wg *sync.WaitGroup, errChannel chan error) {
// Create MakeConfig instance with remote username, server address and path to private key.
ssh := &easyssh.MakeConfig{
Server: host,
User: p.Config.UserName,
Password: p.Config.Password,
Port: strconv.Itoa(p.Config.Port),
Key: p.Config.Key,
KeyPath: p.Config.KeyPath,
Timeout: p.Config.Timeout,
Proxy: easyssh.DefaultConfig{
Server: p.Config.Proxy.Server,
User: p.Config.Proxy.User,
Password: p.Config.Proxy.Password,
Port: p.Config.Proxy.Port,
Key: p.Config.Proxy.Key,
KeyPath: p.Config.Proxy.KeyPath,
Timeout: p.Config.Proxy.Timeout,
},
}
p.log(host, "======CMD======")
p.log(host, strings.Join(p.Config.Script, "\n"))
p.log(host, "======END======")
env := []string{}
for _, key := range p.Config.Envs {
key = strings.ToUpper(key)
if val, found := os.LookupEnv(key); found {
env = append(env, key+"="+escapeArg(val))
}
}
p.Config.Script = append(env, p.scriptCommands()...)
if p.Config.Debug {
p.log(host, "======ENV======")
p.log(host, strings.Join(env, "\n"))
p.log(host, "======END======")
}
stdoutChan, stderrChan, doneChan, errChan, err := ssh.Stream(strings.Join(p.Config.Script, "\n"), p.Config.CommandTimeout)
if err != nil {
errChannel <- err
} else {
// read from the output channel until the done signal is passed
isTimeout := true
loop:
for {
select {
case isTimeout = <-doneChan:
break loop
case outline := <-stdoutChan:
p.log(host, "out:", outline)
case errline := <-stderrChan:
p.log(host, "err:", errline)
case err = <-errChan:
}
}
// get exit code or command error.
if err != nil {
errChannel <- err
}
// command time out
if !isTimeout {
errChannel <- fmt.Errorf(commandTimeOut)
}
}
wg.Done()
}
func (p Plugin) log(host string, message ...interface{}) {
log.Printf("%s: %s", host, fmt.Sprintln(message...))
if p.Writer == nil {
p.Writer = os.Stdout
}
if count := len(p.Config.Host); count == 1 {
fmt.Fprintf(p.Writer, "%s", fmt.Sprintln(message...))
} else {
fmt.Fprintf(p.Writer, "%s: %s", host, fmt.Sprintln(message...))
}
}
// Exec executes the plugin.
func (p Plugin) Exec() error {
if len(p.Config.Host) == 0 && p.Config.UserName == "" {
if len(p.Config.Host) == 0 && len(p.Config.UserName) == 0 {
return fmt.Errorf(missingHostOrUser)
}
if p.Config.Key == "" && p.Config.Password == "" && p.Config.KeyPath == "" {
if len(p.Config.Key) == 0 && len(p.Config.Password) == 0 && len(p.Config.KeyPath) == 0 {
return fmt.Errorf(missingPasswordOrKey)
}
if len(p.Config.Key) != 0 && len(p.Config.Password) != 0 {
return fmt.Errorf(setPasswordandKey)
}
wg := sync.WaitGroup{}
wg.Add(len(p.Config.Host))
errChannel := make(chan error, 1)
finished := make(chan bool, 1)
for _, host := range p.Config.Host {
go func(host string) {
// Create MakeConfig instance with remote username, server address and path to private key.
ssh := &easyssh.MakeConfig{
Server: host,
User: p.Config.UserName,
Password: p.Config.Password,
Port: strconv.Itoa(p.Config.Port),
Key: p.Config.Key,
KeyPath: p.Config.KeyPath,
Timeout: p.Config.Timeout,
Proxy: easyssh.DefaultConfig{
Server: p.Config.Proxy.Server,
User: p.Config.Proxy.User,
Password: p.Config.Proxy.Password,
Port: p.Config.Proxy.Port,
Key: p.Config.Proxy.Key,
KeyPath: p.Config.Proxy.KeyPath,
Timeout: p.Config.Proxy.Timeout,
},
}
p.log(host, "commands: ", strings.Join(p.Config.Script, "\n"))
outStr, errStr, isTimeout, err := ssh.Run(strings.Join(p.Config.Script, "\n"), p.Config.CommandTimeout)
p.log(host, "outputs:", outStr)
if len(errStr) != 0 {
p.log(host, "errors:", errStr)
}
if err != nil {
errChannel <- err
}
if !isTimeout {
errChannel <- fmt.Errorf(commandTimeOut)
}
wg.Done()
}(host)
if p.Config.Sync {
p.exec(host, &wg, errChannel)
} else {
go p.exec(host, &wg, errChannel)
}
}
go func() {
@@ -107,12 +170,32 @@ func (p Plugin) Exec() error {
case <-finished:
case err := <-errChannel:
if err != nil {
log.Println("drone-ssh error: ", err)
return err
}
}
log.Println("Successfully executed commands to all host.")
fmt.Println("==========================================")
fmt.Println("Successfully executed commands to all host.")
fmt.Println("==========================================")
return nil
}
func (p Plugin) scriptCommands() []string {
numCommands := len(p.Config.Script)
if p.Config.ScriptStop {
numCommands *= 2
}
commands := make([]string, numCommands)
for _, cmd := range p.Config.Script {
if p.Config.ScriptStop {
commands = append(commands, "DRONE_SSH_PREV_COMMAND_EXIT_CODE=$? ; if [ $DRONE_SSH_PREV_COMMAND_EXIT_CODE -ne 0 ]; then exit $DRONE_SSH_PREV_COMMAND_EXIT_CODE; fi;")
}
commands = append(commands, cmd)
}
return commands
}
+386
View File
@@ -1,6 +1,9 @@
package main
import (
"bytes"
"os"
"strings"
"testing"
"github.com/appleboy/easyssh-proxy"
@@ -22,6 +25,7 @@ func TestMissingKeyOrPassword(t *testing.T) {
Host: []string{"localhost"},
UserName: "ubuntu",
},
os.Stdout,
}
err := plugin.Exec()
@@ -30,6 +34,23 @@ func TestMissingKeyOrPassword(t *testing.T) {
assert.Equal(t, missingPasswordOrKey, err.Error())
}
func TestSetPasswordAndKey(t *testing.T) {
plugin := Plugin{
Config{
Host: []string{"localhost"},
UserName: "ubuntu",
Password: "1234",
Key: "1234",
},
os.Stdout,
}
err := plugin.Exec()
assert.NotNil(t, err)
assert.Equal(t, setPasswordandKey, err.Error())
}
func TestIncorrectPassword(t *testing.T) {
plugin := Plugin{
Config: Config{
@@ -105,6 +126,39 @@ func TestSSHScriptFromKeyFile(t *testing.T) {
assert.Nil(t, err)
}
func TestStreamFromSSHCommand(t *testing.T) {
plugin := Plugin{
Config: Config{
Host: []string{"localhost", "127.0.0.1"},
UserName: "drone-scp",
Port: 22,
KeyPath: "./tests/.ssh/id_rsa",
Script: []string{"whoami", "for i in {1..5}; do echo ${i}; sleep 1; done", "echo 'done'"},
CommandTimeout: 60,
},
}
err := plugin.Exec()
assert.Nil(t, err)
}
func TestSSHScriptWithError(t *testing.T) {
plugin := Plugin{
Config: Config{
Host: []string{"localhost", "127.0.0.1"},
UserName: "drone-scp",
Port: 22,
KeyPath: "./tests/.ssh/id_rsa",
Script: []string{"exit 1"},
CommandTimeout: 60,
},
}
err := plugin.Exec()
// Process exited with status 1
assert.NotNil(t, err)
}
func TestSSHCommandTimeOut(t *testing.T) {
plugin := Plugin{
Config: Config{
@@ -142,3 +196,335 @@ func TestProxyCommand(t *testing.T) {
err := plugin.Exec()
assert.Nil(t, err)
}
func TestSSHCommandError(t *testing.T) {
plugin := Plugin{
Config: Config{
Host: []string{"localhost"},
UserName: "drone-scp",
Port: 22,
KeyPath: "./tests/.ssh/id_rsa",
Script: []string{"mkdir a", "mkdir a"},
CommandTimeout: 60,
},
}
err := plugin.Exec()
assert.NotNil(t, err)
}
func TestSSHCommandExitCodeError(t *testing.T) {
plugin := Plugin{
Config: Config{
Host: []string{"localhost"},
UserName: "drone-scp",
Port: 22,
KeyPath: "./tests/.ssh/id_rsa",
Script: []string{
"set -e",
"echo 1",
"mkdir a",
"mkdir a",
"echo 2",
},
CommandTimeout: 60,
},
}
err := plugin.Exec()
assert.NotNil(t, err)
}
func TestSetENV(t *testing.T) {
os.Setenv("FOO", `' 1) '`)
plugin := Plugin{
Config: Config{
Host: []string{"localhost"},
UserName: "drone-scp",
Port: 22,
KeyPath: "./tests/.ssh/id_rsa",
Secrets: []string{"FOO"},
Envs: []string{"foo"},
Debug: true,
Script: []string{"whoami; echo $FOO"},
CommandTimeout: 1,
Proxy: easyssh.DefaultConfig{
Server: "localhost",
User: "drone-scp",
Port: "22",
KeyPath: "./tests/.ssh/id_rsa",
},
},
}
err := plugin.Exec()
assert.Nil(t, err)
}
func TestSetExistingENV(t *testing.T) {
os.Setenv("FOO", "Value for foo")
os.Setenv("BAR", "")
plugin := Plugin{
Config: Config{
Host: []string{"localhost"},
UserName: "drone-scp",
Port: 22,
KeyPath: "./tests/.ssh/id_rsa",
Secrets: []string{"FOO"},
Envs: []string{"foo", "bar", "baz"},
Debug: true,
Script: []string{"export FOO", "export BAR", "export BAZ", "env | grep -q '^FOO=Value for foo$'", "env | grep -q '^BAR=$'", "if env | grep -q BAZ; then false; else true; fi"},
CommandTimeout: 1,
Proxy: easyssh.DefaultConfig{
Server: "localhost",
User: "drone-scp",
Port: "22",
KeyPath: "./tests/.ssh/id_rsa",
},
},
}
err := plugin.Exec()
assert.Nil(t, err)
}
func TestSyncMode(t *testing.T) {
plugin := Plugin{
Config: Config{
Host: []string{"localhost", "127.0.0.1"},
UserName: "drone-scp",
Port: 22,
KeyPath: "./tests/.ssh/id_rsa",
Script: []string{"whoami", "for i in {1..3}; do echo ${i}; sleep 1; done", "echo 'done'"},
CommandTimeout: 60,
Sync: true,
},
}
err := plugin.Exec()
assert.Nil(t, err)
}
func Test_escapeArg(t *testing.T) {
type args struct {
arg string
}
tests := []struct {
name string
args args
want string
}{
{
name: "escape nothing",
args: args{
arg: "Hi I am appleboy",
},
want: `'Hi I am appleboy'`,
},
{
name: "escape single quote",
args: args{
arg: "Hi I am 'appleboy'",
},
want: `'Hi I am '\''appleboy'\'''`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := escapeArg(tt.args.arg)
assert.Equal(t, tt.want, got)
})
}
}
func TestCommandOutput(t *testing.T) {
var (
buffer bytes.Buffer
expected = `
localhost: ======CMD======
localhost: pwd
whoami
uname
localhost: ======END======
localhost: out: /home/drone-scp
localhost: out: drone-scp
localhost: out: Linux
127.0.0.1: ======CMD======
127.0.0.1: pwd
whoami
uname
127.0.0.1: ======END======
127.0.0.1: out: /home/drone-scp
127.0.0.1: out: drone-scp
127.0.0.1: out: Linux
`
)
plugin := Plugin{
Config: Config{
Host: []string{"localhost", "127.0.0.1"},
UserName: "drone-scp",
Port: 22,
KeyPath: "./tests/.ssh/id_rsa",
Script: []string{
"pwd",
"whoami",
"uname",
},
CommandTimeout: 60,
Sync: true,
},
Writer: &buffer,
}
err := plugin.Exec()
assert.Nil(t, err)
assert.Equal(t, unindent(expected), unindent(buffer.String()))
}
func TestScriptStop(t *testing.T) {
var (
buffer bytes.Buffer
expected = `
======CMD======
mkdir a/b/c
mkdir d/e/f
======END======
err: mkdir: can't create directory 'a/b/c': No such file or directory
`
)
plugin := Plugin{
Config: Config{
Host: []string{"localhost"},
UserName: "drone-scp",
Port: 22,
KeyPath: "./tests/.ssh/id_rsa",
Script: []string{
"mkdir a/b/c",
"mkdir d/e/f",
},
CommandTimeout: 10,
ScriptStop: true,
},
Writer: &buffer,
}
err := plugin.Exec()
assert.NotNil(t, err)
assert.Equal(t, unindent(expected), unindent(buffer.String()))
}
func TestNoneScriptStop(t *testing.T) {
var (
buffer bytes.Buffer
expected = `
======CMD======
mkdir a/b/c
mkdir d/e/f
======END======
err: mkdir: can't create directory 'a/b/c': No such file or directory
err: mkdir: can't create directory 'd/e/f': No such file or directory
`
)
plugin := Plugin{
Config: Config{
Host: []string{"localhost"},
UserName: "drone-scp",
Port: 22,
KeyPath: "./tests/.ssh/id_rsa",
Script: []string{
"mkdir a/b/c",
"mkdir d/e/f",
},
CommandTimeout: 10,
},
Writer: &buffer,
}
err := plugin.Exec()
assert.NotNil(t, err)
assert.Equal(t, unindent(expected), unindent(buffer.String()))
}
func TestEnvOutput(t *testing.T) {
var (
buffer bytes.Buffer
expected = `
======CMD======
echo "[${ENV_1}]"
echo "[${ENV_2}]"
echo "[${ENV_3}]"
echo "[${ENV_4}]"
echo "[${ENV_5}]"
echo "[${ENV_6}]"
echo "[${ENV_7}]"
======END======
======ENV======
ENV_1='test'
ENV_2='test test'
ENV_3='test '
ENV_4=' test test '
ENV_5='test'\'''
ENV_6='test"'
ENV_7='test,!#;?.@$~'\''"'
======END======
out: [test]
out: [test test]
out: [test ]
out: [ test test ]
out: [test']
out: [test"]
out: [test,!#;?.@$~'"]
`
)
os.Setenv("ENV_1", `test`)
os.Setenv("ENV_2", `test test`)
os.Setenv("ENV_3", `test `)
os.Setenv("ENV_4", ` test test `)
os.Setenv("ENV_5", `test'`)
os.Setenv("ENV_6", `test"`)
os.Setenv("ENV_7", `test,!#;?.@$~'"`)
plugin := Plugin{
Config: Config{
Host: []string{"localhost"},
UserName: "drone-scp",
Port: 22,
KeyPath: "./tests/.ssh/id_rsa",
Envs: []string{"env_1", "env_2", "env_3", "env_4", "env_5", "env_6", "env_7"},
Debug: true,
Script: []string{
`echo "[${ENV_1}]"`,
`echo "[${ENV_2}]"`,
`echo "[${ENV_3}]"`,
`echo "[${ENV_4}]"`,
`echo "[${ENV_5}]"`,
`echo "[${ENV_6}]"`,
`echo "[${ENV_7}]"`,
},
CommandTimeout: 10,
Proxy: easyssh.DefaultConfig{
Server: "localhost",
User: "drone-scp",
Port: "22",
KeyPath: "./tests/.ssh/id_rsa",
},
},
Writer: &buffer,
}
err := plugin.Exec()
assert.Nil(t, err)
assert.Equal(t, unindent(expected), unindent(buffer.String()))
}
func unindent(text string) string {
return strings.TrimSpace(strings.Replace(text, "\t", "", -1))
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+6 -6
View File
@@ -1,8 +1,7 @@
.PHONY: test drone-ssh fmt vet errcheck lint install update coverage embedmd
GOFMT ?= gofmt "-s"
GOFILES := find . -name "*.go" -type f -not -path "./vendor/*"
GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*")
PACKAGES ?= $(shell go list ./... | grep -v /vendor/)
all: install lint
@@ -14,16 +13,17 @@ install:
govendor sync
fmt:
$(GOFILES) | xargs $(GOFMT) -w
$(GOFMT) -w $(GOFILES)
.PHONY: fmt-check
fmt-check:
# get all go files and run go fmt on them
@files=$$($(GOFILES) | xargs $(GOFMT) -l); if [ -n "$$files" ]; then \
@diff=$$($(GOFMT) -d $(GOFILES)); \
if [ -n "$$diff" ]; then \
echo "Please run 'make fmt' and commit the result:"; \
echo "$${files}"; \
echo "$${diff}"; \
exit 1; \
fi;
fi;
vet:
go vet $(PACKAGES)
+56 -1
View File
@@ -1,6 +1,11 @@
# easyssh-proxy
[![GoDoc](https://godoc.org/github.com/appleboy/easyssh-proxy?status.svg)](https://godoc.org/github.com/appleboy/easyssh-proxy) [![Build Status](http://drone.wu-boy.com/api/badges/appleboy/easyssh-proxy/status.svg)](http://drone.wu-boy.com/appleboy/easyssh-proxy) [![codecov](https://codecov.io/gh/appleboy/easyssh-proxy/branch/master/graph/badge.svg)](https://codecov.io/gh/appleboy/easyssh-proxy) [![Go Report Card](https://goreportcard.com/badge/github.com/appleboy/easyssh-proxy)](https://goreportcard.com/report/github.com/appleboy/easyssh-proxy) [![Sourcegraph](https://sourcegraph.com/github.com/appleboy/easyssh-proxy/-/badge.svg)](https://sourcegraph.com/github.com/appleboy/easyssh-proxy?badge)
[![GoDoc](https://godoc.org/github.com/appleboy/easyssh-proxy?status.svg)](https://godoc.org/github.com/appleboy/easyssh-proxy)
[![Build Status](http://drone.wu-boy.com/api/badges/appleboy/easyssh-proxy/status.svg)](http://drone.wu-boy.com/appleboy/easyssh-proxy)
[![codecov](https://codecov.io/gh/appleboy/easyssh-proxy/branch/master/graph/badge.svg)](https://codecov.io/gh/appleboy/easyssh-proxy)
[![Go Report Card](https://goreportcard.com/badge/github.com/appleboy/easyssh-proxy)](https://goreportcard.com/report/github.com/appleboy/easyssh-proxy)
[![Sourcegraph](https://sourcegraph.com/github.com/appleboy/easyssh-proxy/-/badge.svg)](https://sourcegraph.com/github.com/appleboy/easyssh-proxy?badge)
[![Release](https://github-release-version.herokuapp.com/github/appleboy/easyssh-proxy/release.svg?style=flat)](https://github.com/appleboy/easyssh-proxy/releases/latest)
easyssh-proxy provides a simple implementation of some SSH protocol features in Go.
@@ -124,3 +129,53 @@ See [example/proxy/proxy.go](./example/proxy/proxy.go)
},
}
```
### SSH Stream Log
See [example/stream/stream.go](./example/stream/stream.go)
[embedmd]:# (example/stream/stream.go go /func/ /^}$/)
```go
func main() {
// Create MakeConfig instance with remote username, server address and path to private key.
ssh := &easyssh.MakeConfig{
Server: "localhost",
User: "drone-scp",
KeyPath: "./tests/.ssh/id_rsa",
Port: "22",
Timeout: 60 * time.Second,
}
// Call Run method with command you want to run on remote server.
stdoutChan, stderrChan, doneChan, errChan, err := ssh.Stream("for i in {1..5}; do echo ${i}; sleep 1; done; exit 2;", 60)
// Handle errors
if err != nil {
panic("Can't run remote command: " + err.Error())
} else {
// read from the output channel until the done signal is passed
isTimeout := true
loop:
for {
select {
case isTimeout = <-doneChan:
break loop
case outline := <-stdoutChan:
fmt.Println("out:", outline)
case errline := <-stderrChan:
fmt.Println("err:", errline)
case err = <-errChan:
}
}
// get exit code or command error.
if err != nil {
fmt.Println("err: " + err.Error())
}
// command time out
if !isTimeout {
fmt.Println("Error: command timeout")
}
}
}
```
+53 -41
View File
@@ -86,8 +86,9 @@ func getSSHConfig(config DefaultConfig) *ssh.ClientConfig {
}
if config.Key != "" {
signer, _ := ssh.ParsePrivateKey([]byte(config.Key))
auths = append(auths, ssh.PublicKeys(signer))
if signer, err := ssh.ParsePrivateKey([]byte(config.Key)); err == nil {
auths = append(auths, ssh.PublicKeys(signer))
}
}
return &ssh.ClientConfig{
@@ -155,36 +156,45 @@ func (ssh_conf *MakeConfig) connect() (*ssh.Session, error) {
// Stream returns one channel that combines the stdout and stderr of the command
// as it is run on the remote machine, and another that sends true when the
// command is done. The sessions and channels will then be closed.
func (ssh_conf *MakeConfig) Stream(command string, timeout int) (stdout chan string, stderr chan string, done chan bool, err error) {
// connect to remote host
session, err := ssh_conf.connect()
if err != nil {
return stdout, stderr, done, err
}
// connect to both outputs (they are of type io.Reader)
outReader, err := session.StdoutPipe()
if err != nil {
return stdout, stderr, done, err
}
errReader, err := session.StderrPipe()
if err != nil {
return stdout, stderr, done, err
}
// combine outputs, create a line-by-line scanner
stdoutReader := io.MultiReader(outReader)
stderrReader := io.MultiReader(errReader)
err = session.Start(command)
stdoutScanner := bufio.NewScanner(stdoutReader)
stderrScanner := bufio.NewScanner(stderrReader)
func (ssh_conf *MakeConfig) Stream(command string, timeout int) (<-chan string, <-chan string, <-chan bool, <-chan error, error) {
// continuously send the command's output over the channel
stdoutChan := make(chan string)
stderrChan := make(chan string)
done = make(chan bool)
doneChan := make(chan bool)
errChan := make(chan error)
go func(stdoutScanner, stderrScanner *bufio.Scanner, stdoutChan, stderrChan chan string, done chan bool) {
// connect to remote host
session, err := ssh_conf.connect()
if err != nil {
return stdoutChan, stderrChan, doneChan, errChan, err
}
// defer session.Close()
// connect to both outputs (they are of type io.Reader)
outReader, err := session.StdoutPipe()
if err != nil {
return stdoutChan, stderrChan, doneChan, errChan, err
}
errReader, err := session.StderrPipe()
if err != nil {
return stdoutChan, stderrChan, doneChan, errChan, err
}
err = session.Start(command)
if err != nil {
return stdoutChan, stderrChan, doneChan, errChan, err
}
// combine outputs, create a line-by-line scanner
stdoutReader := io.MultiReader(outReader)
stderrReader := io.MultiReader(errReader)
stdoutScanner := bufio.NewScanner(stdoutReader)
stderrScanner := bufio.NewScanner(stderrReader)
go func(stdoutScanner, stderrScanner *bufio.Scanner, stdoutChan, stderrChan chan string, doneChan chan bool, errChan chan error) {
defer close(stdoutChan)
defer close(stderrChan)
defer close(done)
defer close(doneChan)
defer close(errChan)
defer session.Close()
timeoutChan := time.After(time.Duration(timeout) * time.Second)
res := make(chan bool, 1)
@@ -202,32 +212,30 @@ func (ssh_conf *MakeConfig) Stream(command string, timeout int) (stdout chan str
select {
case <-res:
stdoutChan <- ""
stderrChan <- ""
done <- true
errChan <- session.Wait()
doneChan <- true
case <-timeoutChan:
stdoutChan <- ""
stderrChan <- "Run Command Timeout!"
done <- false
errChan <- nil
doneChan <- false
}
}(stdoutScanner, stderrScanner, stdoutChan, stderrChan, doneChan, errChan)
session.Close()
}(stdoutScanner, stderrScanner, stdoutChan, stderrChan, done)
return stdoutChan, stderrChan, done, err
return stdoutChan, stderrChan, doneChan, errChan, err
}
// Run command on remote machine and returns its stdout as a string
func (ssh_conf *MakeConfig) Run(command string, timeout int) (outStr string, errStr string, isTimeout bool, err error) {
stdoutChan, stderrChan, doneChan, err := ssh_conf.Stream(command, timeout)
stdoutChan, stderrChan, doneChan, errChan, err := ssh_conf.Stream(command, timeout)
if err != nil {
return outStr, errStr, isTimeout, err
}
// read from the output channel until the done signal is passed
stillGoing := true
for stillGoing {
loop:
for {
select {
case isTimeout = <-doneChan:
stillGoing = false
break loop
case outline := <-stdoutChan:
if outline != "" {
outStr += outline + "\n"
@@ -236,6 +244,7 @@ func (ssh_conf *MakeConfig) Run(command string, timeout int) (outStr string, err
if errline != "" {
errStr += errline + "\n"
}
case err = <-errChan:
}
}
// return the concatenation of all signals from the output channel
@@ -266,17 +275,20 @@ func (ssh_conf *MakeConfig) Scp(sourceFile string, etargetFile string) error {
}
go func() {
w, _ := session.StdinPipe()
w, err := session.StdinPipe()
if err != nil {
return
}
defer w.Close()
fmt.Fprintln(w, "C0644", srcStat.Size(), targetFile)
if srcStat.Size() > 0 {
io.Copy(w, src)
fmt.Fprint(w, "\x00")
w.Close()
} else {
fmt.Fprint(w, "\x00")
w.Close()
}
}()
+5 -5
View File
@@ -3,12 +3,12 @@
"ignore": "test",
"package": [
{
"checksumSHA1": "L3PugNJJOEpRmRbD+27LgTZC2E4=",
"checksumSHA1": "EcF7T9tPEMMJfuRdPBB3NdRUg4c=",
"path": "github.com/appleboy/easyssh-proxy",
"revision": "a13ed86767b8e8a24d8147a4909a702e7cf6b465",
"revisionTime": "2017-04-14T13:46:38Z",
"version": "1.1.2",
"versionExact": "1.1.2"
"revision": "33d87eae3a018c3312e32cc4eb4578d5a563aabd",
"revisionTime": "2017-05-16T07:22:25Z",
"version": "1.1.6",
"versionExact": "1.1.6"
},
{
"checksumSHA1": "dvabztWVQX8f6oMLRyv4dLH+TGY=",