├── .drone.yml ├── .github ├── code_of_conduct.md ├── issue_template.md ├── pull_request_template.md └── security.md ├── .gitignore ├── BUILDING.md ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── command ├── command.go ├── compile.go ├── daemon │ ├── config.go │ └── daemon.go ├── exec.go └── internal │ └── flags.go ├── docker ├── Dockerfile.linux.amd64 ├── Dockerfile.linux.arm64 └── manifest.tmpl ├── engine ├── compiler │ ├── compiler.go │ ├── compiler_test.go │ ├── os.go │ ├── os_test.go │ ├── testdata │ │ ├── graph.json │ │ ├── graph.yml │ │ ├── match.json │ │ ├── match.yml │ │ ├── noclone_graph.json │ │ ├── noclone_graph.yml │ │ ├── noclone_serial.json │ │ ├── noclone_serial.yml │ │ ├── run_always.json │ │ ├── run_always.yml │ │ ├── run_failure.json │ │ ├── run_failure.yml │ │ ├── secret.yml │ │ ├── serial.json │ │ └── serial.yml │ ├── util.go │ └── util_test.go ├── engine.go ├── engine_impl.go ├── replacer │ ├── replacer.go │ └── replacer_test.go ├── resource │ ├── lookup.go │ ├── lookup_test.go │ ├── parser.go │ ├── parser_test.go │ ├── pipeline.go │ ├── pipeline_test.go │ └── testdata │ │ ├── linterr.yml │ │ ├── malformed.yml │ │ ├── manifest.yml │ │ └── nomatch.yml ├── spec.go ├── util.go └── util_test.go ├── go.mod ├── go.sum ├── internal ├── internal.go ├── match │ ├── match.go │ └── match_test.go └── mock │ └── mock.go ├── licenses ├── Polyform-Free-Trial.md ├── Polyform-Noncommercial.md └── Polyform-Small-Business.md ├── main.go ├── runtime ├── execer.go ├── execer_test.go ├── poller.go ├── poller_test.go ├── runner.go └── runner_test.go └── scripts └── build.sh /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: testing 3 | type: vm 4 | 5 | pool: 6 | use: ubuntu 7 | 8 | platform: 9 | os: linux 10 | arch: amd64 11 | 12 | steps: 13 | - name: test 14 | image: golang:1.13 15 | commands: 16 | - go test -cover ./... 17 | 18 | --- 19 | kind: pipeline 20 | type: vm 21 | name: linux-amd64 22 | 23 | platform: 24 | os: linux 25 | arch: amd64 26 | 27 | pool: 28 | use: ubuntu 29 | 30 | steps: 31 | - name: build 32 | image: golang:1.13 33 | commands: 34 | - sh scripts/build.sh 35 | - name: publish_amd64 36 | image: plugins/docker 37 | pull: if-not-exists 38 | settings: 39 | repo: drone/drone-runner-ssh 40 | auto_tag: true 41 | auto_tag_suffix: linux-amd64 42 | dockerfile: docker/Dockerfile.linux.amd64 43 | username: 44 | from_secret: docker_username 45 | password: 46 | from_secret: docker_password 47 | when: 48 | ref: 49 | - refs/heads/master 50 | - refs/tags/** 51 | 52 | depends_on: 53 | - testing 54 | 55 | trigger: 56 | ref: 57 | - refs/heads/master 58 | - refs/tags/** 59 | - refs/pull/** 60 | 61 | --- 62 | kind: pipeline 63 | type: vm 64 | name: linux-arm64 65 | 66 | platform: 67 | os: linux 68 | arch: arm64 69 | 70 | pool: 71 | use: ubuntu_arm64 72 | 73 | steps: 74 | - name: build 75 | image: golang:1.13 76 | commands: 77 | - sh scripts/build.sh 78 | volumes: 79 | - name: go 80 | path: /go 81 | - name: publish_arm64 82 | image: plugins/docker 83 | pull: if-not-exists 84 | settings: 85 | repo: drone/drone-runner-ssh 86 | auto_tag: true 87 | auto_tag_suffix: linux-arm64 88 | dockerfile: docker/Dockerfile.linux.arm64 89 | username: 90 | from_secret: docker_username 91 | password: 92 | from_secret: docker_password 93 | when: 94 | ref: 95 | - refs/heads/master 96 | - refs/tags/** 97 | 98 | depends_on: 99 | - testing 100 | 101 | trigger: 102 | ref: 103 | - refs/heads/master 104 | - refs/tags/** 105 | - refs/pull/** 106 | 107 | --- 108 | kind: pipeline 109 | type: vm 110 | name: manifest 111 | platform: 112 | os: linux 113 | arch: amd64 114 | pool: 115 | use: ubuntu 116 | 117 | steps: 118 | - name: manifest 119 | image: plugins/manifest 120 | settings: 121 | spec: docker/manifest.tmpl 122 | auto_tag: true 123 | ignore_missing: true 124 | password: 125 | from_secret: docker_password 126 | username: 127 | from_secret: docker_username 128 | 129 | depends_on: 130 | - linux-amd64 131 | - linux-arm64 132 | 133 | trigger: 134 | ref: 135 | - refs/heads/master 136 | - refs/tags/** -------------------------------------------------------------------------------- /.github/code_of_conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at conduct@drone.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drone-runners/drone-runner-ssh/ee70745c60e070a7fac57d9cecc41252e7a3ff55/.github/issue_template.md -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drone-runners/drone-runner-ssh/ee70745c60e070a7fac57d9cecc41252e7a3ff55/.github/pull_request_template.md -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policies and Procedures 2 | 3 | This document outlines security procedures and general policies for this project. 4 | 5 | * [Reporting a Bug](#reporting-a-bug) 6 | * [Disclosure Policy](#disclosure-policy) 7 | * [Comments on this Policy](#comments-on-this-policy) 8 | 9 | ## Reporting a Bug 10 | 11 | Report security bugs by emailing the lead maintainer at security@drone.io. 12 | 13 | The lead maintainer will acknowledge your email within 48 hours, and will send a 14 | more detailed response within 48 hours indicating the next steps in handling 15 | your report. After the initial reply to your report, the security team will 16 | endeavor to keep you informed of the progress towards a fix and full 17 | announcement, and may ask for additional information or guidance. 18 | 19 | Report security bugs in third-party packages to the person or team maintaining 20 | the module. 21 | 22 | ## Disclosure Policy 23 | 24 | When the security team receives a security bug report, they will assign it to a 25 | primary handler. This person will coordinate the fix and release process, 26 | involving the following steps: 27 | 28 | * Confirm the problem and determine the affected versions. 29 | * Audit code to find any potential similar problems. 30 | * Prepare fixes for all releases still under maintenance. These fixes will be 31 | released as fast as possible. 32 | 33 | ## Comments on this Policy 34 | 35 | If you have suggestions on how this process could be improved please submit a 36 | pull request. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | drone-runner-ssh 2 | drone-runner-ssh.exe 3 | release/* 4 | .env 5 | NOTES* 6 | .vscode/ 7 | drone 8 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | 1. Install go 1.12 or later 2 | 2. Test 3 | 4 | go test ./... 5 | 6 | 3. Build executables 7 | 8 | sh scripts/build_all.sh 9 | 10 | 4. Build images 11 | 12 | docker build -t drone/drone-runner-ssh:latest-linux-amd64 -f docker/Dockerfile.linux.amd64 . 13 | docker build -t drone/drone-runner-ssh:latest-linux-arm64 -f docker/Dockerfile.linux.arm64 . 14 | docker build -t drone/drone-runner-ssh:latest-linux-arm -f docker/Dockerfile.linux.arm . 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 2 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 3 | 4 | ## [Unreleased] 5 | ### Added 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | [Polyform-Small-Business-1.0.0](https://polyformproject.org/licenses/small-business/1.0.0) OR 2 | [Polyform-Free-Trial-1.0.0](https://polyformproject.org/licenses/free-trial/1.0.0) 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # drone-runner-ssh 2 | 3 | The ssh runner executes pipelines on a remote server using the ssh protocol. This runner is intended for workloads that are not suitable for running inside containers. Posix and Windows workloads supported. Drone server 1.2.1 or higher is required. 4 | 5 | ## Community and Support 6 | 7 | [Harness Community Slack](https://join.slack.com/t/harnesscommunity/shared_invite/zt-y4hdqh7p-RVuEQyIl5Hcx4Ck8VCvzBw) - Join the #drone slack channel to connect with our engineers and other users running Drone CI. 8 | 9 | [Harness Community Forum](https://community.harness.io/) - Ask questions, find answers, and help other users. 10 | 11 | [Report A Bug](https://community.harness.io/c/bugs/17) - Find a bug? Please report in our forum under Drone Bugs. Please provide screenshots and steps to reproduce. 12 | 13 | [Events](https://www.meetup.com/harness/) - Keep up to date with Drone events and check out previous events [here](https://www.youtube.com/watch?v=Oq34ImUGcHA&list=PLXsYHFsLmqf3zwelQDAKoVNmLeqcVsD9o). 14 | 15 | ## Developer resources 16 | 17 | ### Testing locally 18 | 19 | Running an ssh server locally is easy. 20 | 21 | ``` bash 22 | docker run -it --rm \ 23 | --name=openssh-server \ 24 | --hostname=openssh \ 25 | -e PUID=1000 \ 26 | -e PGID=1000 \ 27 | -e PASSWORD_ACCESS=true \ 28 | -e USER_PASSWORD=secret \ 29 | -e USER_NAME=user \ 30 | -p 2222:2222 \ 31 | lscr.io/linuxserver/openssh-server:latest 32 | ``` 33 | 34 | You can then reference the server in your pipeline. 35 | 36 | ``` yaml 37 | kind: pipeline 38 | type: ssh 39 | name: default 40 | 41 | server: 42 | host: 0.0.0.0:2222 43 | user: user 44 | password: secret 45 | 46 | clone: 47 | disable: true 48 | 49 | steps: 50 | - name: test 51 | commands: 52 | - echo "hello world" 53 | ``` 54 | -------------------------------------------------------------------------------- /command/command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package command 6 | 7 | import ( 8 | "context" 9 | "os" 10 | 11 | "github.com/drone-runners/drone-runner-ssh/command/daemon" 12 | 13 | "gopkg.in/alecthomas/kingpin.v2" 14 | ) 15 | 16 | // program version 17 | var version = "0.0.0" 18 | 19 | // empty context 20 | var nocontext = context.Background() 21 | 22 | // Command parses the command line arguments and then executes a 23 | // subcommand program. 24 | func Command() { 25 | app := kingpin.New("drone", "drone exec runner") 26 | registerCompile(app) 27 | registerExec(app) 28 | daemon.Register(app) 29 | 30 | kingpin.Version(version) 31 | kingpin.MustParse(app.Parse(os.Args[1:])) 32 | } 33 | -------------------------------------------------------------------------------- /command/compile.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package command 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "io/ioutil" 11 | "os" 12 | "strings" 13 | 14 | "github.com/drone-runners/drone-runner-ssh/command/internal" 15 | "github.com/drone-runners/drone-runner-ssh/engine/compiler" 16 | "github.com/drone-runners/drone-runner-ssh/engine/resource" 17 | "github.com/drone/envsubst" 18 | "github.com/drone/runner-go/environ" 19 | "github.com/drone/runner-go/manifest" 20 | "github.com/drone/runner-go/secret" 21 | 22 | "gopkg.in/alecthomas/kingpin.v2" 23 | ) 24 | 25 | type compileCommand struct { 26 | *internal.Flags 27 | 28 | Source *os.File 29 | Environ map[string]string 30 | Secrets map[string]string 31 | } 32 | 33 | func (c *compileCommand) run(*kingpin.ParseContext) error { 34 | rawsource, err := ioutil.ReadAll(c.Source) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | envs := environ.Combine( 40 | c.Environ, 41 | environ.System(c.System), 42 | environ.Repo(c.Repo), 43 | environ.Build(c.Build), 44 | environ.Stage(c.Stage), 45 | environ.Link(c.Repo, c.Build, c.System), 46 | c.Build.Params, 47 | ) 48 | 49 | // string substitution function ensures that string 50 | // replacement variables are escaped and quoted if they 51 | // contain newlines. 52 | subf := func(k string) string { 53 | v := envs[k] 54 | if strings.Contains(v, "\n") { 55 | v = fmt.Sprintf("%q", v) 56 | } 57 | return v 58 | } 59 | 60 | // evaluates string replacement expressions and returns an 61 | // update configuration. 62 | config, err := envsubst.Eval(string(rawsource), subf) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | // parse and lint the configuration 68 | manifest, err := manifest.ParseString(config) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | // a configuration can contain multiple pipelines. 74 | // get a specific pipeline resource for execution. 75 | resource, err := resource.Lookup(c.Stage.Name, manifest) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | // compile the pipeline to an intermediate representation. 81 | comp := &compiler.Compiler{ 82 | Pipeline: resource, 83 | Manifest: manifest, 84 | Build: c.Build, 85 | Netrc: c.Netrc, 86 | Repo: c.Repo, 87 | Stage: c.Stage, 88 | System: c.System, 89 | Environ: c.Environ, 90 | Secret: secret.StaticVars(c.Secrets), 91 | } 92 | spec := comp.Compile(nocontext) 93 | 94 | // encode the pipeline in json format and print to the 95 | // console for inspection. 96 | enc := json.NewEncoder(os.Stdout) 97 | enc.SetIndent("", " ") 98 | enc.Encode(spec) 99 | return nil 100 | } 101 | 102 | func registerCompile(app *kingpin.Application) { 103 | c := new(compileCommand) 104 | c.Environ = map[string]string{} 105 | c.Secrets = map[string]string{} 106 | 107 | cmd := app.Command("compile", "compile the yaml file"). 108 | Action(c.run) 109 | 110 | cmd.Flag("source", "source file location"). 111 | Default(".drone.yml"). 112 | FileVar(&c.Source) 113 | 114 | cmd.Flag("secrets", "secret parameters"). 115 | StringMapVar(&c.Secrets) 116 | 117 | cmd.Flag("environ", "environment variables"). 118 | StringMapVar(&c.Environ) 119 | 120 | // shared pipeline flags 121 | c.Flags = internal.ParseFlags(cmd) 122 | } 123 | -------------------------------------------------------------------------------- /command/daemon/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package daemon 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | 11 | "github.com/kelseyhightower/envconfig" 12 | ) 13 | 14 | // Config stores the system configuration. 15 | type Config struct { 16 | Debug bool `envconfig:"DRONE_DEBUG"` 17 | Trace bool `envconfig:"DRONE_TRACE"` 18 | 19 | Logger struct { 20 | File string `envconfig:"DRONE_LOG_FILE"` 21 | MaxAge int `envconfig:"DRONE_LOG_FILE_MAX_AGE" default:"1"` 22 | MaxBackups int `envconfig:"DRONE_LOG_FILE_MAX_BACKUPS" default:"1"` 23 | MaxSize int `envconfig:"DRONE_LOG_FILE_MAX_SIZE" default:"100"` 24 | } 25 | 26 | Client struct { 27 | Address string `ignored:"true"` 28 | Proto string `envconfig:"DRONE_RPC_PROTO" default:"http"` 29 | Host string `envconfig:"DRONE_RPC_HOST" required:"true"` 30 | Secret string `envconfig:"DRONE_RPC_SECRET" required:"true"` 31 | SkipVerify bool `envconfig:"DRONE_RPC_SKIP_VERIFY"` 32 | Dump bool `envconfig:"DRONE_RPC_DUMP_HTTP"` 33 | DumpBody bool `envconfig:"DRONE_RPC_DUMP_HTTP_BODY"` 34 | } 35 | 36 | Dashboard struct { 37 | Disabled bool `envconfig:"DRONE_UI_DISABLE"` 38 | Username string `envconfig:"DRONE_UI_USERNAME"` 39 | Password string `envconfig:"DRONE_UI_PASSWORD"` 40 | Realm string `envconfig:"DRONE_UI_REALM" default:"MyRealm"` 41 | } 42 | 43 | Server struct { 44 | Proto string `envconfig:"DRONE_SERVER_PROTO"` 45 | Host string `envconfig:"DRONE_SERVER_HOST"` 46 | Port string `envconfig:"DRONE_SERVER_PORT" default:":3000"` 47 | Acme bool `envconfig:"DRONE_SERVER_ACME"` 48 | } 49 | 50 | Runner struct { 51 | Name string `envconfig:"DRONE_RUNNER_NAME"` 52 | Capacity int `envconfig:"DRONE_RUNNER_CAPACITY" default:"10"` 53 | Procs int64 `envconfig:"DRONE_RUNNER_MAX_PROCS"` 54 | Labels map[string]string `envconfig:"DRONE_RUNNER_LABELS"` 55 | Environ map[string]string `envconfig:"DRONE_RUNNER_ENVIRON"` 56 | } 57 | 58 | Limit struct { 59 | Repos []string `envconfig:"DRONE_LIMIT_REPOS"` 60 | Events []string `envconfig:"DRONE_LIMIT_EVENTS"` 61 | Trusted bool `envconfig:"DRONE_LIMIT_TRUSTED"` 62 | } 63 | 64 | Secret struct { 65 | Endpoint string `envconfig:"DRONE_SECRET_PLUGIN_ENDPOINT"` 66 | Token string `envconfig:"DRONE_SECRET_PLUGIN_TOKEN"` 67 | SkipVerify bool `envconfig:"DRONE_SECRET_PLUGIN_SKIP_VERIFY"` 68 | } 69 | } 70 | 71 | func fromEnviron() (Config, error) { 72 | var config Config 73 | err := envconfig.Process("", &config) 74 | if err != nil { 75 | return config, err 76 | } 77 | if config.Runner.Name == "" { 78 | config.Runner.Name, _ = os.Hostname() 79 | } 80 | if config.Dashboard.Password == "" { 81 | config.Dashboard.Disabled = true 82 | } 83 | config.Client.Address = fmt.Sprintf( 84 | "%s://%s", 85 | config.Client.Proto, 86 | config.Client.Host, 87 | ) 88 | return config, nil 89 | } 90 | -------------------------------------------------------------------------------- /command/daemon/daemon.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package daemon 6 | 7 | import ( 8 | "context" 9 | "time" 10 | 11 | "github.com/drone-runners/drone-runner-ssh/engine" 12 | "github.com/drone-runners/drone-runner-ssh/engine/resource" 13 | "github.com/drone-runners/drone-runner-ssh/internal/match" 14 | "github.com/drone-runners/drone-runner-ssh/runtime" 15 | 16 | "github.com/drone/runner-go/client" 17 | "github.com/drone/runner-go/handler/router" 18 | "github.com/drone/runner-go/logger" 19 | loghistory "github.com/drone/runner-go/logger/history" 20 | "github.com/drone/runner-go/pipeline/history" 21 | "github.com/drone/runner-go/pipeline/remote" 22 | "github.com/drone/runner-go/secret" 23 | "github.com/drone/runner-go/server" 24 | "github.com/drone/signal" 25 | 26 | "github.com/joho/godotenv" 27 | "github.com/sirupsen/logrus" 28 | "golang.org/x/sync/errgroup" 29 | "gopkg.in/alecthomas/kingpin.v2" 30 | ) 31 | 32 | // empty context. 33 | var nocontext = context.Background() 34 | 35 | type daemonCommand struct { 36 | envfile string 37 | } 38 | 39 | func (c *daemonCommand) run(*kingpin.ParseContext) error { 40 | // load environment variables from file. 41 | godotenv.Load(c.envfile) 42 | 43 | // load the configuration from the environment 44 | config, err := fromEnviron() 45 | if err != nil { 46 | return err 47 | } 48 | 49 | // setup the global logrus logger. 50 | setupLogger(config) 51 | 52 | cli := client.New( 53 | config.Client.Address, 54 | config.Client.Secret, 55 | config.Client.SkipVerify, 56 | ) 57 | if config.Client.Dump { 58 | cli.Dumper = logger.StandardDumper( 59 | config.Client.DumpBody, 60 | ) 61 | } 62 | cli.Logger = logger.Logrus( 63 | logrus.NewEntry( 64 | logrus.StandardLogger(), 65 | ), 66 | ) 67 | 68 | engine := engine.New() 69 | remote := remote.New(cli) 70 | tracer := history.New(remote) 71 | hook := loghistory.New() 72 | logrus.AddHook(hook) 73 | 74 | poller := &runtime.Poller{ 75 | Client: cli, 76 | Runner: &runtime.Runner{ 77 | Client: cli, 78 | Environ: config.Runner.Environ, 79 | Machine: config.Runner.Name, 80 | Reporter: tracer, 81 | Match: match.Func( 82 | config.Limit.Repos, 83 | config.Limit.Events, 84 | config.Limit.Trusted, 85 | ), 86 | Secret: secret.External( 87 | config.Secret.Endpoint, 88 | config.Secret.Token, 89 | config.Secret.SkipVerify, 90 | ), 91 | Execer: runtime.NewExecer( 92 | tracer, 93 | remote, 94 | engine, 95 | config.Runner.Procs, 96 | ), 97 | }, 98 | Filter: &client.Filter{ 99 | Kind: resource.Kind, 100 | Type: resource.Type, 101 | Labels: config.Runner.Labels, 102 | }, 103 | } 104 | 105 | ctx, cancel := context.WithCancel(nocontext) 106 | defer cancel() 107 | 108 | // listen for termination signals to gracefully shutdown 109 | // the runner daemon. 110 | ctx = signal.WithContextFunc(ctx, func() { 111 | println("received signal, terminating process") 112 | cancel() 113 | }) 114 | 115 | var g errgroup.Group 116 | server := server.Server{ 117 | Addr: config.Server.Port, 118 | Handler: router.New(tracer, hook, router.Config{ 119 | Username: config.Dashboard.Username, 120 | Password: config.Dashboard.Password, 121 | Realm: config.Dashboard.Realm, 122 | }), 123 | } 124 | 125 | logrus.WithField("addr", config.Server.Port). 126 | Infoln("starting the server") 127 | 128 | g.Go(func() error { 129 | return server.ListenAndServe(ctx) 130 | }) 131 | 132 | // Ping the server and block until a successful connection 133 | // to the server has been established. 134 | for { 135 | err := cli.Ping(ctx, config.Runner.Name) 136 | select { 137 | case <-ctx.Done(): 138 | return nil 139 | default: 140 | } 141 | if ctx.Err() != nil { 142 | break 143 | } 144 | if err != nil { 145 | logrus.WithError(err). 146 | Errorln("cannot ping the remote server") 147 | time.Sleep(time.Second) 148 | } else { 149 | logrus.Infoln("successfully pinged the remote server") 150 | break 151 | } 152 | } 153 | 154 | g.Go(func() error { 155 | logrus.WithField("capacity", config.Runner.Capacity). 156 | WithField("endpoint", config.Client.Address). 157 | WithField("kind", resource.Kind). 158 | WithField("type", resource.Type). 159 | Infoln("polling the remote server") 160 | 161 | poller.Poll(ctx, config.Runner.Capacity) 162 | return nil 163 | }) 164 | 165 | err = g.Wait() 166 | if err != nil { 167 | logrus.WithError(err). 168 | Errorln("shutting down the server") 169 | } 170 | return err 171 | } 172 | 173 | // helper function configures the global logger from 174 | // the loaded configuration. 175 | func setupLogger(config Config) { 176 | logger.Default = logger.Logrus( 177 | logrus.NewEntry( 178 | logrus.StandardLogger(), 179 | ), 180 | ) 181 | if config.Debug { 182 | logrus.SetLevel(logrus.DebugLevel) 183 | } 184 | if config.Trace { 185 | logrus.SetLevel(logrus.TraceLevel) 186 | } 187 | } 188 | 189 | // Register the daemon command. 190 | func Register(app *kingpin.Application) { 191 | c := new(daemonCommand) 192 | 193 | cmd := app.Command("daemon", "starts the runner daemon"). 194 | Default(). 195 | Action(c.run) 196 | 197 | cmd.Arg("envfile", "load the environment variable file"). 198 | Default(""). 199 | StringVar(&c.envfile) 200 | } 201 | -------------------------------------------------------------------------------- /command/exec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package command 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | "io/ioutil" 12 | "os" 13 | "strings" 14 | "time" 15 | 16 | "github.com/drone-runners/drone-runner-ssh/command/internal" 17 | "github.com/drone-runners/drone-runner-ssh/engine" 18 | "github.com/drone-runners/drone-runner-ssh/engine/compiler" 19 | "github.com/drone-runners/drone-runner-ssh/engine/resource" 20 | "github.com/drone-runners/drone-runner-ssh/runtime" 21 | "github.com/drone/drone-go/drone" 22 | "github.com/drone/envsubst" 23 | "github.com/drone/runner-go/environ" 24 | "github.com/drone/runner-go/logger" 25 | "github.com/drone/runner-go/manifest" 26 | "github.com/drone/runner-go/pipeline" 27 | "github.com/drone/runner-go/pipeline/console" 28 | "github.com/drone/runner-go/secret" 29 | "github.com/drone/signal" 30 | 31 | "github.com/mattn/go-isatty" 32 | "github.com/sirupsen/logrus" 33 | "gopkg.in/alecthomas/kingpin.v2" 34 | ) 35 | 36 | type execCommand struct { 37 | *internal.Flags 38 | 39 | Source *os.File 40 | Environ map[string]string 41 | Secrets map[string]string 42 | Dump bool 43 | Debug bool 44 | Trace bool 45 | Pretty bool 46 | Procs int64 47 | } 48 | 49 | func (c *execCommand) run(*kingpin.ParseContext) error { 50 | rawsource, err := ioutil.ReadAll(c.Source) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | envs := environ.Combine( 56 | c.Environ, 57 | environ.System(c.System), 58 | environ.Repo(c.Repo), 59 | environ.Build(c.Build), 60 | environ.Stage(c.Stage), 61 | environ.Link(c.Repo, c.Build, c.System), 62 | c.Build.Params, 63 | ) 64 | 65 | // string substitution function ensures that string 66 | // replacement variables are escaped and quoted if they 67 | // contain newlines. 68 | subf := func(k string) string { 69 | v := envs[k] 70 | if strings.Contains(v, "\n") { 71 | v = fmt.Sprintf("%q", v) 72 | } 73 | return v 74 | } 75 | 76 | // evaluates string replacement expressions and returns an 77 | // update configuration. 78 | config, err := envsubst.Eval(string(rawsource), subf) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | // parse and lint the configuration. 84 | manifest, err := manifest.ParseString(config) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | // a configuration can contain multiple pipelines. 90 | // get a specific pipeline resource for execution. 91 | resource, err := resource.Lookup(c.Stage.Name, manifest) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | // compile the pipeline to an intermediate representation. 97 | comp := &compiler.Compiler{ 98 | Pipeline: resource, 99 | Manifest: manifest, 100 | Build: c.Build, 101 | Netrc: c.Netrc, 102 | Repo: c.Repo, 103 | Stage: c.Stage, 104 | System: c.System, 105 | Environ: c.Environ, 106 | Secret: secret.StaticVars(c.Secrets), 107 | } 108 | spec := comp.Compile(nocontext) 109 | 110 | // create a step object for each pipeline step. 111 | for _, step := range spec.Steps { 112 | if step.RunPolicy == engine.RunNever { 113 | continue 114 | } 115 | c.Stage.Steps = append(c.Stage.Steps, &drone.Step{ 116 | StageID: c.Stage.ID, 117 | Number: len(c.Stage.Steps) + 1, 118 | Name: step.Name, 119 | Status: drone.StatusPending, 120 | ErrIgnore: step.IgnoreErr, 121 | }) 122 | } 123 | 124 | // configures the pipeline timeout. 125 | timeout := time.Duration(c.Repo.Timeout) * time.Minute 126 | ctx, cancel := context.WithTimeout(nocontext, timeout) 127 | defer cancel() 128 | 129 | // listen for operating system signals and cancel execution 130 | // when received. 131 | ctx = signal.WithContextFunc(ctx, func() { 132 | println("received signal, terminating process") 133 | cancel() 134 | }) 135 | 136 | state := &pipeline.State{ 137 | Build: c.Build, 138 | Stage: c.Stage, 139 | Repo: c.Repo, 140 | System: c.System, 141 | } 142 | 143 | // enable debug logging 144 | logrus.SetLevel(logrus.WarnLevel) 145 | if c.Debug { 146 | logrus.SetLevel(logrus.DebugLevel) 147 | } 148 | if c.Trace { 149 | logrus.SetLevel(logrus.TraceLevel) 150 | } 151 | logger.Default = logger.Logrus( 152 | logrus.NewEntry( 153 | logrus.StandardLogger(), 154 | ), 155 | ) 156 | 157 | err = runtime.NewExecer( 158 | pipeline.NopReporter(), 159 | console.New(c.Pretty), 160 | engine.New(), 161 | c.Procs, 162 | ).Exec(ctx, spec, state) 163 | if c.Dump { 164 | dump(state) 165 | } 166 | if err != nil { 167 | return err 168 | } 169 | switch state.Stage.Status { 170 | case drone.StatusError, drone.StatusFailing, drone.StatusKilled: 171 | os.Exit(1) 172 | } 173 | return nil 174 | } 175 | 176 | func dump(v interface{}) { 177 | enc := json.NewEncoder(os.Stdout) 178 | enc.SetIndent("", " ") 179 | enc.Encode(v) 180 | } 181 | 182 | func registerExec(app *kingpin.Application) { 183 | c := new(execCommand) 184 | c.Environ = map[string]string{} 185 | c.Secrets = map[string]string{} 186 | 187 | cmd := app.Command("exec", "executes a pipeline"). 188 | Action(c.run) 189 | 190 | cmd.Arg("source", "source file location"). 191 | Default(".drone.yml"). 192 | FileVar(&c.Source) 193 | 194 | cmd.Flag("secrets", "secret parameters"). 195 | StringMapVar(&c.Secrets) 196 | 197 | cmd.Flag("environ", "environment variables"). 198 | StringMapVar(&c.Environ) 199 | 200 | cmd.Flag("debug", "enable debug level logging"). 201 | BoolVar(&c.Debug) 202 | 203 | cmd.Flag("trace", "enable trace level logging"). 204 | BoolVar(&c.Trace) 205 | 206 | cmd.Flag("dump", "dump the pipeline state"). 207 | BoolVar(&c.Dump) 208 | 209 | cmd.Flag("pretty", "pretty print the output"). 210 | Default( 211 | fmt.Sprint( 212 | isatty.IsTerminal( 213 | os.Stdout.Fd(), 214 | ), 215 | ), 216 | ).BoolVar(&c.Pretty) 217 | 218 | // shared pipeline flags 219 | c.Flags = internal.ParseFlags(cmd) 220 | } 221 | -------------------------------------------------------------------------------- /command/internal/flags.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package internal 6 | 7 | import ( 8 | "fmt" 9 | "time" 10 | 11 | "github.com/drone/drone-go/drone" 12 | 13 | "gopkg.in/alecthomas/kingpin.v2" 14 | ) 15 | 16 | // Flags maps 17 | type Flags struct { 18 | Build *drone.Build 19 | Netrc *drone.Netrc 20 | Repo *drone.Repo 21 | Stage *drone.Stage 22 | System *drone.System 23 | } 24 | 25 | // ParseFlags parses the flags from the command args. 26 | func ParseFlags(cmd *kingpin.CmdClause) *Flags { 27 | f := &Flags{ 28 | Build: &drone.Build{}, 29 | Netrc: &drone.Netrc{}, 30 | Repo: &drone.Repo{}, 31 | Stage: &drone.Stage{}, 32 | System: &drone.System{}, 33 | } 34 | 35 | now := fmt.Sprint( 36 | time.Now().Unix(), 37 | ) 38 | 39 | cmd.Flag("repo-id", "repo id").Default("1").Int64Var(&f.Repo.ID) 40 | cmd.Flag("repo-namespace", "repo namespace").Default("").StringVar(&f.Repo.Namespace) 41 | cmd.Flag("repo-name", "repo name").Default("").StringVar(&f.Repo.Name) 42 | cmd.Flag("repo-slug", "repo slug").Default("").StringVar(&f.Repo.Slug) 43 | cmd.Flag("repo-http", "repo http clone url").Default("").StringVar(&f.Repo.HTTPURL) 44 | cmd.Flag("repo-ssh", "repo ssh clone url").Default("").StringVar(&f.Repo.SSHURL) 45 | cmd.Flag("repo-link", "repo link").Default("").StringVar(&f.Repo.Link) 46 | cmd.Flag("repo-branch", "repo branch").Default("").StringVar(&f.Repo.Branch) 47 | cmd.Flag("repo-private", "repo private").Default("false").BoolVar(&f.Repo.Private) 48 | cmd.Flag("repo-visibility", "repo visibility").Default("").StringVar(&f.Repo.Visibility) 49 | cmd.Flag("repo-trusted", "repo trusted").Default("false").BoolVar(&f.Repo.Trusted) 50 | cmd.Flag("repo-protected", "repo protected").Default("false").BoolVar(&f.Repo.Protected) 51 | cmd.Flag("repo-timeout", "repo timeout in minutes").Default("60").Int64Var(&f.Repo.Timeout) 52 | cmd.Flag("repo-created", "repo created").Default(now).Int64Var(&f.Repo.Created) 53 | cmd.Flag("repo-updated", "repo updated").Default(now).Int64Var(&f.Repo.Updated) 54 | 55 | cmd.Flag("build-id", "build id").Default("1").Int64Var(&f.Build.ID) 56 | cmd.Flag("build-number", "build number").Default("1").Int64Var(&f.Build.Number) 57 | cmd.Flag("build-parent", "build parent").Default("0").Int64Var(&f.Build.Parent) 58 | cmd.Flag("build-event", "build event").Default("push").StringVar(&f.Build.Event) 59 | cmd.Flag("build-action", "build action").Default("").StringVar(&f.Build.Action) 60 | cmd.Flag("build-cron", "build cron trigger").Default("").StringVar(&f.Build.Cron) 61 | cmd.Flag("build-target", "build deploy target").Default("").StringVar(&f.Build.Deploy) 62 | cmd.Flag("build-created", "build created").Default(now).Int64Var(&f.Build.Created) 63 | cmd.Flag("build-updated", "build updated").Default(now).Int64Var(&f.Build.Updated) 64 | 65 | cmd.Flag("commit-sender", "commit sender").Default("").StringVar(&f.Build.Sender) 66 | cmd.Flag("commit-link", "commit link").Default("").StringVar(&f.Build.Link) 67 | cmd.Flag("commit-title", "commit title").Default("").StringVar(&f.Build.Title) 68 | cmd.Flag("commit-message", "commit message").Default("").StringVar(&f.Build.Message) 69 | cmd.Flag("commit-before", "commit before").Default("").StringVar(&f.Build.Before) 70 | cmd.Flag("commit-after", "commit after").Default("").StringVar(&f.Build.After) 71 | cmd.Flag("commit-ref", "commit ref").Default("").StringVar(&f.Build.Ref) 72 | cmd.Flag("commit-fork", "commit fork").Default("").StringVar(&f.Build.Fork) 73 | cmd.Flag("commit-source", "commit source branch").Default("").StringVar(&f.Build.Source) 74 | cmd.Flag("commit-target", "commit target branch").Default("").StringVar(&f.Build.Target) 75 | 76 | cmd.Flag("author-login", "commit author login").Default("").StringVar(&f.Build.Author) 77 | cmd.Flag("author-name", "commit author name").Default("").StringVar(&f.Build.AuthorName) 78 | cmd.Flag("author-email", "commit author email").Default("").StringVar(&f.Build.AuthorEmail) 79 | cmd.Flag("author-avatar", "commit author avatar").Default("").StringVar(&f.Build.AuthorAvatar) 80 | 81 | cmd.Flag("stage-id", "stage id").Default("1").Int64Var(&f.Stage.ID) 82 | cmd.Flag("stage-number", "stage number").Default("1").IntVar(&f.Stage.Number) 83 | cmd.Flag("stage-kind", "stage kind").Default("").StringVar(&f.Stage.Kind) 84 | cmd.Flag("stage-type", "stage type").Default("").StringVar(&f.Stage.Type) 85 | cmd.Flag("stage-name", "stage name").Default("default").StringVar(&f.Stage.Name) 86 | cmd.Flag("stage-os", "stage os").Default("").StringVar(&f.Stage.OS) 87 | cmd.Flag("stage-arch", "stage arch").Default("").StringVar(&f.Stage.Arch) 88 | cmd.Flag("stage-variant", "stage variant").Default("").StringVar(&f.Stage.Variant) 89 | cmd.Flag("stage-kernel", "stage kernel").Default("").StringVar(&f.Stage.Kernel) 90 | cmd.Flag("stage-created", "stage created").Default(now).Int64Var(&f.Stage.Created) 91 | cmd.Flag("stage-updated", "stage updated").Default(now).Int64Var(&f.Stage.Updated) 92 | 93 | cmd.Flag("netrc-username", "netrc username").Default("").StringVar(&f.Netrc.Login) 94 | cmd.Flag("netrc-password", "netrc password").Default("").StringVar(&f.Netrc.Password) 95 | cmd.Flag("netrc-machine", "netrc machine").Default("").StringVar(&f.Netrc.Machine) 96 | 97 | cmd.Flag("system-host", "server host").Default("").StringVar(&f.System.Host) 98 | cmd.Flag("system-proto", "server proto").Default("").StringVar(&f.System.Proto) 99 | cmd.Flag("system-link", "server link").Default("").StringVar(&f.System.Link) 100 | cmd.Flag("system-version", "server version").Default("").StringVar(&f.System.Version) 101 | 102 | return f 103 | } 104 | -------------------------------------------------------------------------------- /docker/Dockerfile.linux.amd64: -------------------------------------------------------------------------------- 1 | FROM alpine:3.6 as alpine 2 | RUN apk add -U --no-cache ca-certificates 3 | 4 | FROM alpine:3.6 5 | EXPOSE 3000 6 | 7 | ENV GODEBUG netdns=go 8 | 9 | COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 10 | 11 | ADD release/linux/amd64/drone-runner-ssh /bin/ 12 | ENTRYPOINT ["/bin/drone-runner-ssh"] 13 | -------------------------------------------------------------------------------- /docker/Dockerfile.linux.arm64: -------------------------------------------------------------------------------- 1 | FROM alpine:3.6 as alpine 2 | RUN apk add -U --no-cache ca-certificates 3 | 4 | FROM alpine:3.6 5 | EXPOSE 3000 6 | 7 | ENV GODEBUG netdns=go 8 | 9 | COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 10 | 11 | ADD release/linux/arm64/drone-runner-ssh /bin/ 12 | ENTRYPOINT ["/bin/drone-runner-ssh"] 13 | -------------------------------------------------------------------------------- /docker/manifest.tmpl: -------------------------------------------------------------------------------- 1 | image: drone/drone-runner-ssh:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} 2 | {{#if build.tags}} 3 | tags: 4 | {{#each build.tags}} 5 | - {{this}} 6 | {{/each}} 7 | {{/if}} 8 | manifests: 9 | - 10 | image: drone/drone-runner-ssh:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 11 | platform: 12 | architecture: amd64 13 | os: linux 14 | - 15 | image: drone/drone-runner-ssh:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64 16 | platform: 17 | variant: v8 18 | architecture: arm64 19 | os: linux 20 | -------------------------------------------------------------------------------- /engine/compiler/compiler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package compiler 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "strings" 11 | 12 | "github.com/drone-runners/drone-runner-ssh/engine" 13 | "github.com/drone-runners/drone-runner-ssh/engine/resource" 14 | 15 | "github.com/drone/drone-go/drone" 16 | "github.com/drone/runner-go/clone" 17 | "github.com/drone/runner-go/environ" 18 | "github.com/drone/runner-go/manifest" 19 | "github.com/drone/runner-go/secret" 20 | 21 | "github.com/dchest/uniuri" 22 | "github.com/gosimple/slug" 23 | ) 24 | 25 | // random generator function 26 | var random = uniuri.New 27 | 28 | // Compiler compiles the Yaml configuration file to an 29 | // intermediate representation optimized for simple execution. 30 | type Compiler struct { 31 | // Manifest provides the parsed manifest. 32 | Manifest *manifest.Manifest 33 | 34 | // Pipeline provides the parsed pipeline. This pipeline is 35 | // the compiler source and is converted to the intermediate 36 | // representation by the Compile method. 37 | Pipeline *resource.Pipeline 38 | 39 | // Build provides the compiler with stage information that 40 | // is converted to environment variable format and passed to 41 | // each pipeline step. It is also used to clone the commit. 42 | Build *drone.Build 43 | 44 | // Stage provides the compiler with stage information that 45 | // is converted to environment variable format and passed to 46 | // each pipeline step. 47 | Stage *drone.Stage 48 | 49 | // Repo provides the compiler with repo information. This 50 | // repo information is converted to environment variable 51 | // format and passed to each pipeline step. It is also used 52 | // to clone the repository. 53 | Repo *drone.Repo 54 | 55 | // System provides the compiler with system information that 56 | // is converted to environment variable format and passed to 57 | // each pipeline step. 58 | System *drone.System 59 | 60 | // Environ provides a set of environment varaibles that 61 | // should be added to each pipeline step by default. 62 | Environ map[string]string 63 | 64 | // Netrc provides netrc parameters that can be used by the 65 | // default clone step to authenticate to the remote 66 | // repository. 67 | Netrc *drone.Netrc 68 | 69 | // Secret returns a named secret value that can be injected 70 | // into the pipeline step. 71 | Secret secret.Provider 72 | } 73 | 74 | // Compile compiles the configuration file. 75 | func (c *Compiler) Compile(ctx context.Context) *engine.Spec { 76 | os := c.Pipeline.Platform.OS 77 | 78 | spec := &engine.Spec{ 79 | Platform: engine.Platform{ 80 | OS: c.Pipeline.Platform.OS, 81 | Arch: c.Pipeline.Platform.Arch, 82 | Variant: c.Pipeline.Platform.Variant, 83 | Version: c.Pipeline.Platform.Version, 84 | }, 85 | Server: engine.Server{ 86 | Hostname: c.Pipeline.Server.Host.Value, 87 | Username: c.Pipeline.Server.User.Value, 88 | Password: c.Pipeline.Server.Password.Value, 89 | SSHKey: c.Pipeline.Server.SSHKey.Value, 90 | }, 91 | } 92 | 93 | // maybe load the server host variable from secret 94 | if s, ok := c.findSecret(ctx, c.Pipeline.Server.Host.Secret); ok { 95 | spec.Server.Hostname = s 96 | } 97 | // maybe load the server username variable from secret 98 | if s, ok := c.findSecret(ctx, c.Pipeline.Server.User.Secret); ok { 99 | spec.Server.Username = s 100 | } 101 | // maybe load the server password variable from secret 102 | if s, ok := c.findSecret(ctx, c.Pipeline.Server.Password.Secret); ok { 103 | spec.Server.Password = s 104 | } 105 | // maybe load the server ssh_key variable from secret 106 | if s, ok := c.findSecret(ctx, c.Pipeline.Server.SSHKey.Secret); ok { 107 | spec.Server.SSHKey = s 108 | } 109 | 110 | // append the port to the hostname if not exists 111 | if !strings.Contains(spec.Server.Hostname, ":") { 112 | spec.Server.Hostname = spec.Server.Hostname + ":22" 113 | } 114 | 115 | // create the root directory 116 | spec.Root = tempdir(os) 117 | 118 | // creates a home directory in the root. 119 | // note: mkdirall fails on windows so we need to create all 120 | // directories in the tree. 121 | homedir := join(os, spec.Root, "home", "drone") 122 | spec.Files = append(spec.Files, &engine.File{ 123 | Path: join(os, spec.Root, "home"), 124 | Mode: 0700, 125 | IsDir: true, 126 | }) 127 | spec.Files = append(spec.Files, &engine.File{ 128 | Path: homedir, 129 | Mode: 0700, 130 | IsDir: true, 131 | }) 132 | 133 | // creates a source directory in the root. 134 | // note: mkdirall fails on windows so we need to create all 135 | // directories in the tree. 136 | sourcedir := join(os, spec.Root, "drone", "src") 137 | spec.Files = append(spec.Files, &engine.File{ 138 | Path: join(os, spec.Root, "drone"), 139 | Mode: 0700, 140 | IsDir: true, 141 | }) 142 | spec.Files = append(spec.Files, &engine.File{ 143 | Path: sourcedir, 144 | Mode: 0700, 145 | IsDir: true, 146 | }) 147 | 148 | // creates the opt directory to hold all scripts. 149 | spec.Files = append(spec.Files, &engine.File{ 150 | Path: join(os, spec.Root, "opt"), 151 | Mode: 0700, 152 | IsDir: true, 153 | }) 154 | 155 | // creates the netrc file 156 | if c.Netrc != nil && c.Netrc.Password != "" { 157 | netrcfile := getNetrc(os) 158 | netrcpath := join(os, homedir, netrcfile) 159 | netrcdata := fmt.Sprintf( 160 | "machine %s login %s password %s", 161 | c.Netrc.Machine, 162 | c.Netrc.Login, 163 | c.Netrc.Password, 164 | ) 165 | spec.Files = append(spec.Files, &engine.File{ 166 | Path: netrcpath, 167 | Mode: 0600, 168 | Data: []byte(netrcdata), 169 | }) 170 | } 171 | 172 | // create the default environment variables. 173 | envs := environ.Combine( 174 | c.Environ, 175 | c.Build.Params, 176 | environ.Proxy(), 177 | environ.System(c.System), 178 | environ.Repo(c.Repo), 179 | environ.Build(c.Build), 180 | environ.Stage(c.Stage), 181 | environ.Link(c.Repo, c.Build, c.System), 182 | clone.Environ(clone.Config{ 183 | SkipVerify: c.Pipeline.Clone.SkipVerify, 184 | Trace: c.Pipeline.Clone.Trace, 185 | User: clone.User{ 186 | Name: c.Build.AuthorName, 187 | Email: c.Build.AuthorEmail, 188 | }, 189 | }), 190 | // TODO(bradrydzewski) windows variable HOMEDRIVE 191 | // TODO(bradrydzewski) windows variable LOCALAPPDATA 192 | map[string]string{ 193 | "HOME": homedir, 194 | "HOMEPATH": homedir, // for windows 195 | "USERPROFILE": homedir, // for windows 196 | "DRONE_HOME": sourcedir, 197 | "DRONE_WORKSPACE": sourcedir, 198 | "GIT_TERMINAL_PROMPT": "0", 199 | }, 200 | ) 201 | 202 | // create clone step, maybe 203 | if c.Pipeline.Clone.Disable == false { 204 | clonepath := join(os, spec.Root, "opt", getExt(os, "clone")) 205 | clonefile := genScript(os, 206 | clone.Commands( 207 | clone.Args{ 208 | Branch: c.Build.Target, 209 | Commit: c.Build.After, 210 | Ref: c.Build.Ref, 211 | Remote: c.Repo.HTTPURL, 212 | Depth: c.Pipeline.Clone.Depth, 213 | }, 214 | ), 215 | ) 216 | 217 | cmd, args := getCommand(os, clonepath) 218 | spec.Steps = append(spec.Steps, &engine.Step{ 219 | Name: "clone", 220 | Args: args, 221 | Command: cmd, 222 | Envs: envs, 223 | RunPolicy: engine.RunAlways, 224 | Files: []*engine.File{ 225 | { 226 | Path: clonepath, 227 | Mode: 0700, 228 | Data: []byte(clonefile), 229 | }, 230 | }, 231 | Secrets: []*engine.Secret{}, 232 | WorkingDir: sourcedir, 233 | }) 234 | } 235 | 236 | // create steps 237 | for _, src := range c.Pipeline.Steps { 238 | buildslug := slug.Make(src.Name) 239 | buildpath := join(os, spec.Root, "opt", getExt(os, buildslug)) 240 | buildfile := genScript(os, src.Commands) 241 | 242 | cmd, args := getCommand(os, buildpath) 243 | dst := &engine.Step{ 244 | Name: src.Name, 245 | Args: args, 246 | Command: cmd, 247 | Detach: src.Detach, 248 | DependsOn: src.DependsOn, 249 | Envs: environ.Combine(envs, 250 | environ.Expand( 251 | convertStaticEnv(src.Environment), 252 | ), 253 | ), 254 | IgnoreErr: strings.EqualFold(src.Failure, "ignore"), 255 | IgnoreStdout: false, 256 | IgnoreStderr: false, 257 | RunPolicy: engine.RunOnSuccess, 258 | Files: []*engine.File{ 259 | { 260 | Path: buildpath, 261 | Mode: 0700, 262 | Data: []byte(buildfile), 263 | }, 264 | }, 265 | Secrets: convertSecretEnv(src.Environment), 266 | WorkingDir: sourcedir, 267 | } 268 | spec.Steps = append(spec.Steps, dst) 269 | 270 | // set the pipeline step run policy. steps run on 271 | // success by default, but may be optionally configured 272 | // to run on failure. 273 | if isRunAlways(src) { 274 | dst.RunPolicy = engine.RunAlways 275 | } else if isRunOnFailure(src) { 276 | dst.RunPolicy = engine.RunOnFailure 277 | } 278 | 279 | // if the pipeline step has unmet conditions the step is 280 | // automatically skipped. 281 | if !src.When.Match(manifest.Match{ 282 | Action: c.Build.Action, 283 | Cron: c.Build.Cron, 284 | Ref: c.Build.Ref, 285 | Repo: c.Repo.Slug, 286 | Instance: c.System.Host, 287 | Target: c.Build.Deploy, 288 | Event: c.Build.Event, 289 | Branch: c.Build.Target, 290 | }) { 291 | dst.RunPolicy = engine.RunNever 292 | } 293 | } 294 | 295 | if isGraph(spec) == false { 296 | configureSerial(spec) 297 | } else if c.Pipeline.Clone.Disable == false { 298 | configureCloneDeps(spec) 299 | } else if c.Pipeline.Clone.Disable == true { 300 | removeCloneDeps(spec) 301 | } 302 | 303 | for _, step := range spec.Steps { 304 | for _, s := range step.Secrets { 305 | secret, ok := c.findSecret(ctx, s.Name) 306 | if ok { 307 | s.Data = []byte(secret) 308 | } 309 | } 310 | } 311 | 312 | return spec 313 | } 314 | 315 | // helper function attempts to find and return the named secret. 316 | // from the secret provider. 317 | func (c *Compiler) findSecret(ctx context.Context, name string) (s string, ok bool) { 318 | if name == "" { 319 | return 320 | } 321 | found, _ := c.Secret.Find(ctx, &secret.Request{ 322 | Name: name, 323 | Build: c.Build, 324 | Repo: c.Repo, 325 | Conf: c.Manifest, 326 | }) 327 | if found == nil { 328 | return 329 | } 330 | return found.Data, true 331 | } 332 | -------------------------------------------------------------------------------- /engine/compiler/compiler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // +build !windows 6 | 7 | package compiler 8 | 9 | import ( 10 | "context" 11 | "encoding/json" 12 | "io/ioutil" 13 | "os" 14 | "testing" 15 | 16 | "github.com/dchest/uniuri" 17 | "github.com/drone-runners/drone-runner-ssh/engine" 18 | "github.com/drone-runners/drone-runner-ssh/engine/resource" 19 | "github.com/drone/drone-go/drone" 20 | "github.com/drone/runner-go/manifest" 21 | "github.com/drone/runner-go/secret" 22 | 23 | "github.com/google/go-cmp/cmp" 24 | "github.com/google/go-cmp/cmp/cmpopts" 25 | ) 26 | 27 | var nocontext = context.Background() 28 | 29 | // dummy function that returns a non-random string for testing. 30 | // it is used in place of the random function. 31 | func notRandom() string { 32 | return "random" 33 | } 34 | 35 | // This test verifies the pipeline dependency graph. When no 36 | // dependency graph is defined, a default dependency graph is 37 | // automatically defined to run steps serially. 38 | func TestCompile_Serial(t *testing.T) { 39 | testCompile(t, "testdata/serial.yml", "testdata/serial.json") 40 | } 41 | 42 | // This test verifies the pipeline dependency graph. It also 43 | // verifies that pipeline steps with no dependencies depend on 44 | // the initial clone step. 45 | func TestCompile_Graph(t *testing.T) { 46 | testCompile(t, "testdata/graph.yml", "testdata/graph.json") 47 | } 48 | 49 | // This test verifies no clone step exists in the pipeline if 50 | // cloning is disabled. 51 | func TestCompile_CloneDisabled_Serial(t *testing.T) { 52 | testCompile(t, "testdata/noclone_serial.yml", "testdata/noclone_serial.json") 53 | } 54 | 55 | // This test verifies no clone step exists in the pipeline if 56 | // cloning is disabled. It also verifies no pipeline steps 57 | // depend on a clone step. 58 | func TestCompile_CloneDisabled_Graph(t *testing.T) { 59 | testCompile(t, "testdata/noclone_graph.yml", "testdata/noclone_graph.json") 60 | } 61 | 62 | // This test verifies that steps are disabled if conditions 63 | // defined in the when block are not satisfied. 64 | func TestCompile_Match(t *testing.T) { 65 | ir := testCompile(t, "testdata/match.yml", "testdata/match.json") 66 | if ir.Steps[0].RunPolicy != engine.RunOnSuccess { 67 | t.Errorf("Expect run on success") 68 | } 69 | if ir.Steps[1].RunPolicy != engine.RunNever { 70 | t.Errorf("Expect run never") 71 | } 72 | } 73 | 74 | // This test verifies that steps configured to run on both 75 | // success or failure are configured to always run. 76 | func TestCompile_RunAlways(t *testing.T) { 77 | ir := testCompile(t, "testdata/run_always.yml", "testdata/run_always.json") 78 | if ir.Steps[0].RunPolicy != engine.RunAlways { 79 | t.Errorf("Expect run always") 80 | } 81 | } 82 | 83 | // This test verifies that steps configured to run on failure 84 | // are configured to run on failure. 85 | func TestCompile_RunFaiure(t *testing.T) { 86 | ir := testCompile(t, "testdata/run_failure.yml", "testdata/run_failure.json") 87 | if ir.Steps[0].RunPolicy != engine.RunOnFailure { 88 | t.Errorf("Expect run on failure") 89 | } 90 | } 91 | 92 | // This test verifies that secrets defined in the yaml are 93 | // requested and stored in the intermediate representation 94 | // at compile time. 95 | func TestCompile_Secrets(t *testing.T) { 96 | manifest, _ := manifest.ParseFile("testdata/secret.yml") 97 | compiler := Compiler{} 98 | compiler.Build = &drone.Build{} 99 | compiler.Repo = &drone.Repo{} 100 | compiler.Stage = &drone.Stage{} 101 | compiler.System = &drone.System{} 102 | compiler.Netrc = &drone.Netrc{} 103 | compiler.Manifest = manifest 104 | compiler.Pipeline = manifest.Resources[0].(*resource.Pipeline) 105 | compiler.Secret = secret.StaticVars(map[string]string{ 106 | "ssh_hostname": "localhost:22", 107 | "ssh_username": "root", 108 | "ssh_password": "password", 109 | "ssh_key": "-----BEGIN RSA PRIVATE KEY-----", 110 | "my_username": "octocat", 111 | }) 112 | ir := compiler.Compile(nocontext) 113 | got := ir.Steps[0].Secrets 114 | want := []*engine.Secret{ 115 | { 116 | Name: "my_password", 117 | Env: "PASSWORD", 118 | Data: nil, // secret not found, data nil 119 | Mask: true, 120 | }, 121 | { 122 | Name: "my_username", 123 | Env: "USERNAME", 124 | Data: []byte("octocat"), // secret found 125 | Mask: true, 126 | }, 127 | } 128 | if diff := cmp.Diff(got, want); len(diff) != 0 { 129 | // TODO(bradrydzewski) ordering is not guaranteed. this 130 | // unit tests needs to be adjusted accordingly. 131 | t.Skipf(diff) 132 | } 133 | if got, want := ir.Server.Hostname, "localhost:22"; got != want { 134 | t.Errorf("Want server host %s, got %s", want, got) 135 | } 136 | if got, want := ir.Server.Username, "root"; got != want { 137 | t.Errorf("Want server user %s, got %s", want, got) 138 | } 139 | if got, want := ir.Server.Password, "password"; got != want { 140 | t.Errorf("Want server password %s, got %s", want, got) 141 | } 142 | if got, want := ir.Server.SSHKey, "-----BEGIN RSA PRIVATE KEY-----"; got != want { 143 | t.Errorf("Want server ssk_key %s, got %s", want, got) 144 | } 145 | } 146 | 147 | // helper function parses and compiles the source file and then 148 | // compares to a golden json file. 149 | func testCompile(t *testing.T, source, golden string) *engine.Spec { 150 | // replace the default random function with one that 151 | // is deterministic, for testing purposes. 152 | random = notRandom 153 | 154 | // restore the default random function and the previously 155 | // specified temporary directory 156 | defer func() { 157 | random = uniuri.New 158 | }() 159 | 160 | manifest, err := manifest.ParseFile(source) 161 | if err != nil { 162 | t.Error(err) 163 | return nil 164 | } 165 | 166 | compiler := Compiler{} 167 | compiler.Build = &drone.Build{Target: "master"} 168 | compiler.Repo = &drone.Repo{} 169 | compiler.Stage = &drone.Stage{} 170 | compiler.System = &drone.System{} 171 | compiler.Netrc = &drone.Netrc{Machine: "github.com", Login: "octocat", Password: "correct-horse-battery-staple"} 172 | compiler.Manifest = manifest 173 | compiler.Pipeline = manifest.Resources[0].(*resource.Pipeline) 174 | got := compiler.Compile(nocontext) 175 | 176 | raw, err := ioutil.ReadFile(golden) 177 | if err != nil { 178 | t.Error(err) 179 | } 180 | 181 | want := new(engine.Spec) 182 | err = json.Unmarshal(raw, want) 183 | if err != nil { 184 | t.Error(err) 185 | } 186 | 187 | ignore := cmpopts.IgnoreFields(engine.Step{}, "Envs", "Secrets") 188 | if diff := cmp.Diff(got, want, ignore); len(diff) != 0 { 189 | t.Errorf(diff) 190 | } 191 | 192 | return got 193 | } 194 | 195 | func dump(v interface{}) { 196 | enc := json.NewEncoder(os.Stdout) 197 | enc.SetIndent("", " ") 198 | enc.Encode(v) 199 | } 200 | -------------------------------------------------------------------------------- /engine/compiler/os.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package compiler 6 | 7 | import ( 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/drone/runner-go/shell/bash" 12 | "github.com/drone/runner-go/shell/powershell" 13 | ) 14 | 15 | // helper function returns the base temporary directory based 16 | // on the target platform. 17 | func tempdir(os string) string { 18 | dir := fmt.Sprintf("drone-%s", random()) 19 | switch os { 20 | case "windows": 21 | return join(os, "C:\\Windows\\Temp", dir) 22 | default: 23 | return join(os, "/tmp", dir) 24 | } 25 | } 26 | 27 | // helper function joins the file paths. 28 | func join(os string, paths ...string) string { 29 | switch os { 30 | case "windows": 31 | return strings.Join(paths, "\\") 32 | default: 33 | return strings.Join(paths, "/") 34 | } 35 | } 36 | 37 | // helper function returns the shell extension based on the 38 | // target platform. 39 | func getExt(os, file string) (s string) { 40 | switch os { 41 | case "windows": 42 | return file + ".ps1" 43 | default: 44 | return file 45 | } 46 | } 47 | 48 | // helper function returns the shell command and arguments 49 | // based on the target platform to invoke the script 50 | func getCommand(os, script string) (string, []string) { 51 | cmd, args := bash.Command() 52 | switch os { 53 | case "windows": 54 | cmd, args = powershell.Command() 55 | } 56 | return cmd, append(args, script) 57 | } 58 | 59 | // helper function returns the netrc file name based on the 60 | // target platform. 61 | func getNetrc(os string) string { 62 | switch os { 63 | case "windows": 64 | return "_netrc" 65 | default: 66 | return ".netrc" 67 | } 68 | } 69 | 70 | // helper function generates and returns a shell script to 71 | // execute the provided shell commands. The shell scripting 72 | // language (bash vs pwoershell) is determined by the operating 73 | // system. 74 | func genScript(os string, commands []string) string { 75 | switch os { 76 | case "windows": 77 | return powershell.Script(commands) 78 | default: 79 | return bash.Script(commands) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /engine/compiler/os_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package compiler 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/drone/runner-go/shell/bash" 12 | "github.com/drone/runner-go/shell/powershell" 13 | 14 | "github.com/dchest/uniuri" 15 | ) 16 | 17 | func Test_tempdir(t *testing.T) { 18 | // replace the default random function with one that 19 | // is deterministic, for testing purposes. 20 | random = notRandom 21 | 22 | // restore the default random function and the previously 23 | // specified temporary directory 24 | defer func() { 25 | random = uniuri.New 26 | }() 27 | 28 | tests := []struct { 29 | os string 30 | path string 31 | }{ 32 | {os: "windows", path: "C:\\Windows\\Temp\\drone-random"}, 33 | {os: "linux", path: "/tmp/drone-random"}, 34 | {os: "openbsd", path: "/tmp/drone-random"}, 35 | {os: "netbsd", path: "/tmp/drone-random"}, 36 | {os: "freebsd", path: "/tmp/drone-random"}, 37 | } 38 | 39 | for _, test := range tests { 40 | if got, want := tempdir(test.os), test.path; got != want { 41 | t.Errorf("Want tempdir %s, got %s", want, got) 42 | } 43 | } 44 | } 45 | 46 | func Test_join(t *testing.T) { 47 | tests := []struct { 48 | os string 49 | a []string 50 | b string 51 | }{ 52 | {os: "windows", a: []string{"C:", "Windows", "Temp"}, b: "C:\\Windows\\Temp"}, 53 | {os: "linux", a: []string{"/tmp", "foo", "bar"}, b: "/tmp/foo/bar"}, 54 | } 55 | for _, test := range tests { 56 | if got, want := join(test.os, test.a...), test.b; got != want { 57 | t.Errorf("Want %s, got %s", want, got) 58 | } 59 | } 60 | } 61 | 62 | func Test_getExt(t *testing.T) { 63 | tests := []struct { 64 | os string 65 | a string 66 | b string 67 | }{ 68 | {os: "windows", a: "clone", b: "clone.ps1"}, 69 | {os: "linux", a: "clone", b: "clone"}, 70 | } 71 | for _, test := range tests { 72 | if got, want := getExt(test.os, test.a), test.b; got != want { 73 | t.Errorf("Want %s, got %s", want, got) 74 | } 75 | } 76 | } 77 | 78 | func Test_getCommand(t *testing.T) { 79 | cmd, args := getCommand("linux", "clone.sh") 80 | if got, want := cmd, "/bin/sh"; got != want { 81 | t.Errorf("Want command %s, got %s", want, got) 82 | } 83 | if !reflect.DeepEqual(args, []string{"-e", "clone.sh"}) { 84 | t.Errorf("Unexpected args %v", args) 85 | } 86 | 87 | cmd, args = getCommand("windows", "clone.ps1") 88 | if got, want := cmd, "powershell"; got != want { 89 | t.Errorf("Want command %s, got %s", want, got) 90 | } 91 | if !reflect.DeepEqual(args, []string{"-noprofile", "-noninteractive", "-command", "clone.ps1"}) { 92 | t.Errorf("Unexpected args %v", args) 93 | } 94 | } 95 | 96 | func Test_getNetrc(t *testing.T) { 97 | tests := []struct { 98 | os string 99 | name string 100 | }{ 101 | {os: "windows", name: "_netrc"}, 102 | {os: "linux", name: ".netrc"}, 103 | {os: "openbsd", name: ".netrc"}, 104 | {os: "netbsd", name: ".netrc"}, 105 | {os: "freebsd", name: ".netrc"}, 106 | } 107 | for _, test := range tests { 108 | if got, want := getNetrc(test.os), test.name; got != want { 109 | t.Errorf("Want %s on %s, got %s", want, test.os, got) 110 | } 111 | } 112 | } 113 | 114 | func Test_getScript(t *testing.T) { 115 | commands := []string{"go build"} 116 | 117 | a := genScript("windows", commands) 118 | b := powershell.Script(commands) 119 | if !reflect.DeepEqual(a, b) { 120 | t.Errorf("Generated windows linux script") 121 | } 122 | 123 | a = genScript("linux", commands) 124 | b = bash.Script(commands) 125 | if !reflect.DeepEqual(a, b) { 126 | t.Errorf("Generated invalid linux script") 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /engine/compiler/testdata/graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "platform": {}, 3 | "server": { 4 | "hostname": "localhost:22", 5 | "username": "root", 6 | "password": "root" 7 | }, 8 | "root": "/tmp/drone-random", 9 | "files": [ 10 | { 11 | "path": "/tmp/drone-random/home", 12 | "mode": 448, 13 | "is_dir": true 14 | }, 15 | { 16 | "path": "/tmp/drone-random/home/drone", 17 | "mode": 448, 18 | "is_dir": true 19 | }, 20 | { 21 | "path": "/tmp/drone-random/drone", 22 | "mode": 448, 23 | "is_dir": true 24 | }, 25 | { 26 | "path": "/tmp/drone-random/drone/src", 27 | "mode": 448, 28 | "is_dir": true 29 | }, 30 | { 31 | "path": "/tmp/drone-random/opt", 32 | "mode": 448, 33 | "is_dir": true 34 | }, 35 | { 36 | "path": "/tmp/drone-random/home/drone/.netrc", 37 | "mode": 384, 38 | "data": "bWFjaGluZSBnaXRodWIuY29tIGxvZ2luIG9jdG9jYXQgcGFzc3dvcmQgY29ycmVjdC1ob3JzZS1iYXR0ZXJ5LXN0YXBsZQ==" 39 | } 40 | ], 41 | "steps": [ 42 | { 43 | "args": [ 44 | "-e", 45 | "/tmp/drone-random/opt/clone" 46 | ], 47 | "command": "/bin/sh", 48 | "files": [ 49 | { 50 | "path": "/tmp/drone-random/opt/clone", 51 | "mode": 448, 52 | "data": "CnNldCAtZQoKZWNobyArICJnaXQgaW5pdCIKZ2l0IGluaXQKCmVjaG8gKyAiZ2l0IHJlbW90ZSBhZGQgb3JpZ2luICIKZ2l0IHJlbW90ZSBhZGQgb3JpZ2luIAoKZWNobyArICJnaXQgZmV0Y2ggIG9yaWdpbiArcmVmcy9oZWFkcy9tYXN0ZXI6IgpnaXQgZmV0Y2ggIG9yaWdpbiArcmVmcy9oZWFkcy9tYXN0ZXI6CgplY2hvICsgImdpdCBjaGVja291dCAgLWIgbWFzdGVyIgpnaXQgY2hlY2tvdXQgIC1iIG1hc3Rlcgo=" 53 | } 54 | ], 55 | "secrets": [], 56 | "name": "clone", 57 | "run_policy": 2, 58 | "working_dir": "/tmp/drone-random/drone/src" 59 | }, 60 | { 61 | "args": [ 62 | "-e", 63 | "/tmp/drone-random/opt/build" 64 | ], 65 | "command": "/bin/sh", 66 | "depends_on": [ 67 | "clone" 68 | ], 69 | "files": [ 70 | { 71 | "path": "/tmp/drone-random/opt/build", 72 | "mode": 448, 73 | "data": "CnNldCAtZQoKZWNobyArICJnbyBidWlsZCIKZ28gYnVpbGQK" 74 | } 75 | ], 76 | "secrets": [], 77 | "name": "build", 78 | "working_dir": "/tmp/drone-random/drone/src" 79 | }, 80 | { 81 | "args": [ 82 | "-e", 83 | "/tmp/drone-random/opt/test" 84 | ], 85 | "command": "/bin/sh", 86 | "depends_on": [ 87 | "build" 88 | ], 89 | "files": [ 90 | { 91 | "path": "/tmp/drone-random/opt/test", 92 | "mode": 448, 93 | "data": "CnNldCAtZQoKZWNobyArICJnbyB0ZXN0IgpnbyB0ZXN0Cg==" 94 | } 95 | ], 96 | "secrets": [], 97 | "name": "test", 98 | "working_dir": "/tmp/drone-random/drone/src" 99 | } 100 | ] 101 | } -------------------------------------------------------------------------------- /engine/compiler/testdata/graph.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | type: ssh 3 | name: default 4 | 5 | server: 6 | host: localhost 7 | user: root 8 | password: root 9 | 10 | steps: 11 | - name: build 12 | commands: 13 | - go build 14 | 15 | - name: test 16 | commands: 17 | - go test 18 | depends_on: [ build ] 19 | -------------------------------------------------------------------------------- /engine/compiler/testdata/match.json: -------------------------------------------------------------------------------- 1 | { 2 | "platform": {}, 3 | "server": { 4 | "hostname": "localhost:22", 5 | "username": "root", 6 | "password": "root" 7 | }, 8 | "root": "/tmp/drone-random", 9 | "files": [ 10 | { 11 | "path": "/tmp/drone-random/home", 12 | "mode": 448, 13 | "is_dir": true 14 | }, 15 | { 16 | "path": "/tmp/drone-random/home/drone", 17 | "mode": 448, 18 | "is_dir": true 19 | }, 20 | { 21 | "path": "/tmp/drone-random/drone", 22 | "mode": 448, 23 | "is_dir": true 24 | }, 25 | { 26 | "path": "/tmp/drone-random/drone/src", 27 | "mode": 448, 28 | "is_dir": true 29 | }, 30 | { 31 | "path": "/tmp/drone-random/opt", 32 | "mode": 448, 33 | "is_dir": true 34 | }, 35 | { 36 | "path": "/tmp/drone-random/home/drone/.netrc", 37 | "mode": 384, 38 | "data": "bWFjaGluZSBnaXRodWIuY29tIGxvZ2luIG9jdG9jYXQgcGFzc3dvcmQgY29ycmVjdC1ob3JzZS1iYXR0ZXJ5LXN0YXBsZQ==" 39 | } 40 | ], 41 | "steps": [ 42 | { 43 | "args": [ 44 | "-e", 45 | "/tmp/drone-random/opt/build" 46 | ], 47 | "command": "/bin/sh", 48 | "files": [ 49 | { 50 | "path": "/tmp/drone-random/opt/build", 51 | "mode": 448, 52 | "data": "CnNldCAtZQoKZWNobyArICJnbyBidWlsZCIKZ28gYnVpbGQK" 53 | } 54 | ], 55 | "name": "build", 56 | "working_dir": "/tmp/drone-random/drone/src" 57 | }, 58 | { 59 | "args": [ 60 | "-e", 61 | "/tmp/drone-random/opt/test" 62 | ], 63 | "command": "/bin/sh", 64 | "depends_on": [ 65 | "build" 66 | ], 67 | "files": [ 68 | { 69 | "path": "/tmp/drone-random/opt/test", 70 | "mode": 448, 71 | "data": "CnNldCAtZQoKZWNobyArICJnbyB0ZXN0IgpnbyB0ZXN0Cg==" 72 | } 73 | ], 74 | "name": "test", 75 | "run_policy": 3, 76 | "working_dir": "/tmp/drone-random/drone/src" 77 | } 78 | ] 79 | } -------------------------------------------------------------------------------- /engine/compiler/testdata/match.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | type: ssh 3 | name: default 4 | 5 | clone: 6 | disable: true 7 | 8 | server: 9 | host: localhost 10 | user: root 11 | password: root 12 | 13 | steps: 14 | - name: build 15 | commands: 16 | - go build 17 | when: 18 | branch: [ master ] 19 | 20 | - name: test 21 | commands: 22 | - go test 23 | when: 24 | branch: [ develop ] 25 | -------------------------------------------------------------------------------- /engine/compiler/testdata/noclone_graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "platform": {}, 3 | "server": { 4 | "hostname": "localhost:22", 5 | "username": "root", 6 | "password": "root" 7 | }, 8 | "root": "/tmp/drone-random", 9 | "files": [ 10 | { 11 | "path": "/tmp/drone-random/home", 12 | "mode": 448, 13 | "is_dir": true 14 | }, 15 | { 16 | "path": "/tmp/drone-random/home/drone", 17 | "mode": 448, 18 | "is_dir": true 19 | }, 20 | { 21 | "path": "/tmp/drone-random/drone", 22 | "mode": 448, 23 | "is_dir": true 24 | }, 25 | { 26 | "path": "/tmp/drone-random/drone/src", 27 | "mode": 448, 28 | "is_dir": true 29 | }, 30 | { 31 | "path": "/tmp/drone-random/opt", 32 | "mode": 448, 33 | "is_dir": true 34 | }, 35 | { 36 | "path": "/tmp/drone-random/home/drone/.netrc", 37 | "mode": 384, 38 | "data": "bWFjaGluZSBnaXRodWIuY29tIGxvZ2luIG9jdG9jYXQgcGFzc3dvcmQgY29ycmVjdC1ob3JzZS1iYXR0ZXJ5LXN0YXBsZQ==" 39 | } 40 | ], 41 | "steps": [ 42 | { 43 | "args": [ 44 | "-e", 45 | "/tmp/drone-random/opt/build" 46 | ], 47 | "command": "/bin/sh", 48 | "files": [ 49 | { 50 | "path": "/tmp/drone-random/opt/build", 51 | "mode": 448, 52 | "data": "CnNldCAtZQoKZWNobyArICJnbyBidWlsZCIKZ28gYnVpbGQK" 53 | } 54 | ], 55 | "name": "build", 56 | "secrets": [], 57 | "working_dir": "/tmp/drone-random/drone/src" 58 | }, 59 | { 60 | "args": [ 61 | "-e", 62 | "/tmp/drone-random/opt/test" 63 | ], 64 | "command": "/bin/sh", 65 | "depends_on": [ 66 | "build" 67 | ], 68 | "files": [ 69 | { 70 | "path": "/tmp/drone-random/opt/test", 71 | "mode": 448, 72 | "data": "CnNldCAtZQoKZWNobyArICJnbyB0ZXN0IgpnbyB0ZXN0Cg==" 73 | } 74 | ], 75 | "name": "test", 76 | "secrets": [], 77 | "working_dir": "/tmp/drone-random/drone/src" 78 | } 79 | ] 80 | } -------------------------------------------------------------------------------- /engine/compiler/testdata/noclone_graph.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | type: ssh 3 | name: default 4 | 5 | clone: 6 | disable: true 7 | 8 | server: 9 | host: localhost 10 | user: root 11 | password: root 12 | 13 | steps: 14 | - name: build 15 | commands: 16 | - go build 17 | 18 | - name: test 19 | commands: 20 | - go test 21 | depends_on: [ build ] -------------------------------------------------------------------------------- /engine/compiler/testdata/noclone_serial.json: -------------------------------------------------------------------------------- 1 | { 2 | "platform": {}, 3 | "server": { 4 | "hostname": "localhost:22", 5 | "username": "root", 6 | "password": "root" 7 | }, 8 | "root": "/tmp/drone-random", 9 | "files": [ 10 | { 11 | "path": "/tmp/drone-random/home", 12 | "mode": 448, 13 | "is_dir": true 14 | }, 15 | { 16 | "path": "/tmp/drone-random/home/drone", 17 | "mode": 448, 18 | "is_dir": true 19 | }, 20 | { 21 | "path": "/tmp/drone-random/drone", 22 | "mode": 448, 23 | "is_dir": true 24 | }, 25 | { 26 | "path": "/tmp/drone-random/drone/src", 27 | "mode": 448, 28 | "is_dir": true 29 | }, 30 | { 31 | "path": "/tmp/drone-random/opt", 32 | "mode": 448, 33 | "is_dir": true 34 | }, 35 | { 36 | "path": "/tmp/drone-random/home/drone/.netrc", 37 | "mode": 384, 38 | "data": "bWFjaGluZSBnaXRodWIuY29tIGxvZ2luIG9jdG9jYXQgcGFzc3dvcmQgY29ycmVjdC1ob3JzZS1iYXR0ZXJ5LXN0YXBsZQ==" 39 | } 40 | ], 41 | "steps": [ 42 | { 43 | "args": [ 44 | "-e", 45 | "/tmp/drone-random/opt/build" 46 | ], 47 | "command": "/bin/sh", 48 | "files": [ 49 | { 50 | "path": "/tmp/drone-random/opt/build", 51 | "mode": 448, 52 | "data": "CnNldCAtZQoKZWNobyArICJnbyBidWlsZCIKZ28gYnVpbGQKCmVjaG8gKyAiZ28gdGVzdCIKZ28gdGVzdAo=" 53 | } 54 | ], 55 | "name": "build", 56 | "working_dir": "/tmp/drone-random/drone/src" 57 | } 58 | ] 59 | } -------------------------------------------------------------------------------- /engine/compiler/testdata/noclone_serial.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | type: ssh 3 | name: default 4 | 5 | clone: 6 | disable: true 7 | 8 | server: 9 | host: localhost 10 | user: root 11 | password: root 12 | 13 | steps: 14 | - name: build 15 | commands: 16 | - go build 17 | - go test 18 | -------------------------------------------------------------------------------- /engine/compiler/testdata/run_always.json: -------------------------------------------------------------------------------- 1 | { 2 | "platform": {}, 3 | "server": { 4 | "hostname": "localhost:22", 5 | "username": "root", 6 | "password": "root" 7 | }, 8 | "root": "/tmp/drone-random", 9 | "files": [ 10 | { 11 | "path": "/tmp/drone-random/home", 12 | "mode": 448, 13 | "is_dir": true 14 | }, 15 | { 16 | "path": "/tmp/drone-random/home/drone", 17 | "mode": 448, 18 | "is_dir": true 19 | }, 20 | { 21 | "path": "/tmp/drone-random/drone", 22 | "mode": 448, 23 | "is_dir": true 24 | }, 25 | { 26 | "path": "/tmp/drone-random/drone/src", 27 | "mode": 448, 28 | "is_dir": true 29 | }, 30 | { 31 | "path": "/tmp/drone-random/opt", 32 | "mode": 448, 33 | "is_dir": true 34 | }, 35 | { 36 | "path": "/tmp/drone-random/home/drone/.netrc", 37 | "mode": 384, 38 | "data": "bWFjaGluZSBnaXRodWIuY29tIGxvZ2luIG9jdG9jYXQgcGFzc3dvcmQgY29ycmVjdC1ob3JzZS1iYXR0ZXJ5LXN0YXBsZQ==" 39 | } 40 | ], 41 | "steps": [ 42 | { 43 | "args": [ 44 | "-e", 45 | "/tmp/drone-random/opt/build" 46 | ], 47 | "command": "/bin/sh", 48 | "files": [ 49 | { 50 | "path": "/tmp/drone-random/opt/build", 51 | "mode": 448, 52 | "data": "CnNldCAtZQoKZWNobyArICJnbyBidWlsZCIKZ28gYnVpbGQK" 53 | } 54 | ], 55 | "name": "build", 56 | "run_policy": 2, 57 | "working_dir": "/tmp/drone-random/drone/src" 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /engine/compiler/testdata/run_always.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | type: ssh 3 | name: default 4 | 5 | clone: 6 | disable: true 7 | 8 | server: 9 | host: localhost 10 | user: root 11 | password: root 12 | 13 | steps: 14 | - name: build 15 | commands: 16 | - go build 17 | when: 18 | status: [ success, failure ] 19 | -------------------------------------------------------------------------------- /engine/compiler/testdata/run_failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "platform": {}, 3 | "server": { 4 | "hostname": "localhost:22", 5 | "username": "root", 6 | "password": "root" 7 | }, 8 | "root": "/tmp/drone-random", 9 | "files": [ 10 | { 11 | "path": "/tmp/drone-random/home", 12 | "mode": 448, 13 | "is_dir": true 14 | }, 15 | { 16 | "path": "/tmp/drone-random/home/drone", 17 | "mode": 448, 18 | "is_dir": true 19 | }, 20 | { 21 | "path": "/tmp/drone-random/drone", 22 | "mode": 448, 23 | "is_dir": true 24 | }, 25 | { 26 | "path": "/tmp/drone-random/drone/src", 27 | "mode": 448, 28 | "is_dir": true 29 | }, 30 | { 31 | "path": "/tmp/drone-random/opt", 32 | "mode": 448, 33 | "is_dir": true 34 | }, 35 | { 36 | "path": "/tmp/drone-random/home/drone/.netrc", 37 | "mode": 384, 38 | "data": "bWFjaGluZSBnaXRodWIuY29tIGxvZ2luIG9jdG9jYXQgcGFzc3dvcmQgY29ycmVjdC1ob3JzZS1iYXR0ZXJ5LXN0YXBsZQ==" 39 | } 40 | ], 41 | "steps": [ 42 | { 43 | "args": [ 44 | "-e", 45 | "/tmp/drone-random/opt/build" 46 | ], 47 | "command": "/bin/sh", 48 | "files": [ 49 | { 50 | "path": "/tmp/drone-random/opt/build", 51 | "mode": 448, 52 | "data": "CnNldCAtZQoKZWNobyArICJnbyBidWlsZCIKZ28gYnVpbGQK" 53 | } 54 | ], 55 | "name": "build", 56 | "run_policy": 1, 57 | "working_dir": "/tmp/drone-random/drone/src" 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /engine/compiler/testdata/run_failure.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | type: ssh 3 | name: default 4 | 5 | clone: 6 | disable: true 7 | 8 | server: 9 | host: localhost 10 | user: root 11 | password: root 12 | 13 | steps: 14 | - name: build 15 | commands: 16 | - go build 17 | when: 18 | status: [ failure ] 19 | -------------------------------------------------------------------------------- /engine/compiler/testdata/secret.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | type: ssh 3 | name: default 4 | 5 | clone: 6 | disable: true 7 | 8 | server: 9 | host: 10 | from_secret: ssh_hostname 11 | user: 12 | from_secret: ssh_username 13 | password: 14 | from_secret: ssh_password 15 | ssh_key: 16 | from_secret: ssh_key 17 | 18 | steps: 19 | - name: build 20 | environment: 21 | PASSWORD: 22 | from_secret: my_password 23 | USERNAME: 24 | from_secret: my_username 25 | commands: 26 | - go build 27 | - go test 28 | -------------------------------------------------------------------------------- /engine/compiler/testdata/serial.json: -------------------------------------------------------------------------------- 1 | { 2 | "platform": {}, 3 | "server": { 4 | "hostname": "localhost:22", 5 | "username": "root", 6 | "password": "root" 7 | }, 8 | "root": "/tmp/drone-random", 9 | "files": [ 10 | { 11 | "path": "/tmp/drone-random/home", 12 | "mode": 448, 13 | "is_dir": true 14 | }, 15 | { 16 | "path": "/tmp/drone-random/home/drone", 17 | "mode": 448, 18 | "is_dir": true 19 | }, 20 | { 21 | "path": "/tmp/drone-random/drone", 22 | "mode": 448, 23 | "is_dir": true 24 | }, 25 | { 26 | "path": "/tmp/drone-random/drone/src", 27 | "mode": 448, 28 | "is_dir": true 29 | }, 30 | { 31 | "path": "/tmp/drone-random/opt", 32 | "mode": 448, 33 | "is_dir": true 34 | }, 35 | { 36 | "path": "/tmp/drone-random/home/drone/.netrc", 37 | "mode": 384, 38 | "data": "bWFjaGluZSBnaXRodWIuY29tIGxvZ2luIG9jdG9jYXQgcGFzc3dvcmQgY29ycmVjdC1ob3JzZS1iYXR0ZXJ5LXN0YXBsZQ==" 39 | } 40 | ], 41 | "steps": [ 42 | { 43 | "args": [ 44 | "-e", 45 | "/tmp/drone-random/opt/clone" 46 | ], 47 | "command": "/bin/sh", 48 | "files": [ 49 | { 50 | "path": "/tmp/drone-random/opt/clone", 51 | "mode": 448, 52 | "data": "CnNldCAtZQoKZWNobyArICJnaXQgaW5pdCIKZ2l0IGluaXQKCmVjaG8gKyAiZ2l0IHJlbW90ZSBhZGQgb3JpZ2luICIKZ2l0IHJlbW90ZSBhZGQgb3JpZ2luIAoKZWNobyArICJnaXQgZmV0Y2ggIG9yaWdpbiArcmVmcy9oZWFkcy9tYXN0ZXI6IgpnaXQgZmV0Y2ggIG9yaWdpbiArcmVmcy9oZWFkcy9tYXN0ZXI6CgplY2hvICsgImdpdCBjaGVja291dCAgLWIgbWFzdGVyIgpnaXQgY2hlY2tvdXQgIC1iIG1hc3Rlcgo=" 53 | } 54 | ], 55 | "secrets": [], 56 | "name": "clone", 57 | "run_policy": 2, 58 | "working_dir": "/tmp/drone-random/drone/src" 59 | }, 60 | { 61 | "args": [ 62 | "-e", 63 | "/tmp/drone-random/opt/build" 64 | ], 65 | "command": "/bin/sh", 66 | "depends_on": [ 67 | "clone" 68 | ], 69 | "files": [ 70 | { 71 | "path": "/tmp/drone-random/opt/build", 72 | "mode": 448, 73 | "data": "CnNldCAtZQoKZWNobyArICJnbyBidWlsZCIKZ28gYnVpbGQK" 74 | } 75 | ], 76 | "secrets": [], 77 | "name": "build", 78 | "working_dir": "/tmp/drone-random/drone/src" 79 | }, 80 | { 81 | "args": [ 82 | "-e", 83 | "/tmp/drone-random/opt/test" 84 | ], 85 | "command": "/bin/sh", 86 | "depends_on": [ 87 | "build" 88 | ], 89 | "files": [ 90 | { 91 | "path": "/tmp/drone-random/opt/test", 92 | "mode": 448, 93 | "data": "CnNldCAtZQoKZWNobyArICJnbyB0ZXN0IgpnbyB0ZXN0Cg==" 94 | } 95 | ], 96 | "secrets": [], 97 | "name": "test", 98 | "working_dir": "/tmp/drone-random/drone/src" 99 | } 100 | ] 101 | } -------------------------------------------------------------------------------- /engine/compiler/testdata/serial.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | type: ssh 3 | name: default 4 | 5 | server: 6 | host: localhost 7 | user: root 8 | password: root 9 | 10 | steps: 11 | - name: build 12 | commands: 13 | - go build 14 | 15 | - name: test 16 | commands: 17 | - go test 18 | -------------------------------------------------------------------------------- /engine/compiler/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package compiler 6 | 7 | import ( 8 | "strings" 9 | 10 | "github.com/drone-runners/drone-runner-ssh/engine" 11 | "github.com/drone-runners/drone-runner-ssh/engine/resource" 12 | "github.com/drone/drone-go/drone" 13 | "github.com/drone/runner-go/manifest" 14 | ) 15 | 16 | // helper function returns true if the step is configured to 17 | // always run regardless of status. 18 | func isRunAlways(step *resource.Step) bool { 19 | if len(step.When.Status.Include) == 0 && 20 | len(step.When.Status.Exclude) == 0 { 21 | return false 22 | } 23 | return step.When.Status.Match(drone.StatusFailing) && 24 | step.When.Status.Match(drone.StatusPassing) 25 | } 26 | 27 | // helper function returns true if the step is configured to 28 | // only run on failure. 29 | func isRunOnFailure(step *resource.Step) bool { 30 | if len(step.When.Status.Include) == 0 && 31 | len(step.When.Status.Exclude) == 0 { 32 | return false 33 | } 34 | return step.When.Status.Match(drone.StatusFailing) 35 | } 36 | 37 | // helper function returns true if the pipeline specification 38 | // manually defines an execution graph. 39 | func isGraph(spec *engine.Spec) bool { 40 | for _, step := range spec.Steps { 41 | if len(step.DependsOn) > 0 { 42 | return true 43 | } 44 | } 45 | return false 46 | } 47 | 48 | // helper function creates the dependency graph for serial 49 | // pipeline execution. 50 | func configureSerial(spec *engine.Spec) { 51 | var prev *engine.Step 52 | for _, step := range spec.Steps { 53 | if prev != nil { 54 | step.DependsOn = []string{prev.Name} 55 | } 56 | prev = step 57 | } 58 | } 59 | 60 | // helper function converts the environment variables to a map, 61 | // returning only inline environment variables not derived from 62 | // a secret. 63 | func convertStaticEnv(src map[string]*manifest.Variable) map[string]string { 64 | dst := map[string]string{} 65 | for k, v := range src { 66 | if v == nil { 67 | continue 68 | } 69 | if strings.TrimSpace(v.Secret) == "" { 70 | dst[k] = v.Value 71 | } 72 | } 73 | return dst 74 | } 75 | 76 | // helper function converts the environment variables to a map, 77 | // returning only inline environment variables not derived from 78 | // a secret. 79 | func convertSecretEnv(src map[string]*manifest.Variable) []*engine.Secret { 80 | dst := []*engine.Secret{} 81 | for k, v := range src { 82 | if v == nil { 83 | continue 84 | } 85 | if strings.TrimSpace(v.Secret) != "" { 86 | dst = append(dst, &engine.Secret{ 87 | Name: v.Secret, 88 | Mask: true, 89 | Env: k, 90 | }) 91 | } 92 | } 93 | return dst 94 | } 95 | 96 | // helper function modifies the pipeline dependency graph to 97 | // account for the clone step. 98 | func configureCloneDeps(spec *engine.Spec) { 99 | for _, step := range spec.Steps { 100 | if step.Name == "clone" { 101 | continue 102 | } 103 | if len(step.DependsOn) == 0 { 104 | step.DependsOn = []string{"clone"} 105 | } 106 | } 107 | } 108 | 109 | // helper function modifies the pipeline dependency graph to 110 | // account for a disabled clone step. 111 | func removeCloneDeps(spec *engine.Spec) { 112 | for _, step := range spec.Steps { 113 | if step.Name == "clone" { 114 | return 115 | } 116 | } 117 | for _, step := range spec.Steps { 118 | if len(step.DependsOn) == 1 && 119 | step.DependsOn[0] == "clone" { 120 | step.DependsOn = []string{} 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /engine/compiler/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package compiler 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/drone-runners/drone-runner-ssh/engine" 11 | "github.com/drone-runners/drone-runner-ssh/engine/resource" 12 | "github.com/drone/runner-go/manifest" 13 | 14 | "github.com/google/go-cmp/cmp" 15 | ) 16 | 17 | func Test_isRunAlways(t *testing.T) { 18 | step := new(resource.Step) 19 | if isRunAlways(step) == true { 20 | t.Errorf("Want always run false if empty when clause") 21 | } 22 | step.When.Status.Include = []string{"success"} 23 | if isRunAlways(step) == true { 24 | t.Errorf("Want always run false if when success") 25 | } 26 | step.When.Status.Include = []string{"failure"} 27 | if isRunAlways(step) == true { 28 | t.Errorf("Want always run false if when faiure") 29 | } 30 | step.When.Status.Include = []string{"success", "failure"} 31 | if isRunAlways(step) == false { 32 | t.Errorf("Want always run true if when success, failure") 33 | } 34 | } 35 | 36 | func Test_isRunOnFailure(t *testing.T) { 37 | step := new(resource.Step) 38 | if isRunOnFailure(step) == true { 39 | t.Errorf("Want run on failure false if empty when clause") 40 | } 41 | step.When.Status.Include = []string{"success"} 42 | if isRunOnFailure(step) == true { 43 | t.Errorf("Want run on failure false if when success") 44 | } 45 | step.When.Status.Include = []string{"failure"} 46 | if isRunOnFailure(step) == false { 47 | t.Errorf("Want run on failure true if when faiure") 48 | } 49 | step.When.Status.Include = []string{"success", "failure"} 50 | if isRunOnFailure(step) == false { 51 | t.Errorf("Want run on failure true if when success, failure") 52 | } 53 | } 54 | 55 | func Test_isGraph(t *testing.T) { 56 | spec := new(engine.Spec) 57 | spec.Steps = []*engine.Step{ 58 | {DependsOn: []string{}}, 59 | } 60 | if isGraph(spec) == true { 61 | t.Errorf("Expect is graph false if deps not exist") 62 | } 63 | spec.Steps[0].DependsOn = []string{"clone"} 64 | if isGraph(spec) == false { 65 | t.Errorf("Expect is graph true if deps exist") 66 | } 67 | } 68 | 69 | func Test_configureSerial(t *testing.T) { 70 | before := new(engine.Spec) 71 | before.Steps = []*engine.Step{ 72 | {Name: "build"}, 73 | {Name: "test"}, 74 | {Name: "deploy"}, 75 | } 76 | 77 | after := new(engine.Spec) 78 | after.Steps = []*engine.Step{ 79 | {Name: "build"}, 80 | {Name: "test", DependsOn: []string{"build"}}, 81 | {Name: "deploy", DependsOn: []string{"test"}}, 82 | } 83 | configureSerial(before) 84 | if diff := cmp.Diff(before, after); diff != "" { 85 | t.Errorf("Unexpected serial configuration") 86 | t.Log(diff) 87 | } 88 | } 89 | 90 | func Test_convertStaticEnv(t *testing.T) { 91 | vars := map[string]*manifest.Variable{ 92 | "username": &manifest.Variable{Value: "octocat"}, 93 | "password": &manifest.Variable{Secret: "password"}, 94 | } 95 | envs := convertStaticEnv(vars) 96 | want := map[string]string{"username": "octocat"} 97 | if diff := cmp.Diff(envs, want); diff != "" { 98 | t.Errorf("Unexpected environment variable set") 99 | t.Log(diff) 100 | } 101 | } 102 | 103 | func Test_convertSecretEnv(t *testing.T) { 104 | vars := map[string]*manifest.Variable{ 105 | "USERNAME": &manifest.Variable{Value: "octocat"}, 106 | "PASSWORD": &manifest.Variable{Secret: "password"}, 107 | } 108 | envs := convertSecretEnv(vars) 109 | want := []*engine.Secret{ 110 | { 111 | Name: "password", 112 | Env: "PASSWORD", 113 | Mask: true, 114 | }, 115 | } 116 | if diff := cmp.Diff(envs, want); diff != "" { 117 | t.Errorf("Unexpected secret list") 118 | t.Log(diff) 119 | } 120 | } 121 | 122 | func Test_configureCloneDeps(t *testing.T) { 123 | before := new(engine.Spec) 124 | before.Steps = []*engine.Step{ 125 | {Name: "clone"}, 126 | {Name: "backend"}, 127 | {Name: "frontend"}, 128 | {Name: "deploy", DependsOn: []string{ 129 | "backend", "frontend", 130 | }}, 131 | } 132 | 133 | after := new(engine.Spec) 134 | after.Steps = []*engine.Step{ 135 | {Name: "clone"}, 136 | {Name: "backend", DependsOn: []string{"clone"}}, 137 | {Name: "frontend", DependsOn: []string{"clone"}}, 138 | {Name: "deploy", DependsOn: []string{ 139 | "backend", "frontend", 140 | }}, 141 | } 142 | configureCloneDeps(before) 143 | if diff := cmp.Diff(before, after); diff != "" { 144 | t.Errorf("Unexpected dependency adjustment") 145 | t.Log(diff) 146 | } 147 | } 148 | 149 | func Test_removeCloneDeps(t *testing.T) { 150 | before := new(engine.Spec) 151 | before.Steps = []*engine.Step{ 152 | {Name: "backend", DependsOn: []string{"clone"}}, 153 | {Name: "frontend", DependsOn: []string{"clone"}}, 154 | {Name: "deploy", DependsOn: []string{ 155 | "backend", "frontend", 156 | }}, 157 | } 158 | 159 | after := new(engine.Spec) 160 | after.Steps = []*engine.Step{ 161 | {Name: "backend", DependsOn: []string{}}, 162 | {Name: "frontend", DependsOn: []string{}}, 163 | {Name: "deploy", DependsOn: []string{ 164 | "backend", "frontend", 165 | }}, 166 | } 167 | removeCloneDeps(before) 168 | if diff := cmp.Diff(before, after); diff != "" { 169 | t.Errorf("Unexpected result after removing clone deps") 170 | t.Log(diff) 171 | } 172 | } 173 | 174 | func Test_removeCloneDeps_CloneEnabled(t *testing.T) { 175 | before := new(engine.Spec) 176 | before.Steps = []*engine.Step{ 177 | {Name: "clone"}, 178 | {Name: "test", DependsOn: []string{"clone"}}, 179 | } 180 | 181 | after := new(engine.Spec) 182 | after.Steps = []*engine.Step{ 183 | {Name: "clone"}, 184 | {Name: "test", DependsOn: []string{"clone"}}, 185 | } 186 | removeCloneDeps(before) 187 | if diff := cmp.Diff(before, after); diff != "" { 188 | t.Errorf("Expect clone dependencies not removed") 189 | t.Log(diff) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /engine/engine.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package engine 6 | 7 | import ( 8 | "context" 9 | "io" 10 | ) 11 | 12 | // Engine is the interface that must be implemented by a 13 | // pipeline execution engine. 14 | type Engine interface { 15 | // Setup the pipeline environment. 16 | Setup(context.Context, *Spec) error 17 | 18 | // Destroy the pipeline environment. 19 | Destroy(context.Context, *Spec) error 20 | 21 | // Run runs the pipeine step. 22 | Run(context.Context, *Spec, *Step, io.Writer) (*State, error) 23 | } 24 | -------------------------------------------------------------------------------- /engine/engine_impl.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package engine 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "io" 11 | "os" 12 | "strings" 13 | 14 | "github.com/drone/runner-go/logger" 15 | 16 | "github.com/pkg/sftp" 17 | "golang.org/x/crypto/ssh" 18 | ) 19 | 20 | // New returns a new engine. 21 | func New() Engine { 22 | return new(engine) 23 | } 24 | 25 | type engine struct{} 26 | 27 | // Setup the pipeline environment. 28 | func (e *engine) Setup(ctx context.Context, spec *Spec) error { 29 | client, err := dial( 30 | spec.Server.Hostname, 31 | spec.Server.Username, 32 | spec.Server.Password, 33 | spec.Server.SSHKey, 34 | ) 35 | if err != nil { 36 | return err 37 | } 38 | defer client.Close() 39 | 40 | clientftp, err := sftp.NewClient(client) 41 | if err != nil { 42 | return err 43 | } 44 | defer clientftp.Close() 45 | 46 | // the pipeline workspace is created before pipeline 47 | // execution begins. All files and folders created during 48 | // pipeline execution are isolated to this workspace. 49 | err = mkdir(clientftp, spec.Root, 0777) 50 | if err != nil { 51 | logger.FromContext(ctx). 52 | WithError(err). 53 | WithField("path", spec.Root). 54 | Error("cannot create workspace directory") 55 | return err 56 | } 57 | 58 | // the pipeline specification may define global folders, such 59 | // as the pipeline working directory, wich must be created 60 | // before pipeline execution begins. 61 | for _, file := range spec.Files { 62 | if file.IsDir == false { 63 | continue 64 | } 65 | err = mkdir(clientftp, file.Path, file.Mode) 66 | if err != nil { 67 | logger.FromContext(ctx). 68 | WithError(err). 69 | WithField("path", file.Path). 70 | Error("cannot create directory") 71 | return err 72 | } 73 | } 74 | 75 | // the pipeline specification may define global files such 76 | // as authentication credentials that should be uploaded 77 | // before pipeline execution begins. 78 | for _, file := range spec.Files { 79 | if file.IsDir == true { 80 | continue 81 | } 82 | err = upload(clientftp, file.Path, file.Data, file.Mode) 83 | if err != nil { 84 | logger.FromContext(ctx). 85 | WithError(err). 86 | Error("cannot write file") 87 | return err 88 | } 89 | } 90 | 91 | return nil 92 | } 93 | 94 | // Destroy the pipeline environment. 95 | func (e *engine) Destroy(ctx context.Context, spec *Spec) error { 96 | client, err := dial( 97 | spec.Server.Hostname, 98 | spec.Server.Username, 99 | spec.Server.Password, 100 | spec.Server.SSHKey, 101 | ) 102 | if err != nil { 103 | return err 104 | } 105 | defer client.Close() 106 | 107 | ftp, err := sftp.NewClient(client) 108 | if err != nil { 109 | return err 110 | } 111 | defer ftp.Close() 112 | if err = ftp.RemoveDirectory(spec.Root); err == nil { 113 | return nil 114 | } 115 | 116 | // ideally we would remove the directory using sftp, however, 117 | // it consistnetly errors on linux and windows. We therefore 118 | // fallback to executing ssh commands to remove the directory 119 | 120 | logger.FromContext(ctx). 121 | WithError(err). 122 | WithField("path", spec.Root). 123 | Trace("cannot remove workspace using sftp") 124 | 125 | session, err := client.NewSession() 126 | if err != nil { 127 | return err 128 | } 129 | defer session.Close() 130 | 131 | err = session.Run( 132 | removeCommand(spec.Platform.OS, spec.Root)) 133 | if err != nil { 134 | logger.FromContext(ctx). 135 | WithError(err). 136 | WithField("path", spec.Root). 137 | Warn("cannot remove workspace") 138 | } 139 | return err 140 | } 141 | 142 | // Run runs the pipeline step. 143 | func (e *engine) Run(ctx context.Context, spec *Spec, step *Step, output io.Writer) (*State, error) { 144 | client, err := dial( 145 | spec.Server.Hostname, 146 | spec.Server.Username, 147 | spec.Server.Password, 148 | spec.Server.SSHKey, 149 | ) 150 | if err != nil { 151 | return nil, err 152 | } 153 | defer client.Close() 154 | 155 | clientftp, err := sftp.NewClient(client) 156 | if err != nil { 157 | return nil, err 158 | } 159 | defer clientftp.Close() 160 | 161 | // unlike os/exec there is no good way to set environment 162 | // the working directory or configure environment variables. 163 | // we work around this by pre-pending these configurations 164 | // to the pipeline execution script. 165 | for _, file := range step.Files { 166 | w := new(bytes.Buffer) 167 | writeWorkdir(w, step.WorkingDir) 168 | writeSecrets(w, spec.Platform.OS, step.Secrets) 169 | writeEnviron(w, spec.Platform.OS, step.Envs) 170 | w.Write(file.Data) 171 | err = upload(clientftp, file.Path, w.Bytes(), file.Mode) 172 | if err != nil { 173 | logger.FromContext(ctx). 174 | WithError(err). 175 | WithField("path", file.Path). 176 | Error("cannot write file") 177 | return nil, err 178 | } 179 | } 180 | 181 | session, err := client.NewSession() 182 | if err != nil { 183 | return nil, err 184 | } 185 | defer session.Close() 186 | 187 | session.Stdout = output 188 | session.Stderr = output 189 | cmd := step.Command + " " + strings.Join(step.Args, " ") 190 | 191 | log := logger.FromContext(ctx) 192 | log.Debug("ssh session started") 193 | 194 | done := make(chan error) 195 | go func() { 196 | done <- session.Run(cmd) 197 | }() 198 | 199 | select { 200 | case err = <-done: 201 | case <-ctx.Done(): 202 | // BUG(bradrydzewski): openssh does not support the signal 203 | // command and will not signal remote processes. This may 204 | // be resolved in openssh 7.9 or higher. Please subscribe 205 | // to https://github.com/golang/go/issues/16597. 206 | if err := session.Signal(ssh.SIGKILL); err != nil { 207 | log.WithError(err).Debug("kill remote process") 208 | } 209 | 210 | log.Debug("ssh session killed") 211 | return nil, ctx.Err() 212 | } 213 | 214 | state := &State{ 215 | ExitCode: 0, 216 | Exited: true, 217 | OOMKilled: false, 218 | } 219 | if err != nil { 220 | state.ExitCode = 255 221 | } 222 | if exiterr, ok := err.(*ssh.ExitError); ok { 223 | state.ExitCode = exiterr.ExitStatus() 224 | } 225 | 226 | log.WithField("ssh.exit", state.ExitCode). 227 | Debug("ssh session finished") 228 | return state, err 229 | } 230 | 231 | // helper function configures and dials the ssh server. 232 | func dial(server, username, password, privatekey string) (*ssh.Client, error) { 233 | config := &ssh.ClientConfig{ 234 | User: username, 235 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 236 | } 237 | if privatekey != "" { 238 | pem := []byte(privatekey) 239 | signer, err := ssh.ParsePrivateKey(pem) 240 | if err != nil { 241 | return nil, err 242 | } 243 | config.Auth = append(config.Auth, ssh.PublicKeys(signer)) 244 | } 245 | if password != "" { 246 | config.Auth = append(config.Auth, ssh.Password(password)) 247 | } 248 | return ssh.Dial("tcp", server, config) 249 | } 250 | 251 | // helper function writes the file to the remote server and then 252 | // configures the file permissions. 253 | func upload(client *sftp.Client, path string, data []byte, mode uint32) error { 254 | f, err := client.Create(path) 255 | if err != nil { 256 | return err 257 | } 258 | defer f.Close() 259 | if _, err := f.Write(data); err != nil { 260 | return err 261 | } 262 | err = f.Chmod(os.FileMode(mode)) 263 | if err != nil { 264 | return err 265 | } 266 | return nil 267 | } 268 | 269 | // helper function creates the folder on the remote server and 270 | // then configures the folder permissions. 271 | func mkdir(client *sftp.Client, path string, mode uint32) error { 272 | err := client.MkdirAll(path) 273 | if err != nil { 274 | return err 275 | } 276 | return client.Chmod(path, os.FileMode(mode)) 277 | } 278 | -------------------------------------------------------------------------------- /engine/replacer/replacer.go: -------------------------------------------------------------------------------- 1 | // Code generated automatically. DO NOT EDIT. 2 | 3 | // Copyright 2019 Drone.IO Inc. All rights reserved. 4 | // Use of this source code is governed by the Polyform License 5 | // that can be found in the LICENSE file. 6 | 7 | package replacer 8 | 9 | import ( 10 | "fmt" 11 | "io" 12 | "strings" 13 | 14 | "github.com/drone-runners/drone-runner-ssh/engine" 15 | ) 16 | 17 | const maskedf = "[secret:%s]" 18 | 19 | // Replacer is an io.Writer that finds and masks sensitive data. 20 | type Replacer struct { 21 | w io.WriteCloser 22 | r *strings.Replacer 23 | } 24 | 25 | // New returns a replacer that wraps writer w. 26 | func New(w io.WriteCloser, secrets []*engine.Secret) io.WriteCloser { 27 | var oldnew []string 28 | for _, secret := range secrets { 29 | if len(secret.Data) == 0 || secret.Mask == false { 30 | continue 31 | } 32 | name := strings.ToLower(secret.Name) 33 | masked := fmt.Sprintf(maskedf, name) 34 | oldnew = append(oldnew, string(secret.Data)) 35 | oldnew = append(oldnew, masked) 36 | } 37 | if len(oldnew) == 0 { 38 | return w 39 | } 40 | return &Replacer{ 41 | w: w, 42 | r: strings.NewReplacer(oldnew...), 43 | } 44 | } 45 | 46 | // Write writes p to the base writer. The method scans for any 47 | // sensitive data in p and masks before writing. 48 | func (r *Replacer) Write(p []byte) (n int, err error) { 49 | _, err = r.w.Write([]byte(r.r.Replace(string(p)))) 50 | return len(p), err 51 | } 52 | 53 | // Close closes the base writer. 54 | func (r *Replacer) Close() error { 55 | return r.w.Close() 56 | } 57 | -------------------------------------------------------------------------------- /engine/replacer/replacer_test.go: -------------------------------------------------------------------------------- 1 | // Code generated automatically. DO NOT EDIT. 2 | 3 | // Copyright 2019 Drone.IO Inc. All rights reserved. 4 | // Use of this source code is governed by the Polyform License 5 | // that can be found in the LICENSE file. 6 | 7 | package replacer 8 | 9 | import ( 10 | "bytes" 11 | "io" 12 | "testing" 13 | 14 | "github.com/drone-runners/drone-runner-ssh/engine" 15 | ) 16 | 17 | func TestReplace(t *testing.T) { 18 | secrets := []*engine.Secret{ 19 | {Name: "DOCKER_USERNAME", Data: []byte("octocat"), Mask: false}, 20 | {Name: "DOCKER_PASSWORD", Data: []byte("correct-horse-batter-staple"), Mask: true}, 21 | {Name: "DOCKER_EMAIL", Data: []byte(""), Mask: true}, 22 | } 23 | 24 | buf := new(bytes.Buffer) 25 | w := New(&nopCloser{buf}, secrets) 26 | w.Write([]byte("username octocat password correct-horse-batter-staple")) 27 | w.Close() 28 | 29 | if got, want := buf.String(), "username octocat password [secret:docker_password]"; got != want { 30 | t.Errorf("Want masked string %s, got %s", want, got) 31 | } 32 | } 33 | 34 | // this test verifies that if there are no secrets to scan and 35 | // mask, the io.WriteCloser is returned as-is. 36 | func TestReplaceNone(t *testing.T) { 37 | secrets := []*engine.Secret{ 38 | {Name: "DOCKER_USERNAME", Data: []byte("octocat"), Mask: false}, 39 | {Name: "DOCKER_PASSWORD", Data: []byte("correct-horse-batter-staple"), Mask: false}, 40 | } 41 | 42 | buf := new(bytes.Buffer) 43 | w := &nopCloser{buf} 44 | r := New(w, secrets) 45 | if w != r { 46 | t.Errorf("Expect buffer returned with no replacer") 47 | } 48 | } 49 | 50 | type nopCloser struct { 51 | io.Writer 52 | } 53 | 54 | func (*nopCloser) Close() error { 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /engine/resource/lookup.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package resource 6 | 7 | import ( 8 | "errors" 9 | 10 | "github.com/drone/runner-go/manifest" 11 | ) 12 | 13 | // Lookup returns the named pipeline from the Manifest. 14 | func Lookup(name string, manifest *manifest.Manifest) (*Pipeline, error) { 15 | for _, resource := range manifest.Resources { 16 | if resource.GetName() != name { 17 | continue 18 | } 19 | if pipeline, ok := resource.(*Pipeline); ok { 20 | return pipeline, nil 21 | } 22 | } 23 | return nil, errors.New("resource not found") 24 | } 25 | -------------------------------------------------------------------------------- /engine/resource/lookup_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package resource 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/drone/runner-go/manifest" 11 | ) 12 | 13 | func TestLookup(t *testing.T) { 14 | want := &Pipeline{Name: "default"} 15 | m := &manifest.Manifest{ 16 | Resources: []manifest.Resource{want}, 17 | } 18 | got, err := Lookup("default", m) 19 | if err != nil { 20 | t.Error(err) 21 | } 22 | if got != want { 23 | t.Errorf("Expect resource not found error") 24 | } 25 | } 26 | 27 | func TestLookupNotFound(t *testing.T) { 28 | m := &manifest.Manifest{ 29 | Resources: []manifest.Resource{ 30 | &manifest.Secret{ 31 | Kind: "secret", 32 | Name: "password", 33 | }, 34 | // matches name, but is not of kind pipeline 35 | &manifest.Secret{ 36 | Kind: "secret", 37 | Name: "default", 38 | }, 39 | }, 40 | } 41 | _, err := Lookup("default", m) 42 | if err == nil { 43 | t.Errorf("Expect resource not found error") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /engine/resource/parser.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package resource 6 | 7 | import ( 8 | "errors" 9 | 10 | "github.com/drone/runner-go/manifest" 11 | 12 | "github.com/buildkite/yaml" 13 | ) 14 | 15 | func init() { 16 | manifest.Register(parse) 17 | } 18 | 19 | // parse parses the raw resource and returns an Exec pipeline. 20 | func parse(r *manifest.RawResource) (manifest.Resource, bool, error) { 21 | if !match(r) { 22 | return nil, false, nil 23 | } 24 | out := new(Pipeline) 25 | err := yaml.Unmarshal(r.Data, out) 26 | if err != nil { 27 | return out, true, err 28 | } 29 | err = lint(out) 30 | return out, true, err 31 | } 32 | 33 | // match returns true if the resource matches the kind and type. 34 | func match(r *manifest.RawResource) bool { 35 | return r.Kind == Kind && r.Type == Type 36 | } 37 | 38 | // lint returns an error if any pipeline values are invalid. 39 | func lint(pipeline *Pipeline) error { 40 | // ensure server configuration provided. 41 | if pipeline.Server.Host.Value == "" && pipeline.Server.Host.Secret == "" { 42 | return errors.New("Linter: invalid or missing server host") 43 | } 44 | if pipeline.Server.User.Value == "" && pipeline.Server.User.Secret == "" { 45 | return errors.New("Linter: invalid or missing server user") 46 | } 47 | if pipeline.Server.Password.Value == "" && pipeline.Server.Password.Secret == "" && 48 | pipeline.Server.SSHKey.Value == "" && pipeline.Server.SSHKey.Secret == "" { 49 | return errors.New("Linter: invalid or missing server password or ssh_key") 50 | } 51 | 52 | // ensure pipeline steps are not unique. 53 | names := map[string]struct{}{} 54 | for _, step := range pipeline.Steps { 55 | if step.Detach { 56 | return errors.New("Linter: detached steps are not allowed") 57 | } 58 | if step.Name == "" { 59 | return errors.New("Linter: invalid or missing step name") 60 | } 61 | if _, ok := names[step.Name]; ok { 62 | return errors.New("Linter: duplicate step name") 63 | } 64 | names[step.Name] = struct{}{} 65 | } 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /engine/resource/parser_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package resource 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/drone/runner-go/manifest" 11 | 12 | "github.com/google/go-cmp/cmp" 13 | ) 14 | 15 | func TestParse(t *testing.T) { 16 | got, err := manifest.ParseFile("testdata/manifest.yml") 17 | if err != nil { 18 | t.Error(err) 19 | return 20 | } 21 | 22 | want := []manifest.Resource{ 23 | &manifest.Signature{ 24 | Kind: "signature", 25 | Hmac: "a8842634682b78946a2", 26 | }, 27 | &manifest.Secret{ 28 | Kind: "secret", 29 | Type: "encrypted", 30 | Name: "username", 31 | Data: "f0e4c2f76c58916ec25", 32 | }, 33 | &Pipeline{ 34 | Kind: "pipeline", 35 | Type: "ssh", 36 | Name: "default", 37 | Version: "1", 38 | Server: Server{ 39 | Host: manifest.Variable{Value: "localhost"}, 40 | User: manifest.Variable{Value: "root"}, 41 | Password: manifest.Variable{Value: "correct-horse-battery-staple"}, 42 | SSHKey: manifest.Variable{Secret: "private_key"}, 43 | }, 44 | Workspace: manifest.Workspace{ 45 | Path: "/drone/src", 46 | }, 47 | Platform: manifest.Platform{ 48 | OS: "linux", 49 | Arch: "arm64", 50 | }, 51 | Clone: manifest.Clone{ 52 | Depth: 50, 53 | }, 54 | Trigger: manifest.Conditions{ 55 | Branch: manifest.Condition{ 56 | Include: []string{"master"}, 57 | }, 58 | }, 59 | Steps: []*Step{ 60 | { 61 | Name: "build", 62 | Shell: "/bin/sh", 63 | Detach: false, 64 | DependsOn: []string{"clone"}, 65 | Commands: []string{ 66 | "go build", 67 | "go test", 68 | }, 69 | Environment: map[string]*manifest.Variable{ 70 | "GOOS": &manifest.Variable{Value: "linux"}, 71 | "GOARCH": &manifest.Variable{Value: "arm64"}, 72 | }, 73 | Failure: "never", 74 | When: manifest.Conditions{ 75 | Event: manifest.Condition{ 76 | Include: []string{"push"}, 77 | }, 78 | }, 79 | }, 80 | }, 81 | }, 82 | } 83 | 84 | if diff := cmp.Diff(got.Resources, want); diff != "" { 85 | t.Errorf("Unexpected manifest") 86 | t.Log(diff) 87 | } 88 | } 89 | 90 | func TestParseErr(t *testing.T) { 91 | _, err := manifest.ParseFile("testdata/malformed.yml") 92 | if err == nil { 93 | t.Errorf("Expect error when malformed yaml") 94 | } 95 | } 96 | 97 | func TestParseLintErr(t *testing.T) { 98 | _, err := manifest.ParseFile("testdata/linterr.yml") 99 | if err == nil { 100 | t.Errorf("Expect linter returns error") 101 | return 102 | } 103 | } 104 | 105 | func TestParseNoMatch(t *testing.T) { 106 | r := &manifest.RawResource{Kind: "pipeline", Type: "docker"} 107 | _, match, _ := parse(r) 108 | if match { 109 | t.Errorf("Expect no match") 110 | } 111 | } 112 | 113 | func TestMatch(t *testing.T) { 114 | r := &manifest.RawResource{ 115 | Kind: "pipeline", 116 | Type: "ssh", 117 | } 118 | if match(r) == false { 119 | t.Errorf("Expect match, got false") 120 | } 121 | 122 | r = &manifest.RawResource{ 123 | Kind: "approval", 124 | Type: "ssh", 125 | } 126 | if match(r) == true { 127 | t.Errorf("Expect kind mismatch, got true") 128 | } 129 | 130 | r = &manifest.RawResource{ 131 | Kind: "pipeline", 132 | Type: "docker", 133 | } 134 | if match(r) == true { 135 | t.Errorf("Expect type mismatch, got true") 136 | } 137 | 138 | } 139 | 140 | func TestLint(t *testing.T) { 141 | p := new(Pipeline) 142 | p.Server = Server{ 143 | Host: manifest.Variable{Value: "localhost"}, 144 | User: manifest.Variable{Value: "root"}, 145 | Password: manifest.Variable{Value: "root"}, 146 | } 147 | p.Steps = []*Step{{Name: "build"}, {Name: "test"}} 148 | if err := lint(p); err != nil { 149 | t.Errorf("Expect no lint error, got %s", err) 150 | } 151 | 152 | p.Steps = []*Step{{Name: "build"}, {Name: "build"}} 153 | if err := lint(p); err == nil { 154 | t.Errorf("Expect error when duplicate name") 155 | } 156 | 157 | p.Steps = []*Step{{Name: "build"}, {Name: ""}} 158 | if err := lint(p); err == nil { 159 | t.Errorf("Expect error when empty name") 160 | } 161 | 162 | p.Steps = []*Step{{Name: "build", Detach: true}} 163 | if err := lint(p); err == nil { 164 | t.Errorf("Expect error when step detached") 165 | } 166 | } 167 | 168 | func TestLint_ServerError(t *testing.T) { 169 | p := new(Pipeline) 170 | p.Server = Server{ 171 | Host: manifest.Variable{Value: "localhost"}, 172 | User: manifest.Variable{Value: "root"}, 173 | Password: manifest.Variable{Value: "root"}, 174 | } 175 | if err := lint(p); err != nil { 176 | t.Errorf("Expect no lint error, got %s", err) 177 | return 178 | } 179 | 180 | p.Server = Server{ 181 | User: manifest.Variable{Value: "root"}, 182 | Password: manifest.Variable{Value: "root"}, 183 | } 184 | if err := lint(p); err == nil { 185 | t.Errorf("Expect lint error for missing host") 186 | } 187 | 188 | p.Server = Server{ 189 | Host: manifest.Variable{Value: "localhost"}, 190 | Password: manifest.Variable{Value: "root"}, 191 | } 192 | if err := lint(p); err == nil { 193 | t.Errorf("Expect lint error for missing user") 194 | } 195 | 196 | p.Server = Server{ 197 | Host: manifest.Variable{Value: "localhost"}, 198 | User: manifest.Variable{Value: "root"}, 199 | } 200 | if err := lint(p); err == nil { 201 | t.Errorf("Expect lint error for missing passwords") 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /engine/resource/pipeline.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package resource 6 | 7 | import "github.com/drone/runner-go/manifest" 8 | 9 | var ( 10 | _ manifest.Resource = (*Pipeline)(nil) 11 | _ manifest.TriggeredResource = (*Pipeline)(nil) 12 | _ manifest.DependantResource = (*Pipeline)(nil) 13 | _ manifest.PlatformResource = (*Pipeline)(nil) 14 | ) 15 | 16 | // Defines the Resource Kind and Type. 17 | const ( 18 | Kind = "pipeline" 19 | Type = "ssh" 20 | ) 21 | 22 | type ( 23 | // Pipeline is a pipeline resource that executes pipelines 24 | // on the host machine without any virtualization. 25 | Pipeline struct { 26 | Version string `json:"version,omitempty"` 27 | Kind string `json:"kind,omitempty"` 28 | Type string `json:"type,omitempty"` 29 | Name string `json:"name,omitempty"` 30 | Deps []string `json:"depends_on,omitempty"` 31 | Server Server `json:"server,omitempty"` 32 | Clone manifest.Clone `json:"clone,omitempty"` 33 | Platform manifest.Platform `json:"platform,omitempty"` 34 | Trigger manifest.Conditions `json:"conditions,omitempty"` 35 | Workspace manifest.Workspace `json:"workspace,omitempty"` 36 | 37 | Steps []*Step `json:"steps,omitempty"` 38 | } 39 | 40 | // Server defines a remote server. 41 | Server struct { 42 | Host manifest.Variable `json:"host,omitempty"` 43 | User manifest.Variable `json:"user,omitempty"` 44 | Password manifest.Variable `json:"password,omitempty"` 45 | SSHKey manifest.Variable `json:"ssh_key,omitempty" yaml:"ssh_key"` 46 | } 47 | 48 | // Step defines a Pipeline step. 49 | Step struct { 50 | Name string `json:"name,omitempty"` 51 | Shell string `json:"shell,omitempty"` 52 | DependsOn []string `json:"depends_on,omitempty" yaml:"depends_on"` 53 | Detach bool `json:"detach,omitempty"` 54 | Environment map[string]*manifest.Variable `json:"environment,omitempty"` 55 | Failure string `json:"failure,omitempty"` 56 | Commands []string `json:"commands,omitempty"` 57 | When manifest.Conditions `json:"when,omitempty"` 58 | } 59 | ) 60 | 61 | // GetVersion returns the resource version. 62 | func (p *Pipeline) GetVersion() string { return p.Version } 63 | 64 | // GetKind returns the resource kind. 65 | func (p *Pipeline) GetKind() string { return p.Kind } 66 | 67 | // GetType returns the resource type. 68 | func (p *Pipeline) GetType() string { return p.Type } 69 | 70 | // GetName returns the resource name. 71 | func (p *Pipeline) GetName() string { return p.Name } 72 | 73 | // GetDependsOn returns the resource dependencies. 74 | func (p *Pipeline) GetDependsOn() []string { return p.Deps } 75 | 76 | // GetTrigger returns the resource triggers. 77 | func (p *Pipeline) GetTrigger() manifest.Conditions { return p.Trigger } 78 | 79 | // GetPlatform returns the resource platform. 80 | func (p *Pipeline) GetPlatform() manifest.Platform { return p.Platform } 81 | 82 | // GetStep returns the named step. If no step exists with the 83 | // given name, a nil value is returned. 84 | func (p *Pipeline) GetStep(name string) *Step { 85 | for _, step := range p.Steps { 86 | if step.Name == name { 87 | return step 88 | } 89 | } 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /engine/resource/pipeline_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package resource 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/drone/runner-go/manifest" 11 | 12 | "github.com/google/go-cmp/cmp" 13 | ) 14 | 15 | func TestGetStep(t *testing.T) { 16 | step1 := &Step{Name: "build"} 17 | step2 := &Step{Name: "test"} 18 | pipeline := &Pipeline{ 19 | Steps: []*Step{step1, step2}, 20 | } 21 | if pipeline.GetStep("build") != step1 { 22 | t.Errorf("Expected named step") 23 | } 24 | if pipeline.GetStep("deploy") != nil { 25 | t.Errorf("Expected nil step") 26 | } 27 | } 28 | 29 | func TestGetters(t *testing.T) { 30 | platform := manifest.Platform{ 31 | OS: "linux", 32 | Arch: "amd64", 33 | } 34 | trigger := manifest.Conditions{ 35 | Branch: manifest.Condition{ 36 | Include: []string{"master"}, 37 | }, 38 | } 39 | pipeline := &Pipeline{ 40 | Version: "1.0.0", 41 | Kind: "pipeline", 42 | Type: "ssh", 43 | Name: "default", 44 | Deps: []string{"before"}, 45 | Platform: platform, 46 | Trigger: trigger, 47 | } 48 | if got, want := pipeline.GetVersion(), pipeline.Version; got != want { 49 | t.Errorf("Want Version %s, got %s", want, got) 50 | } 51 | if got, want := pipeline.GetKind(), pipeline.Kind; got != want { 52 | t.Errorf("Want Kind %s, got %s", want, got) 53 | } 54 | if got, want := pipeline.GetType(), pipeline.Type; got != want { 55 | t.Errorf("Want Type %s, got %s", want, got) 56 | } 57 | if got, want := pipeline.GetName(), pipeline.Name; got != want { 58 | t.Errorf("Want Name %s, got %s", want, got) 59 | } 60 | if diff := cmp.Diff(pipeline.GetDependsOn(), pipeline.Deps); diff != "" { 61 | t.Errorf("Unexpected Deps") 62 | t.Log(diff) 63 | } 64 | if diff := cmp.Diff(pipeline.GetTrigger(), pipeline.Trigger); diff != "" { 65 | t.Errorf("Unexpected Trigger") 66 | t.Log(diff) 67 | } 68 | if got, want := pipeline.GetPlatform(), pipeline.Platform; got != want { 69 | t.Errorf("Want Platform %s, got %s", want, got) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /engine/resource/testdata/linterr.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | type: ssh 4 | 5 | server: 6 | host: localhost 7 | user: root 8 | password: root 9 | 10 | steps: 11 | - commands: 12 | - go build 13 | - go test 14 | 15 | ... -------------------------------------------------------------------------------- /engine/resource/testdata/malformed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | type: ssh 4 | 5 | steps: 6 | foo: bar 7 | 8 | ... -------------------------------------------------------------------------------- /engine/resource/testdata/manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: signature 3 | hmac: a8842634682b78946a2 4 | 5 | --- 6 | kind: secret 7 | type: encrypted 8 | name: username 9 | data: f0e4c2f76c58916ec25 10 | 11 | --- 12 | kind: pipeline 13 | type: ssh 14 | name: default 15 | version: 1 16 | 17 | platform: 18 | os: linux 19 | arch: arm64 20 | 21 | workspace: 22 | path: /drone/src 23 | 24 | clone: 25 | depth: 50 26 | 27 | server: 28 | host: localhost 29 | user: root 30 | password: correct-horse-battery-staple 31 | ssh_key: 32 | from_secret: private_key 33 | 34 | # server: 35 | # hostname: localhost 36 | # username: root 37 | # password: correct-horse-battery-staple 38 | # identity: 39 | # from_secret: private_key 40 | 41 | steps: 42 | - name: build 43 | shell: /bin/sh 44 | detach: false 45 | failure: never 46 | commands: 47 | - go build 48 | - go test 49 | environment: 50 | GOOS: linux 51 | GOARCH: arm64 52 | depends_on: [ clone ] 53 | when: 54 | event: [ push ] 55 | 56 | trigger: 57 | branch: [ master ] 58 | 59 | ... -------------------------------------------------------------------------------- /engine/resource/testdata/nomatch.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | type: docker 4 | 5 | ... -------------------------------------------------------------------------------- /engine/spec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package engine 6 | 7 | type ( 8 | // Spec provides the pipeline spec. This provides the 9 | // required instructions for reproducable pipeline 10 | // execution. 11 | Spec struct { 12 | Server Server `json:"server,omitempty"` 13 | Platform Platform `json:"platform,omitempty"` 14 | Root string `json:"root,omitempty"` 15 | Files []*File `json:"files,omitempty"` 16 | Steps []*Step `json:"steps,omitempty"` 17 | } 18 | 19 | // Server provides the secret configuration. 20 | Server struct { 21 | Hostname string `json:"hostname,omitempty"` 22 | Username string `json:"username,omitempty"` 23 | Password string `json:"password,omitempty"` 24 | SSHKey string `json:"ssh_key,omitempty"` 25 | } 26 | 27 | // Step defines a pipeline step. 28 | Step struct { 29 | Args []string `json:"args,omitempty"` 30 | Command string `json:"command,omitempty"` 31 | Detach bool `json:"detach,omitempty"` 32 | DependsOn []string `json:"depends_on,omitempty"` 33 | Envs map[string]string `json:"environment,omitempty"` 34 | Files []*File `json:"files,omitempty"` 35 | IgnoreErr bool `json:"ignore_err,omitempty"` 36 | IgnoreStdout bool `json:"ignore_stderr,omitempty"` 37 | IgnoreStderr bool `json:"ignore_stdout,omitempty"` 38 | Name string `json:"name,omitempt"` 39 | RunPolicy RunPolicy `json:"run_policy,omitempty"` 40 | Secrets []*Secret `json:"secrets,omitempty"` 41 | WorkingDir string `json:"working_dir,omitempty"` 42 | } 43 | 44 | // File defines a file that should be uploaded or 45 | // mounted somewhere in the step container or virtual 46 | // machine prior to command execution. 47 | File struct { 48 | Path string `json:"path,omitempty"` 49 | Mode uint32 `json:"mode,omitempty"` 50 | Data []byte `json:"data,omitempty"` 51 | IsDir bool `json:"is_dir,omitempty"` 52 | } 53 | 54 | // Platform defines the target platform. 55 | Platform struct { 56 | OS string `json:"os,omitempty"` 57 | Arch string `json:"arch,omitempty"` 58 | Variant string `json:"variant,omitempty"` 59 | Version string `json:"version,omitempty"` 60 | } 61 | 62 | // Secret represents a secret variable. 63 | Secret struct { 64 | Name string `json:"name,omitempty"` 65 | Env string `json:"env,omitempty"` 66 | Data []byte `json:"data,omitempty"` 67 | Mask bool `json:"mask,omitempty"` 68 | } 69 | 70 | // State represents the process state. 71 | State struct { 72 | ExitCode int // Container exit code 73 | Exited bool // Container exited 74 | OOMKilled bool // Container is oom killed 75 | } 76 | ) 77 | 78 | // RunPolicy defines the policy for starting containers 79 | // based on the point-in-time pass or fail state of 80 | // the pipeline. 81 | type RunPolicy int 82 | 83 | // RunPolicy enumeration. 84 | const ( 85 | RunOnSuccess RunPolicy = iota 86 | RunOnFailure 87 | RunAlways 88 | RunNever 89 | ) 90 | -------------------------------------------------------------------------------- /engine/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package engine 6 | 7 | import ( 8 | "encoding/base64" 9 | "fmt" 10 | "io" 11 | "sort" 12 | ) 13 | 14 | // helper function writes a shell command to the io.Writer that 15 | // changes the current working directory. 16 | func writeWorkdir(w io.Writer, path string) { 17 | fmt.Fprintf(w, "cd %s", path) 18 | fmt.Fprintln(w) 19 | } 20 | 21 | // helper function writes a shell command to the io.Writer that 22 | // exports all secrets as environment variables. 23 | func writeSecrets(w io.Writer, os string, secrets []*Secret) { 24 | for _, s := range secrets { 25 | writeEnv(w, os, s.Env, string(s.Data)) 26 | } 27 | } 28 | 29 | // helper function writes a shell command to the io.Writer that 30 | // exports the key value pairs as environment variables. 31 | func writeEnviron(w io.Writer, os string, envs map[string]string) { 32 | var keys []string 33 | for k := range envs { 34 | keys = append(keys, k) 35 | } 36 | sort.Strings(keys) 37 | for _, k := range keys { 38 | writeEnv(w, os, k, envs[k]) 39 | } 40 | } 41 | 42 | // helper function writes a shell command to the io.Writer that 43 | // exports and key value pair as an environment variable. 44 | func writeEnv(w io.Writer, os, key, value string) { 45 | // we are encoding the value as base64 to avoid any accidental escaping 46 | encodedValue := base64.StdEncoding.EncodeToString([]byte(value)) 47 | switch os { 48 | case "windows": 49 | fmt.Fprintf(w, `$Env:%s = "$([Text.Encoding]::Utf8.GetString([Convert]::FromBase64String('%s')))"`, key, encodedValue) 50 | fmt.Fprintln(w) 51 | default: 52 | fmt.Fprintf(w, `export %s="$(echo %s | base64 -d)"`, key, encodedValue) 53 | fmt.Fprintln(w) 54 | } 55 | } 56 | 57 | // helper function returns a shell command for removing a 58 | // directory that is compatible with the operating system. 59 | func removeCommand(os, path string) string { 60 | switch os { 61 | case "windows": 62 | return fmt.Sprintf("powershell -noprofile -noninteractive -command \"Remove-Item %s -Recurse -Force\"", path) 63 | default: 64 | return fmt.Sprintf("rm -rf %s", path) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /engine/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package engine 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | ) 11 | 12 | func TestWriteWorkdir(t *testing.T) { 13 | buf := new(bytes.Buffer) 14 | writeWorkdir(buf, "/tmp/drone-temp") 15 | 16 | want := "cd /tmp/drone-temp\n" 17 | if got := buf.String(); got != want { 18 | t.Errorf("Want workding dir %q, got %q", want, got) 19 | } 20 | } 21 | 22 | func TestWriteSecrets(t *testing.T) { 23 | buf := new(bytes.Buffer) 24 | sec := []*Secret{{Env: "a", Data: []byte("b")}} 25 | writeSecrets(buf, "linux", sec) 26 | 27 | want := `export a="$(echo Yg== | base64 -d)"` + "\n" 28 | if got := buf.String(); got != want { 29 | t.Errorf("Want secret script %q, got %q", want, got) 30 | } 31 | 32 | buf.Reset() 33 | writeSecrets(buf, "windows", sec) 34 | want = `$Env:a = "$([Text.Encoding]::Utf8.GetString([Convert]::FromBase64String('Yg==')))"` + "\n" 35 | if got := buf.String(); got != want { 36 | t.Errorf("Want secret script %q, got %q", want, got) 37 | } 38 | } 39 | 40 | func TestWriteEnv(t *testing.T) { 41 | buf := new(bytes.Buffer) 42 | env := map[string]string{"a": "b", "c": "d"} 43 | writeEnviron(buf, "linux", env) 44 | 45 | want := `export a="$(echo Yg== | base64 -d)"` + "\n" + `export c="$(echo ZA== | base64 -d)"` + "\n" 46 | if got := buf.String(); got != want { 47 | t.Errorf("Want environment script %q, got %q", want, got) 48 | } 49 | 50 | buf.Reset() 51 | writeEnviron(buf, "windows", env) 52 | want = `$Env:a = "$([Text.Encoding]::Utf8.GetString([Convert]::FromBase64String('Yg==')))"` + "\n" + `$Env:c = "$([Text.Encoding]::Utf8.GetString([Convert]::FromBase64String('ZA==')))"` + "\n" 53 | if got := buf.String(); got != want { 54 | t.Errorf("Want environment script %q, got %q", want, got) 55 | } 56 | } 57 | 58 | func TestRemoveCommand(t *testing.T) { 59 | got := removeCommand("linux", "/tmp/drone-temp") 60 | want := "rm -rf /tmp/drone-temp" 61 | if got != want { 62 | t.Errorf("Want rm script %q, got %q", want, got) 63 | } 64 | 65 | got = removeCommand("windows", `C:\Windows\Temp\Drone-temp`) 66 | want = `powershell -noprofile -noninteractive -command "Remove-Item C:\Windows\Temp\Drone-temp -Recurse -Force"` 67 | if got != want { 68 | t.Errorf("Want rm script %q, got %q", want, got) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/drone-runners/drone-runner-ssh 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect 7 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect 8 | github.com/buildkite/yaml v2.1.0+incompatible 9 | github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 10 | github.com/drone/drone-go v1.7.1 11 | github.com/drone/envsubst v1.0.2 12 | github.com/drone/runner-go v1.3.1 13 | github.com/drone/signal v1.0.0 14 | github.com/google/go-cmp v0.3.0 15 | github.com/gosimple/slug v1.5.0 16 | github.com/hashicorp/go-multierror v1.0.0 17 | github.com/joho/godotenv v1.3.0 18 | github.com/kelseyhightower/envconfig v1.4.0 19 | github.com/kr/fs v0.1.0 // indirect 20 | github.com/mattn/go-isatty v0.0.8 21 | github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 22 | github.com/pkg/errors v0.8.1 // indirect 23 | github.com/pkg/sftp v1.10.1-0.20190613163056-79ae07e7783e 24 | github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect 25 | github.com/sirupsen/logrus v1.9.0 26 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa 27 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 28 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 29 | ) 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d h1:j6oB/WPCigdOkxtuPl1VSIiLpy7Mdsu6phQffbF19Ng= 2 | github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d/go.mod h1:3cARGAK9CfW3HoxCy1a0G4TKrdiKke8ftOMEOHyySYs= 3 | github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e h1:rl2Aq4ZODqTDkeSqQBy+fzpZPamacO1Srp8zq7jf2Sc= 4 | github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY= 5 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= 6 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 7 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= 8 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 9 | github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ= 10 | github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= 11 | github.com/buildkite/yaml v2.1.0+incompatible h1:xirI+ql5GzfikVNDmt+yeiXpf/v1Gt03qXTtT5WXdr8= 12 | github.com/buildkite/yaml v2.1.0+incompatible/go.mod h1:UoU8vbcwu1+vjZq01+KrpSeLBgQQIjL/H7Y6KwikUrI= 13 | github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= 14 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU= 19 | github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= 20 | github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= 21 | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 22 | github.com/drone/drone-go v1.0.5-0.20190504210458-4d6116b897ba h1:GKiT4UPBligLXJAP1zRllHvTUygAAlgS3t9LM9aasp0= 23 | github.com/drone/drone-go v1.0.5-0.20190504210458-4d6116b897ba/go.mod h1:GxyeGClYohaKNYJv/ZpsmVHtMJ7WhoT+uDaJNcDIrk4= 24 | github.com/drone/drone-go v1.7.1 h1:ZX+3Rs8YHUSUQ5mkuMLmm1zr1ttiiE2YGNxF3AnyDKw= 25 | github.com/drone/drone-go v1.7.1/go.mod h1:fxCf9jAnXDZV1yDr0ckTuWd1intvcQwfJmTRpTZ1mXg= 26 | github.com/drone/envsubst v1.0.2 h1:dpYLMAspQHW0a8dZpLRKe9jCNvIGZPhCPrycZzIHdqo= 27 | github.com/drone/envsubst v1.0.2/go.mod h1:bkZbnc/2vh1M12Ecn7EYScpI4YGYU0etwLJICOWi8Z0= 28 | github.com/drone/runner-go v1.3.1 h1:RNLOQOH0EZD0vMT1SDQUPReVOnh1Wbx1D9gQyKH1McI= 29 | github.com/drone/runner-go v1.3.1/go.mod h1:61VgQWhZbNPXp01lBuR7PAztTMySGLnMzK/4oYE3D9Y= 30 | github.com/drone/signal v1.0.0 h1:NrnM2M/4yAuU/tXs6RP1a1ZfxnaHwYkd0kJurA1p6uI= 31 | github.com/drone/signal v1.0.0/go.mod h1:S8t92eFT0g4WUgEc/LxG+LCuiskpMNsG0ajAMGnyZpc= 32 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 33 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 34 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 35 | github.com/gosimple/slug v1.5.0 h1:AIIjgCjHcLpX8LzM2NpG4QGW9kUfqv0OLiFRfPv/H3E= 36 | github.com/gosimple/slug v1.5.0/go.mod h1:ER78kgg1Mv0NQGlXiDe57DpCyfbNywXXZ9mIorhxAf0= 37 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 38 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 39 | github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= 40 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 41 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 42 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 43 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= 44 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 45 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 46 | github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= 47 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 48 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= 49 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 50 | github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 h1:dnMxwus89s86tI8rcGVp2HwZzlz7c5o92VOy7dSckBQ= 51 | github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4/go.mod h1:cojhOHk1gbMeklOyDP2oKKLftefXoJreOQGOrXk+Z38= 52 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 53 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 54 | github.com/pkg/sftp v1.10.1-0.20190613163056-79ae07e7783e h1:OFJvqBwYiN41kBIfsgm7DZhqT3bMljQ4UIr86BcPoVI= 55 | github.com/pkg/sftp v1.10.1-0.20190613163056-79ae07e7783e/go.mod h1:NxmoDg/QLVWluQDUYG7XBZTLUpKeFa8e3aMf1BfjyHk= 56 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 57 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 58 | github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= 59 | github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= 60 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 61 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 62 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 63 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 64 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 65 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 66 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 67 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 68 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 69 | golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 70 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= 71 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 72 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 73 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= 74 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 75 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 76 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 77 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 78 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 79 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 80 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 81 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 82 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 83 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 84 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 85 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 86 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 87 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 88 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 89 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 90 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 91 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 92 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 93 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 94 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 95 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 96 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 97 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 98 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 99 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 100 | -------------------------------------------------------------------------------- /internal/internal.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | // Package internal contains runner internals. 6 | package internal 7 | -------------------------------------------------------------------------------- /internal/match/match.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package match 6 | 7 | import ( 8 | "path/filepath" 9 | 10 | "github.com/drone/drone-go/drone" 11 | ) 12 | 13 | // NOTE most runners do not require match capabilities. This is 14 | // provided as a defense in depth mechanism given the sensitive 15 | // nature of this runner executing code directly on the host. 16 | // The matching function is a last line of defence to prevent 17 | // unauthorized code from running on the host machine. 18 | 19 | // Func returns a new match function that returns true if the 20 | // repository and build do not match the allowd repository names 21 | // and build events. 22 | func Func(repos, events []string, trusted bool) func(*drone.Repo, *drone.Build) bool { 23 | return func(repo *drone.Repo, build *drone.Build) bool { 24 | // if trusted mode is enabled, only match repositories 25 | // that are trusted. 26 | if trusted && repo.Trusted == false { 27 | return false 28 | } 29 | if match(repo.Slug, repos) == false { 30 | return false 31 | } 32 | if match(build.Event, events) == false { 33 | return false 34 | } 35 | return true 36 | } 37 | } 38 | 39 | func match(s string, patterns []string) bool { 40 | // if no matching patterns are defined the string 41 | // is always considered a match. 42 | if len(patterns) == 0 { 43 | return true 44 | } 45 | for _, pattern := range patterns { 46 | if match, _ := filepath.Match(pattern, s); match { 47 | return true 48 | } 49 | } 50 | return false 51 | } 52 | -------------------------------------------------------------------------------- /internal/match/match_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package match 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/drone/drone-go/drone" 11 | ) 12 | 13 | func TestFunc(t *testing.T) { 14 | tests := []struct { 15 | repo string 16 | event string 17 | trusted bool 18 | match bool 19 | matcher func(*drone.Repo, *drone.Build) bool 20 | }{ 21 | // 22 | // Expect match true 23 | // 24 | 25 | // repository, event and trusted flag matching 26 | { 27 | repo: "octocat/hello-world", 28 | event: "push", 29 | trusted: true, 30 | match: true, 31 | matcher: Func([]string{"spaceghost/*", "octocat/*"}, []string{"push"}, true), 32 | }, 33 | // repoisitory matching 34 | { 35 | repo: "octocat/hello-world", 36 | event: "pull_request", 37 | trusted: false, 38 | match: true, 39 | matcher: Func([]string{"spaceghost/*", "octocat/*"}, []string{}, false), 40 | }, 41 | // event matching 42 | { 43 | repo: "octocat/hello-world", 44 | event: "pull_request", 45 | trusted: false, 46 | match: true, 47 | matcher: Func([]string{}, []string{"pull_request"}, false), 48 | }, 49 | // trusted flag matching 50 | { 51 | repo: "octocat/hello-world", 52 | event: "pull_request", 53 | trusted: true, 54 | match: true, 55 | matcher: Func([]string{}, []string{}, true), 56 | }, 57 | 58 | // 59 | // Expect match false 60 | // 61 | 62 | // repository matching 63 | { 64 | repo: "spaceghost/hello-world", 65 | event: "pull_request", 66 | trusted: false, 67 | match: false, 68 | matcher: Func([]string{"octocat/*"}, []string{}, false), 69 | }, 70 | // event matching 71 | { 72 | repo: "octocat/hello-world", 73 | event: "pull_request", 74 | trusted: false, 75 | match: false, 76 | matcher: Func([]string{}, []string{"push"}, false), 77 | }, 78 | // trusted flag matching 79 | { 80 | repo: "octocat/hello-world", 81 | event: "pull_request", 82 | trusted: false, 83 | match: false, 84 | matcher: Func([]string{}, []string{}, true), 85 | }, 86 | // does not match repository 87 | { 88 | repo: "foo/hello-world", 89 | event: "push", 90 | trusted: true, 91 | match: false, 92 | matcher: Func([]string{"spaceghost/*", "octocat/*"}, []string{"push"}, true), 93 | }, 94 | // does not match event 95 | { 96 | repo: "octocat/hello-world", 97 | event: "pull_request", 98 | trusted: true, 99 | match: false, 100 | matcher: Func([]string{"spaceghost/*", "octocat/*"}, []string{"push"}, true), 101 | }, 102 | // does not match trusted flag 103 | { 104 | repo: "octocat/hello-world", 105 | event: "push", 106 | trusted: false, 107 | match: false, 108 | matcher: Func([]string{"spaceghost/*", "octocat/*"}, []string{"push"}, true), 109 | }, 110 | } 111 | 112 | for i, test := range tests { 113 | repo := &drone.Repo{ 114 | Slug: test.repo, 115 | Trusted: test.trusted, 116 | } 117 | build := &drone.Build{ 118 | Event: test.event, 119 | } 120 | match := test.matcher(repo, build) 121 | if match != test.match { 122 | t.Errorf("Expect match %v at index %d", test.match, i) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /internal/mock/mock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package mock 6 | 7 | //go:generate mockgen -package=mock -destination=mock_engine_gen.go github.com/drone-runners/drone-runner-ssh/engine Engine 8 | //go:generate mockgen -package=mock -destination=mock_execer_gen.go github.com/drone-runners/drone-runner-ssh/runtime Execer 9 | -------------------------------------------------------------------------------- /licenses/Polyform-Free-Trial.md: -------------------------------------------------------------------------------- 1 | # Polyform Free Trial License 1.0.0 2 | 3 | 4 | 5 | ## Acceptance 6 | 7 | In order to get any license under these terms, you must agree 8 | to them as both strict obligations and conditions to all 9 | your licenses. 10 | 11 | ## Copyright License 12 | 13 | The licensor grants you a copyright license for the software 14 | to do everything you might do with the software that would 15 | otherwise infringe the licensor's copyright in it for any 16 | permitted purpose. However, you may only make changes or 17 | new works based on the software according to [Changes and New 18 | Works License](#changes-and-new-works-license), and you may 19 | not distribute copies of the software. 20 | 21 | ## Changes and New Works License 22 | 23 | The licensor grants you an additional copyright license to 24 | make changes and new works based on the software for any 25 | permitted purpose. 26 | 27 | ## Patent License 28 | 29 | The licensor grants you a patent license for the software that 30 | covers patent claims the licensor can license, or becomes able 31 | to license, that you would infringe by using the software. 32 | 33 | ## Fair Use 34 | 35 | You may have "fair use" rights for the software under the 36 | law. These terms do not limit them. 37 | 38 | ## Free Trial 39 | 40 | Use to evaluate whether the software suits a particular 41 | application for less than 32 consecutive calendar days, on 42 | behalf of you or your company, is use for a permitted purpose. 43 | 44 | ## No Other Rights 45 | 46 | These terms do not allow you to sublicense or transfer any of 47 | your licenses to anyone else, or prevent the licensor from 48 | granting licenses to anyone else. These terms do not imply 49 | any other licenses. 50 | 51 | ## Patent Defense 52 | 53 | If you make any written claim that the software infringes or 54 | contributes to infringement of any patent, your patent license 55 | for the software granted under these terms ends immediately. If 56 | your company makes such a claim, your patent license ends 57 | immediately for work on behalf of your company. 58 | 59 | ## Violations 60 | 61 | If you violate any of these terms, or do anything with the 62 | software not covered by your licenses, all your licenses 63 | end immediately. 64 | 65 | ## No Liability 66 | 67 | ***As far as the law allows, the software comes as is, without 68 | any warranty or condition, and the licensor will not be liable 69 | to you for any damages arising out of these terms or the use 70 | or nature of the software, under any kind of legal claim.*** 71 | 72 | ## Definitions 73 | 74 | The **licensor** is the individual or entity offering these 75 | terms, and the **software** is the software the licensor makes 76 | available under these terms. 77 | 78 | **You** refers to the individual or entity agreeing to these 79 | terms. 80 | 81 | **Your company** is any legal entity, sole proprietorship, 82 | or other kind of organization that you work for, plus all 83 | organizations that have control over, are under the control of, 84 | or are under common control with that organization. **Control** 85 | means ownership of substantially all the assets of an entity, 86 | or the power to direct its management and policies by vote, 87 | contract, or otherwise. Control can be direct or indirect. 88 | 89 | **Your licenses** are all the licenses granted to you for the 90 | software under these terms. 91 | 92 | **Use** means anything you do with the software requiring one 93 | of your licenses. -------------------------------------------------------------------------------- /licenses/Polyform-Noncommercial.md: -------------------------------------------------------------------------------- 1 | # Polyform Noncommercial License 1.0.0 2 | 3 | 4 | 5 | ## Acceptance 6 | 7 | In order to get any license under these terms, you must agree 8 | to them as both strict obligations and conditions to all 9 | your licenses. 10 | 11 | ## Copyright License 12 | 13 | The licensor grants you a copyright license for the 14 | software to do everything you might do with the software 15 | that would otherwise infringe the licensor's copyright 16 | in it for any permitted purpose. However, you may 17 | only distribute the software according to [Distribution 18 | License](#distribution-license) and make changes or new works 19 | based on the software according to [Changes and New Works 20 | License](#changes-and-new-works-license). 21 | 22 | ## Distribution License 23 | 24 | The licensor grants you an additional copyright license 25 | to distribute copies of the software. Your license 26 | to distribute covers distributing the software with 27 | changes and new works permitted by [Changes and New Works 28 | License](#changes-and-new-works-license). 29 | 30 | ## Notices 31 | 32 | You must ensure that anyone who gets a copy of any part of 33 | the software from you also gets a copy of these terms or the 34 | URL for them above, as well as copies of any plain-text lines 35 | beginning with `Required Notice:` that the licensor provided 36 | with the software. For example: 37 | 38 | > Required Notice: Copyright Yoyodyne, Inc. (http://example.com) 39 | 40 | ## Changes and New Works License 41 | 42 | The licensor grants you an additional copyright license to 43 | make changes and new works based on the software for any 44 | permitted purpose. 45 | 46 | ## Patent License 47 | 48 | The licensor grants you a patent license for the software that 49 | covers patent claims the licensor can license, or becomes able 50 | to license, that you would infringe by using the software. 51 | 52 | ## Noncommercial Purposes 53 | 54 | Any noncommercial purpose is a permitted purpose. 55 | 56 | ## Personal Uses 57 | 58 | Personal use for research, experiment, and testing for 59 | the benefit of public knowledge, personal study, private 60 | entertainment, hobby projects, amateur pursuits, or religious 61 | observance, without any anticipated commercial application, 62 | is use for a permitted purpose. 63 | 64 | ## Noncommercial Organizations 65 | 66 | Use by any charitable organization, educational institution, 67 | public research organization, public safety or health 68 | organization, environmental protection organization, 69 | or government institution is use for a permitted purpose 70 | regardless of the source of funding or obligations resulting 71 | from the funding. 72 | 73 | ## Fair Use 74 | 75 | You may have "fair use" rights for the software under the 76 | law. These terms do not limit them. 77 | 78 | ## No Other Rights 79 | 80 | These terms do not allow you to sublicense or transfer any of 81 | your licenses to anyone else, or prevent the licensor from 82 | granting licenses to anyone else. These terms do not imply 83 | any other licenses. 84 | 85 | ## Patent Defense 86 | 87 | If you make any written claim that the software infringes or 88 | contributes to infringement of any patent, your patent license 89 | for the software granted under these terms ends immediately. If 90 | your company makes such a claim, your patent license ends 91 | immediately for work on behalf of your company. 92 | 93 | ## Violations 94 | 95 | The first time you are notified in writing that you have 96 | violated any of these terms, or done anything with the software 97 | not covered by your licenses, your licenses can nonetheless 98 | continue if you come into full compliance with these terms, 99 | and take practical steps to correct past violations, within 100 | 32 days of receiving notice. Otherwise, all your licenses 101 | end immediately. 102 | 103 | ## No Liability 104 | 105 | ***As far as the law allows, the software comes as is, without 106 | any warranty or condition, and the licensor will not be liable 107 | to you for any damages arising out of these terms or the use 108 | or nature of the software, under any kind of legal claim.*** 109 | 110 | ## Definitions 111 | 112 | The **licensor** is the individual or entity offering these 113 | terms, and the **software** is the software the licensor makes 114 | available under these terms. 115 | 116 | **You** refers to the individual or entity agreeing to these 117 | terms. 118 | 119 | **Your company** is any legal entity, sole proprietorship, 120 | or other kind of organization that you work for, plus all 121 | organizations that have control over, are under the control of, 122 | or are under common control with that organization. **Control** 123 | means ownership of substantially all the assets of an entity, 124 | or the power to direct its management and policies by vote, 125 | contract, or otherwise. Control can be direct or indirect. 126 | 127 | **Your licenses** are all the licenses granted to you for the 128 | software under these terms. 129 | 130 | **Use** means anything you do with the software requiring one 131 | of your licenses. -------------------------------------------------------------------------------- /licenses/Polyform-Small-Business.md: -------------------------------------------------------------------------------- 1 | # Polyform Small Business License 1.0.0 2 | 3 | 4 | 5 | ## Acceptance 6 | 7 | In order to get any license under these terms, you must agree 8 | to them as both strict obligations and conditions to all 9 | your licenses. 10 | 11 | ## Copyright License 12 | 13 | The licensor grants you a copyright license for the 14 | software to do everything you might do with the software 15 | that would otherwise infringe the licensor's copyright 16 | in it for any permitted purpose. However, you may 17 | only distribute the software according to [Distribution 18 | License](#distribution-license) and make changes or new works 19 | based on the software according to [Changes and New Works 20 | License](#changes-and-new-works-license). 21 | 22 | ## Distribution License 23 | 24 | The licensor grants you an additional copyright license 25 | to distribute copies of the software. Your license 26 | to distribute covers distributing the software with 27 | changes and new works permitted by [Changes and New Works 28 | License](#changes-and-new-works-license). 29 | 30 | ## Notices 31 | 32 | You must ensure that anyone who gets a copy of any part of 33 | the software from you also gets a copy of these terms or the 34 | URL for them above, as well as copies of any plain-text lines 35 | beginning with `Required Notice:` that the licensor provided 36 | with the software. For example: 37 | 38 | > Required Notice: Copyright Yoyodyne, Inc. (http://example.com) 39 | 40 | ## Changes and New Works License 41 | 42 | The licensor grants you an additional copyright license to 43 | make changes and new works based on the software for any 44 | permitted purpose. 45 | 46 | ## Patent License 47 | 48 | The licensor grants you a patent license for the software that 49 | covers patent claims the licensor can license, or becomes able 50 | to license, that you would infringe by using the software. 51 | 52 | ## Fair Use 53 | 54 | You may have "fair use" rights for the software under the 55 | law. These terms do not limit them. 56 | 57 | ## Small Business 58 | 59 | Use of the software for the benefit of your company is use for 60 | a permitted purpose if your company has fewer than 100 total 61 | individuals working as employees and independent contractors, 62 | and less than 1,000,000 USD (2019) total revenue in the prior 63 | tax year. Adjust this revenue threshold for inflation according 64 | to the United States Bureau of Labor Statistics' consumer price 65 | index for all urban consumers, U.S. city average, for all items, 66 | not seasonally adjusted, with 1982–1984=100 reference base. 67 | 68 | ## No Other Rights 69 | 70 | These terms do not allow you to sublicense or transfer any of 71 | your licenses to anyone else, or prevent the licensor from 72 | granting licenses to anyone else. These terms do not imply 73 | any other licenses. 74 | 75 | ## Patent Defense 76 | 77 | If you make any written claim that the software infringes or 78 | contributes to infringement of any patent, your patent license 79 | for the software granted under these terms ends immediately. If 80 | your company makes such a claim, your patent license ends 81 | immediately for work on behalf of your company. 82 | 83 | ## Violations 84 | 85 | The first time you are notified in writing that you have 86 | violated any of these terms, or done anything with the software 87 | not covered by your licenses, your licenses can nonetheless 88 | continue if you come into full compliance with these terms, 89 | and take practical steps to correct past violations, within 90 | 32 days of receiving notice. Otherwise, all your licenses 91 | end immediately. 92 | 93 | ## No Liability 94 | 95 | ***As far as the law allows, the software comes as is, without 96 | any warranty or condition, and the licensor will not be liable 97 | to you for any damages arising out of these terms or the use 98 | or nature of the software, under any kind of legal claim.*** 99 | 100 | ## Definitions 101 | 102 | The **licensor** is the individual or entity offering these 103 | terms, and the **software** is the software the licensor makes 104 | available under these terms. 105 | 106 | **You** refers to the individual or entity agreeing to these 107 | terms. 108 | 109 | **Your company** is any legal entity, sole proprietorship, 110 | or other kind of organization that you work for, plus all 111 | organizations that have control over, are under the control of, 112 | or are under common control with that organization. **Control** 113 | means ownership of substantially all the assets of an entity, 114 | or the power to direct its management and policies by vote, 115 | contract, or otherwise. Control can be direct or indirect. 116 | 117 | **Your licenses** are all the licenses granted to you for the 118 | software under these terms. 119 | 120 | **Use** means anything you do with the software requiring one 121 | of your licenses. 122 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Drone.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Polyform License 3 | // that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "github.com/drone-runners/drone-runner-ssh/command" 9 | _ "github.com/joho/godotenv/autoload" 10 | ) 11 | 12 | func main() { 13 | command.Command() 14 | } 15 | -------------------------------------------------------------------------------- /runtime/execer.go: -------------------------------------------------------------------------------- 1 | // Code generated automatically. DO NOT EDIT. 2 | 3 | // Copyright 2019 Drone.IO Inc. All rights reserved. 4 | // Use of this source code is governed by the Polyform License 5 | // that can be found in the LICENSE file. 6 | 7 | package runtime 8 | 9 | import ( 10 | "context" 11 | "sync" 12 | 13 | "github.com/drone-runners/drone-runner-ssh/engine" 14 | "github.com/drone-runners/drone-runner-ssh/engine/replacer" 15 | "github.com/drone/drone-go/drone" 16 | "github.com/drone/runner-go/environ" 17 | "github.com/drone/runner-go/logger" 18 | "github.com/drone/runner-go/pipeline" 19 | 20 | "github.com/hashicorp/go-multierror" 21 | "github.com/natessilva/dag" 22 | "golang.org/x/sync/semaphore" 23 | ) 24 | 25 | // Execer is the execution context for executing the intermediate 26 | // representation of a pipeline. 27 | type Execer interface { 28 | Exec(context.Context, *engine.Spec, *pipeline.State) error 29 | } 30 | 31 | type execer struct { 32 | mu sync.Mutex 33 | engine engine.Engine 34 | reporter pipeline.Reporter 35 | streamer pipeline.Streamer 36 | sem *semaphore.Weighted 37 | } 38 | 39 | // NewExecer returns a new execer used 40 | func NewExecer( 41 | reporter pipeline.Reporter, 42 | streamer pipeline.Streamer, 43 | engine engine.Engine, 44 | procs int64, 45 | ) Execer { 46 | exec := &execer{ 47 | reporter: reporter, 48 | streamer: streamer, 49 | engine: engine, 50 | } 51 | if procs > 0 { 52 | // optional semaphor that limits the number of steps 53 | // that can execute concurrently. 54 | exec.sem = semaphore.NewWeighted(procs) 55 | } 56 | return exec 57 | } 58 | 59 | // Exec executes the intermediate representation of the pipeline 60 | // and returns an error if execution fails. 61 | func (e *execer) Exec(ctx context.Context, spec *engine.Spec, state *pipeline.State) error { 62 | defer e.engine.Destroy(noContext, spec) 63 | 64 | if err := e.engine.Setup(noContext, spec); err != nil { 65 | state.FailAll(err) 66 | return e.reporter.ReportStage(noContext, state) 67 | } 68 | 69 | // create a directed graph, where each vertex in the graph 70 | // is a pipeline step. 71 | var d dag.Runner 72 | for _, s := range spec.Steps { 73 | step := s 74 | d.AddVertex(step.Name, func() error { 75 | return e.exec(ctx, state, spec, step) 76 | }) 77 | } 78 | 79 | // create the vertex edges from the values configured in the 80 | // depends_on attribute. 81 | for _, s := range spec.Steps { 82 | for _, dep := range s.DependsOn { 83 | d.AddEdge(dep, s.Name) 84 | } 85 | } 86 | 87 | var result error 88 | if err := d.Run(); err != nil { 89 | multierror.Append(result, err) 90 | } 91 | 92 | // once pipeline execution completes, notify the state 93 | // manageer that all steps are finished. 94 | state.FinishAll() 95 | if err := e.reporter.ReportStage(noContext, state); err != nil { 96 | multierror.Append(result, err) 97 | } 98 | return result 99 | } 100 | 101 | func (e *execer) exec(ctx context.Context, state *pipeline.State, spec *engine.Spec, step *engine.Step) error { 102 | var result error 103 | 104 | select { 105 | case <-ctx.Done(): 106 | state.Cancel() 107 | return nil 108 | default: 109 | } 110 | 111 | log := logger.FromContext(ctx) 112 | log = log.WithField("step.name", step.Name) 113 | ctx = logger.WithContext(ctx, log) 114 | 115 | if e.sem != nil { 116 | // the semaphore limits the number of steps that can run 117 | // concurrently. acquire the semaphore and release when 118 | // the pipeline completes. 119 | if err := e.sem.Acquire(ctx, 1); err != nil { 120 | return nil 121 | } 122 | 123 | defer func() { 124 | // recover from a panic to ensure the semaphore is 125 | // released to prevent deadlock. we do not expect a 126 | // panic, however, we are being overly cautious. 127 | if r := recover(); r != nil { 128 | // TODO(bradrydzewsi) log the panic. 129 | } 130 | // release the semaphore 131 | e.sem.Release(1) 132 | }() 133 | } 134 | 135 | switch { 136 | case state.Skipped(): 137 | return nil 138 | case state.Cancelled(): 139 | return nil 140 | case step.RunPolicy == engine.RunNever: 141 | return nil 142 | case step.RunPolicy == engine.RunAlways: 143 | break 144 | case step.RunPolicy == engine.RunOnFailure && state.Failed() == false: 145 | state.Skip(step.Name) 146 | return e.reporter.ReportStep(noContext, state, step.Name) 147 | case step.RunPolicy == engine.RunOnSuccess && state.Failed(): 148 | state.Skip(step.Name) 149 | return e.reporter.ReportStep(noContext, state, step.Name) 150 | } 151 | 152 | state.Start(step.Name) 153 | err := e.reporter.ReportStep(noContext, state, step.Name) 154 | if err != nil { 155 | return err 156 | } 157 | 158 | copy := cloneStep(step) 159 | 160 | // the pipeline environment variables need to be updated to 161 | // reflect the current state of the build and stage. 162 | state.Lock() 163 | copy.Envs = environ.Combine( 164 | copy.Envs, 165 | environ.Build(state.Build), 166 | environ.Stage(state.Stage), 167 | environ.Step(findStep(state, step.Name)), 168 | ) 169 | state.Unlock() 170 | 171 | // writer used to stream build logs. 172 | wc := e.streamer.Stream(noContext, state, step.Name) 173 | wc = replacer.New(wc, step.Secrets) 174 | 175 | // if the step is configured as a daemon, it is detached 176 | // from the main process and executed separately. 177 | // todo(bradrydzewski) this code is still experimental. 178 | if step.Detach { 179 | go func() { 180 | e.engine.Run(ctx, spec, copy, wc) 181 | wc.Close() 182 | }() 183 | return nil 184 | } 185 | 186 | exited, err := e.engine.Run(ctx, spec, copy, wc) 187 | 188 | // close the stream. If the session is a remote session, the 189 | // full log buffer is uploaded to the remote server. 190 | if err := wc.Close(); err != nil { 191 | multierror.Append(result, err) 192 | } 193 | 194 | if exited != nil { 195 | state.Finish(step.Name, exited.ExitCode) 196 | err := e.reporter.ReportStep(noContext, state, step.Name) 197 | if err != nil { 198 | multierror.Append(result, err) 199 | } 200 | // if the exit code is 78 the system will skip all 201 | // subsequent pending steps in the pipeline. 202 | if exited.ExitCode == 78 { 203 | state.SkipAll() 204 | } 205 | return result 206 | } 207 | 208 | switch err { 209 | case context.Canceled, context.DeadlineExceeded: 210 | state.Cancel() 211 | return nil 212 | } 213 | 214 | // if the step failed with an internal error (as oppsed to a 215 | // runtime error) the step is failed. 216 | state.Fail(step.Name, err) 217 | err = e.reporter.ReportStep(noContext, state, step.Name) 218 | if err != nil { 219 | multierror.Append(result, err) 220 | } 221 | return result 222 | } 223 | 224 | // helper function to clone a step. The runner mutates a step to 225 | // update the environment variables to reflect the current 226 | // pipeline state. 227 | func cloneStep(src *engine.Step) *engine.Step { 228 | dst := new(engine.Step) 229 | *dst = *src 230 | dst.Envs = environ.Combine(src.Envs) 231 | return dst 232 | } 233 | 234 | // helper function returns the named step from the state. 235 | func findStep(state *pipeline.State, name string) *drone.Step { 236 | for _, step := range state.Stage.Steps { 237 | if step.Name == name { 238 | return step 239 | } 240 | } 241 | panic("step not found: " + name) 242 | } 243 | -------------------------------------------------------------------------------- /runtime/execer_test.go: -------------------------------------------------------------------------------- 1 | // Code generated automatically. DO NOT EDIT. 2 | 3 | // Copyright 2019 Drone.IO Inc. All rights reserved. 4 | // Use of this source code is governed by the Polyform License 5 | // that can be found in the LICENSE file. 6 | 7 | package runtime 8 | 9 | import ( 10 | "testing" 11 | ) 12 | 13 | func TestExec(t *testing.T) { 14 | t.Skip() 15 | } 16 | 17 | func TestExec_NonZeroExit(t *testing.T) { 18 | t.Skip() 19 | } 20 | 21 | func TestExec_Exit78(t *testing.T) { 22 | t.Skip() 23 | } 24 | 25 | func TestExec_Error(t *testing.T) { 26 | t.Skip() 27 | } 28 | 29 | func TestExec_CtxError(t *testing.T) { 30 | t.Skip() 31 | } 32 | 33 | func TestExec_ReportError(t *testing.T) { 34 | t.Skip() 35 | } 36 | 37 | func TestExec_SkipCtxDone(t *testing.T) { 38 | t.Skip() 39 | } 40 | -------------------------------------------------------------------------------- /runtime/poller.go: -------------------------------------------------------------------------------- 1 | // Code generated automatically. DO NOT EDIT. 2 | 3 | // Copyright 2019 Drone.IO Inc. All rights reserved. 4 | // Use of this source code is governed by the Polyform License 5 | // that can be found in the LICENSE file. 6 | 7 | package runtime 8 | 9 | import ( 10 | "context" 11 | "sync" 12 | 13 | "github.com/drone/runner-go/client" 14 | "github.com/drone/runner-go/logger" 15 | ) 16 | 17 | var noContext = context.Background() 18 | 19 | // Poller polls the server for pending stages and dispatches 20 | // for execution by the Runner. 21 | type Poller struct { 22 | Client client.Client 23 | Filter *client.Filter 24 | Runner *Runner 25 | } 26 | 27 | // Poll opens N connections to the server to poll for pending 28 | // stages for execution. Pending stages are dispatched to a 29 | // Runner for execution. 30 | func (p *Poller) Poll(ctx context.Context, n int) { 31 | var wg sync.WaitGroup 32 | for i := 0; i < n; i++ { 33 | wg.Add(1) 34 | go func(i int) { 35 | for { 36 | select { 37 | case <-ctx.Done(): 38 | wg.Done() 39 | return 40 | default: 41 | p.poll(ctx, i+1) 42 | } 43 | } 44 | }(i) 45 | } 46 | 47 | wg.Wait() 48 | } 49 | 50 | // poll requests a stage for execution from the server, and then 51 | // dispatches for execution. 52 | func (p *Poller) poll(ctx context.Context, thread int) error { 53 | log := logger.FromContext(ctx).WithField("thread", thread) 54 | log.WithField("thread", thread).Debug("request stage from remote server") 55 | 56 | // request a new build stage for execution from the central 57 | // build server. 58 | stage, err := p.Client.Request(ctx, p.Filter) 59 | if err == context.Canceled || err == context.DeadlineExceeded { 60 | log.WithError(err).Trace("no stage returned") 61 | return nil 62 | } 63 | if err != nil { 64 | log.WithError(err).Error("cannot request stage") 65 | return err 66 | } 67 | 68 | // exit if a nil or empty stage is returned from the system 69 | // and allow the runner to retry. 70 | if stage == nil || stage.ID == 0 { 71 | return nil 72 | } 73 | 74 | return p.Runner.Run( 75 | logger.WithContext(noContext, log), stage) 76 | } 77 | -------------------------------------------------------------------------------- /runtime/poller_test.go: -------------------------------------------------------------------------------- 1 | // Code generated automatically. DO NOT EDIT. 2 | 3 | // Copyright 2019 Drone.IO Inc. All rights reserved. 4 | // Use of this source code is governed by the Polyform License 5 | // that can be found in the LICENSE file. 6 | 7 | package runtime 8 | 9 | import ( 10 | "testing" 11 | ) 12 | 13 | func TestPoll(t *testing.T) { 14 | t.Skip() 15 | } 16 | 17 | func TestPoll_NilStage(t *testing.T) { 18 | t.Skip() 19 | } 20 | 21 | func TestPoll_EmptyStage(t *testing.T) { 22 | t.Skip() 23 | } 24 | 25 | func TestPoll_RequestError(t *testing.T) { 26 | t.Skip() 27 | } 28 | -------------------------------------------------------------------------------- /runtime/runner.go: -------------------------------------------------------------------------------- 1 | // Code generated automatically. DO NOT EDIT. 2 | 3 | // Copyright 2019 Drone.IO Inc. All rights reserved. 4 | // Use of this source code is governed by the Polyform License 5 | // that can be found in the LICENSE file. 6 | 7 | package runtime 8 | 9 | import ( 10 | "context" 11 | "errors" 12 | "fmt" 13 | "strings" 14 | "time" 15 | 16 | "github.com/drone-runners/drone-runner-ssh/engine" 17 | "github.com/drone-runners/drone-runner-ssh/engine/compiler" 18 | "github.com/drone-runners/drone-runner-ssh/engine/resource" 19 | 20 | "github.com/drone/drone-go/drone" 21 | "github.com/drone/envsubst" 22 | "github.com/drone/runner-go/client" 23 | "github.com/drone/runner-go/environ" 24 | "github.com/drone/runner-go/logger" 25 | "github.com/drone/runner-go/manifest" 26 | "github.com/drone/runner-go/pipeline" 27 | "github.com/drone/runner-go/secret" 28 | ) 29 | 30 | // Runnner runs the pipeline. 31 | type Runner struct { 32 | // Client is the remote client responsible for interacting 33 | // with the central server. 34 | Client client.Client 35 | 36 | // Execer is responsible for executing intermediate 37 | // representation of the pipeline and returns its results. 38 | Execer Execer 39 | 40 | // Reporter reports pipeline status back to the remote 41 | // server. 42 | Reporter pipeline.Reporter 43 | 44 | // Environ provides custom, global environment variables 45 | // that are added to every pipeline step. 46 | Environ map[string]string 47 | 48 | // Machine provides the runner with the name of the host 49 | // machine executing the pipeline. 50 | Machine string 51 | 52 | // Match is an optional function that returns true if the 53 | // repository or build match user-defined criteria. This is 54 | // intended as a security measure to prevent a runner from 55 | // processing an unwanted pipeline. 56 | Match func(*drone.Repo, *drone.Build) bool 57 | 58 | // Secret provides the compiler with secrets. 59 | Secret secret.Provider 60 | } 61 | 62 | // Run runs the pipeline stage. 63 | func (s *Runner) Run(ctx context.Context, stage *drone.Stage) error { 64 | log := logger.FromContext(ctx). 65 | WithField("stage.id", stage.ID). 66 | WithField("stage.name", stage.Name). 67 | WithField("stage.number", stage.Number) 68 | 69 | log.Debug("stage received") 70 | 71 | // delivery to a single agent is not guaranteed, which means 72 | // we need confirm receipt. The first agent that confirms 73 | // receipt of the stage can assume ownership. 74 | 75 | stage.Machine = s.Machine 76 | err := s.Client.Accept(ctx, stage) 77 | if err != nil { 78 | log.WithError(err).Error("cannot accept stage") 79 | return err 80 | } 81 | 82 | log.Debug("stage accepted") 83 | 84 | data, err := s.Client.Detail(ctx, stage) 85 | if err != nil { 86 | log.WithError(err).Error("cannot get stage details") 87 | return err 88 | } 89 | 90 | log = log.WithField("repo.id", data.Repo.ID). 91 | WithField("repo.namespace", data.Repo.Namespace). 92 | WithField("repo.name", data.Repo.Name). 93 | WithField("build.id", data.Build.ID). 94 | WithField("build.number", data.Build.Number) 95 | 96 | log.Debug("stage details fetched") 97 | 98 | ctxdone, cancel := context.WithCancel(ctx) 99 | defer cancel() 100 | 101 | timeout := time.Duration(data.Repo.Timeout) * time.Minute 102 | ctxtimeout, cancel := context.WithTimeout(ctxdone, timeout) 103 | defer cancel() 104 | 105 | ctxcancel, cancel := context.WithCancel(ctxtimeout) 106 | defer cancel() 107 | 108 | // next we opens a connection to the server to watch for 109 | // cancellation requests. If a build is cancelled the running 110 | // stage should also be cancelled. 111 | go func() { 112 | done, _ := s.Client.Watch(ctxdone, data.Build.ID) 113 | if done { 114 | cancel() 115 | log.Debugln("received cancellation") 116 | } else { 117 | log.Debugln("done listening for cancellations") 118 | } 119 | }() 120 | 121 | envs := environ.Combine( 122 | s.Environ, 123 | environ.System(data.System), 124 | environ.Repo(data.Repo), 125 | environ.Build(data.Build), 126 | environ.Stage(stage), 127 | environ.Link(data.Repo, data.Build, data.System), 128 | data.Build.Params, 129 | ) 130 | 131 | // string substitution function ensures that string 132 | // replacement variables are escaped and quoted if they 133 | // contain a newline character. 134 | subf := func(k string) string { 135 | v := envs[k] 136 | if strings.Contains(v, "\n") { 137 | v = fmt.Sprintf("%q", v) 138 | } 139 | return v 140 | } 141 | 142 | state := &pipeline.State{ 143 | Build: data.Build, 144 | Stage: stage, 145 | Repo: data.Repo, 146 | System: data.System, 147 | } 148 | 149 | // evaluates whether or not the agent can process the 150 | // pipeline. An agent may choose to reject a repository 151 | // or build for security reasons. 152 | if s.Match != nil && s.Match(data.Repo, data.Build) == false { 153 | log.Error("cannot process stage, access denied") 154 | state.FailAll(errors.New("insufficient permission to run the pipeline")) 155 | return s.Reporter.ReportStage(noContext, state) 156 | } 157 | 158 | // evaluates string replacement expressions and returns an 159 | // update configuration file string. 160 | config, err := envsubst.Eval(string(data.Config.Data), subf) 161 | if err != nil { 162 | log.WithError(err).Error("cannot emulate bash substitution") 163 | state.FailAll(err) 164 | return s.Reporter.ReportStage(noContext, state) 165 | } 166 | 167 | // parse the yaml configuration file. 168 | manifest, err := manifest.ParseString(config) 169 | if err != nil { 170 | log.WithError(err).Error("cannot parse configuration file") 171 | state.FailAll(err) 172 | return s.Reporter.ReportStage(noContext, state) 173 | } 174 | 175 | // find the named stage in the yaml configuration file. 176 | resource, err := resource.Lookup(stage.Name, manifest) 177 | if err != nil { 178 | log.WithError(err).Error("cannot find pipeline resource") 179 | state.FailAll(err) 180 | return s.Reporter.ReportStage(noContext, state) 181 | } 182 | 183 | secrets := secret.Combine( 184 | secret.Static(data.Secrets), 185 | secret.Encrypted(), 186 | s.Secret, 187 | ) 188 | 189 | // compile the yaml configuration file to an intermediate 190 | // representation, and then 191 | comp := &compiler.Compiler{ 192 | Pipeline: resource, 193 | Manifest: manifest, 194 | Environ: s.Environ, 195 | Build: data.Build, 196 | Stage: stage, 197 | Repo: data.Repo, 198 | System: data.System, 199 | Netrc: data.Netrc, 200 | Secret: secrets, 201 | } 202 | 203 | spec := comp.Compile(ctx) 204 | for _, src := range spec.Steps { 205 | // steps that are skipped are ignored and are not stored 206 | // in the drone database, nor displayed in the UI. 207 | if src.RunPolicy == engine.RunNever { 208 | continue 209 | } 210 | stage.Steps = append(stage.Steps, &drone.Step{ 211 | Name: src.Name, 212 | Number: len(stage.Steps) + 1, 213 | StageID: stage.ID, 214 | Status: drone.StatusPending, 215 | ErrIgnore: src.IgnoreErr, 216 | }) 217 | } 218 | 219 | stage.Started = time.Now().Unix() 220 | stage.Status = drone.StatusRunning 221 | if err := s.Client.Update(ctx, stage); err != nil { 222 | log.WithError(err).Error("cannot update stage") 223 | return err 224 | } 225 | 226 | log.Debug("updated stage to running") 227 | 228 | ctxcancel = logger.WithContext(ctxcancel, log) 229 | err = s.Execer.Exec(ctxcancel, spec, state) 230 | if err != nil { 231 | log.WithError(err).Debug("stage failed") 232 | return err 233 | } 234 | log.Debug("updated stage to complete") 235 | return nil 236 | } 237 | -------------------------------------------------------------------------------- /runtime/runner_test.go: -------------------------------------------------------------------------------- 1 | // Code generated automatically. DO NOT EDIT. 2 | 3 | // Copyright 2019 Drone.IO Inc. All rights reserved. 4 | // Use of this source code is governed by the Polyform License 5 | // that can be found in the LICENSE file. 6 | 7 | package runtime 8 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # disable go modules 4 | export GOPATH="" 5 | 6 | # disable cgo 7 | export CGO_ENABLED=0 8 | 9 | set -e 10 | set -x 11 | 12 | # linux 13 | GOOS=linux GOARCH=amd64 go build -o release/linux/amd64/drone-runner-ssh 14 | GOOS=linux GOARCH=arm64 go build -o release/linux/arm64/drone-runner-ssh 15 | GOOS=linux GOARCH=arm go build -o release/linux/arm/drone-runner-ssh 16 | --------------------------------------------------------------------------------