From 8123ab76799dd8e35cfdd19bb6d408ab4b02b804 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 27 Sep 2021 10:48:58 +0530 Subject: [PATCH] Working github actions --- .gitignore | 3 + README.md | 62 ++++++++++ cmd/main.go | 205 ++++++++++++++++++++++++++++++++++ daemon/daemon.go | 51 +++++++++ daemon/daemon_unix.go | 76 +++++++++++++ daemon/daemon_win.go | 11 ++ docker/Dockerfile.linux.amd64 | 9 ++ go.mod | 11 ++ go.sum | 23 ++++ plugin.go | 73 ++++++++++++ scripts/build.sh | 13 +++ scripts/docker.sh | 15 +++ utils/workflow.go | 75 +++++++++++++ 13 files changed, 627 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cmd/main.go create mode 100644 daemon/daemon.go create mode 100644 daemon/daemon_unix.go create mode 100644 daemon/daemon_win.go create mode 100644 docker/Dockerfile.linux.amd64 create mode 100644 go.mod create mode 100644 go.sum create mode 100644 plugin.go create mode 100755 scripts/build.sh create mode 100755 scripts/docker.sh create mode 100644 utils/workflow.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5077f86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +release +coverage.out +vendor diff --git a/README.md b/README.md new file mode 100644 index 0000000..309667e --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# drone-github-action-plugin + +This plugins allows running github actions as a drone plugin + +## Build + +Build the binaries with the following commands: + +```console +export GOOS=linux +export GOARCH=amd64 +export CGO_ENABLED=0 +export GO111MODULE=on + +go build -v -a -tags netgo -o release/linux/amd64/plugin ./cmd + +``` + +## Docker + +Build the Docker images with the following commands: + +```console +docker build \ + --label org.label-schema.build-date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + --label org.label-schema.vcs-ref=$(git rev-parse --short HEAD) \ + --file docker/Dockerfile.linux.amd64 --tag plugins/github-actions . + +``` + +## Plugin step usage + +Provide uses, with & env of github action to use in plugin step settings. + +```console +steps: +- name: github-action + image: plugins/github-actions + settings: + uses: actions/hello-world-javascript-action@v1.1 + with: + who-to-greet: Mona the Octocat + env: + hello: world + +``` + +## Running locally + +1. Running actions/hello-world-javascript-action action locally via docker: + +```console + + docker run --rm \ + --privileged \ + -w /drone \ + -e PLUGIN_USES="actions/hello-world-javascript-action@v1.1" \ + -e PLUGIN_WITH="{\"who-to-greet\":\"Mona the Octocat\"}" \ + -e PLUGIN_VERBOSE=true \ + plugins/github-actions + +``` \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..eb04b76 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,205 @@ +package main + +import ( + "encoding/json" + "os" + + plugin "github.com/drone-plugins/drone-github-actions" + "github.com/drone-plugins/drone-github-actions/daemon" + "github.com/joho/godotenv" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +var ( + version = "unknown" +) + +type genericMapType struct { + m map[string]string + strVal string +} + +func (g *genericMapType) Set(value string) error { + m := make(map[string]string) + if err := json.Unmarshal([]byte(value), &m); err != nil { + return err + } + g.m = m + g.strVal = value + return nil +} + +func (g *genericMapType) String() string { + return g.strVal +} + +func main() { + // Load env-file if it exists first + if env := os.Getenv("PLUGIN_ENV_FILE"); env != "" { + if err := godotenv.Load(env); err != nil { + logrus.Fatal(err) + } + } + + app := cli.NewApp() + app.Name = "drone github actions plugin" + app.Usage = "drone github actions plugin" + app.Action = run + app.Version = version + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "action-name", + Usage: "Github action name", + EnvVar: "PLUGIN_USES", + }, + cli.StringFlag{ + Name: "action-with", + Usage: "Github action with", + EnvVar: "PLUGIN_WITH", + }, + cli.StringFlag{ + Name: "action-env", + Usage: "Github action env", + EnvVar: "PLUGIN_ENV", + }, + cli.BoolFlag{ + Name: "action-verbose", + Usage: "Github action enable verbose logging", + EnvVar: "PLUGIN_VERBOSE", + }, + cli.StringFlag{ + Name: "action-image", + Usage: "Image to use for running github actions", + Value: "node:12-buster-slim", + EnvVar: "PLUGIN_ACTION_IMAGE", + }, + + // daemon flags + cli.StringFlag{ + Name: "docker.registry", + Usage: "docker daemon registry", + Value: "https://index.docker.io/v1/", + EnvVar: "PLUGIN_DAEMON_REGISTRY", + }, + cli.StringFlag{ + Name: "daemon.mirror", + Usage: "docker daemon registry mirror", + EnvVar: "PLUGIN_DAEMON_MIRROR", + }, + cli.StringFlag{ + Name: "daemon.storage-driver", + Usage: "docker daemon storage driver", + EnvVar: "PLUGIN_DAEMON_STORAGE_DRIVER", + }, + cli.StringFlag{ + Name: "daemon.storage-path", + Usage: "docker daemon storage path", + Value: "/var/lib/docker", + EnvVar: "PLUGIN_DAEMON_STORAGE_PATH", + }, + cli.StringFlag{ + Name: "daemon.bip", + Usage: "docker daemon bride ip address", + EnvVar: "PLUGIN_DAEMON_BIP", + }, + cli.StringFlag{ + Name: "daemon.mtu", + Usage: "docker daemon custom mtu setting", + EnvVar: "PLUGIN_DAEMON_MTU", + }, + cli.StringSliceFlag{ + Name: "daemon.dns", + Usage: "docker daemon dns server", + EnvVar: "PLUGIN_DAEMON_CUSTOM_DNS", + }, + cli.StringSliceFlag{ + Name: "daemon.dns-search", + Usage: "docker daemon dns search domains", + EnvVar: "PLUGIN_DAEMON_CUSTOM_DNS_SEARCH", + }, + cli.BoolFlag{ + Name: "daemon.insecure", + Usage: "docker daemon allows insecure registries", + EnvVar: "PLUGIN_DAEMON_INSECURE", + }, + cli.BoolFlag{ + Name: "daemon.ipv6", + Usage: "docker daemon IPv6 networking", + EnvVar: "PLUGIN_DAEMON_IPV6", + }, + cli.BoolFlag{ + Name: "daemon.experimental", + Usage: "docker daemon Experimental mode", + EnvVar: "PLUGIN_DAEMON_EXPERIMENTAL", + }, + cli.BoolFlag{ + Name: "daemon.debug", + Usage: "docker daemon executes in debug mode", + EnvVar: "PLUGIN_DAEMON_DEBUG", + }, + cli.BoolFlag{ + Name: "daemon.off", + Usage: "don't start the docker daemon", + EnvVar: "PLUGIN_DAEMON_OFF", + }, + } + + if err := app.Run(os.Args); err != nil { + logrus.Fatal(err) + } +} + +func run(c *cli.Context) error { + if c.String("action-name") == "" { + return errors.New("uses attribute must be set") + } + + actionWith, err := strToMap(c.String("action-with")) + if err != nil { + return errors.Wrap(err, "with attribute is not of map type with key & value as string") + } + actionEnv, err := strToMap(c.String("action-env")) + if err != nil { + return errors.Wrap(err, "env attribute is not of map type with key & value as string") + } + + plugin := plugin.Plugin{ + Action: plugin.Action{ + Uses: c.String("action-name"), + With: actionWith, + Env: actionEnv, + Verbose: c.Bool("action-verbose"), + Image: c.String("action-image"), + }, + Daemon: daemon.Daemon{ + Registry: c.String("docker.registry"), + Mirror: c.String("daemon.mirror"), + StorageDriver: c.String("daemon.storage-driver"), + StoragePath: c.String("daemon.storage-path"), + Insecure: c.Bool("daemon.insecure"), + Disabled: c.Bool("daemon.off"), + IPv6: c.Bool("daemon.ipv6"), + Debug: c.Bool("daemon.debug"), + Bip: c.String("daemon.bip"), + DNS: c.StringSlice("daemon.dns"), + DNSSearch: c.StringSlice("daemon.dns-search"), + MTU: c.String("daemon.mtu"), + Experimental: c.Bool("daemon.experimental"), + }, + } + return plugin.Exec() +} + +func strToMap(s string) (map[string]string, error) { + m := make(map[string]string) + if s == "" { + return m, nil + } + + if err := json.Unmarshal([]byte(s), &m); err != nil { + return nil, err + } + return m, nil +} diff --git a/daemon/daemon.go b/daemon/daemon.go new file mode 100644 index 0000000..3a33400 --- /dev/null +++ b/daemon/daemon.go @@ -0,0 +1,51 @@ +package daemon + +import ( + "fmt" + "os/exec" + "time" +) + +type Daemon struct { + Registry string // Docker registry + Mirror string // Docker registry mirror + Insecure bool // Docker daemon enable insecure registries + StorageDriver string // Docker daemon storage driver + StoragePath string // Docker daemon storage path + Disabled bool // DOcker daemon is disabled (already running) + Debug bool // Docker daemon started in debug mode + Bip string // Docker daemon network bridge IP address + DNS []string // Docker daemon dns server + DNSSearch []string // Docker daemon dns search domain + MTU string // Docker daemon mtu setting + IPv6 bool // Docker daemon IPv6 networking + Experimental bool // Docker daemon enable experimental mode +} + +func StartDaemon(d Daemon) error { + startDaemon(d) + return waitForDaemon() +} + +func waitForDaemon() error { + // poll the docker daemon until it is started. This ensures the daemon is + // ready to accept connections before we proceed. + for i := 0; ; i++ { + cmd := commandInfo() + err := cmd.Run() + if err == nil { + break + } + if i == 15 { + fmt.Println("Unable to reach Docker Daemon after 15 attempts.") + return fmt.Errorf("failed to reach docker daemon after 15 attempts: %v", err) + } + time.Sleep(time.Second * 1) + } + return nil +} + +// helper function to create the docker info command. +func commandInfo() *exec.Cmd { + return exec.Command(dockerExe, "info") +} diff --git a/daemon/daemon_unix.go b/daemon/daemon_unix.go new file mode 100644 index 0000000..28270b7 --- /dev/null +++ b/daemon/daemon_unix.go @@ -0,0 +1,76 @@ +// +build !windows + +package daemon + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "strings" +) + +const dockerExe = "/usr/local/bin/docker" +const dockerdExe = "/usr/local/bin/dockerd" + +func startDaemon(daemon Daemon) { + cmd := commandDaemon(daemon) + if daemon.Debug { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } else { + cmd.Stdout = ioutil.Discard + cmd.Stderr = ioutil.Discard + } + go func() { + trace(cmd) + cmd.Run() + }() +} + +// helper function to create the docker daemon command. +func commandDaemon(daemon Daemon) *exec.Cmd { + args := []string{ + "--data-root", daemon.StoragePath, + "--host=unix:///var/run/docker.sock", + } + + if _, err := os.Stat("/etc/docker/default.json"); err == nil { + args = append(args, "--seccomp-profile=/etc/docker/default.json") + } + + if daemon.StorageDriver != "" { + args = append(args, "-s", daemon.StorageDriver) + } + if daemon.Insecure && daemon.Registry != "" { + args = append(args, "--insecure-registry", daemon.Registry) + } + if daemon.IPv6 { + args = append(args, "--ipv6") + } + if len(daemon.Mirror) != 0 { + args = append(args, "--registry-mirror", daemon.Mirror) + } + if len(daemon.Bip) != 0 { + args = append(args, "--bip", daemon.Bip) + } + for _, dns := range daemon.DNS { + args = append(args, "--dns", dns) + } + for _, dnsSearch := range daemon.DNSSearch { + args = append(args, "--dns-search", dnsSearch) + } + if len(daemon.MTU) != 0 { + args = append(args, "--mtu", daemon.MTU) + } + if daemon.Experimental { + args = append(args, "--experimental") + } + return exec.Command(dockerdExe, args...) +} + +// trace writes each command to stdout with the command wrapped in an xml +// tag so that it can be extracted and displayed in the logs. +func trace(cmd *exec.Cmd) { + fmt.Fprintf(os.Stdout, "+ %s\n", strings.Join(cmd.Args, " ")) +} diff --git a/daemon/daemon_win.go b/daemon/daemon_win.go new file mode 100644 index 0000000..e2cc274 --- /dev/null +++ b/daemon/daemon_win.go @@ -0,0 +1,11 @@ +// +build windows + +package daemon + +const dockerExe = "C:\\bin\\docker.exe" +const dockerdExe = "" +const dockerHome = "C:\\ProgramData\\docker\\" + +func startDaemon(daemon Daemon) { + // this is a no-op on windows +} diff --git a/docker/Dockerfile.linux.amd64 b/docker/Dockerfile.linux.amd64 new file mode 100644 index 0000000..e937d4f --- /dev/null +++ b/docker/Dockerfile.linux.amd64 @@ -0,0 +1,9 @@ +FROM docker:dind + +ENV DOCKER_HOST=unix:///var/run/docker.sock + +RUN apk add --no-cache ca-certificates curl bash +RUN curl -s https://raw.githubusercontent.com/nektos/act/master/install.sh | bash -s v0.2.24 + +ADD release/linux/amd64/plugin /bin/ +ENTRYPOINT ["/usr/local/bin/dockerd-entrypoint.sh", "/bin/plugin"] \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1a93104 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/drone-plugins/drone-github-actions + +go 1.16 + +require ( + github.com/joho/godotenv v1.3.0 + github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.8.1 + github.com/urfave/cli v1.22.5 + gopkg.in/yaml.v2 v2.2.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e1f95e0 --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= +github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/plugin.go b/plugin.go new file mode 100644 index 0000000..a46473f --- /dev/null +++ b/plugin.go @@ -0,0 +1,73 @@ +package plugin + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "github.com/drone-plugins/drone-github-actions/daemon" + "github.com/drone-plugins/drone-github-actions/utils" +) + +const ( + workflowFile = "/tmp/workflow.yml" + webhookFile = "/tmp/webhook" + envFile = "/tmp/action.env" +) + +type ( + Action struct { + Uses string + With map[string]string + Env map[string]string + Image string + Verbose bool + } + + Plugin struct { + Action Action + Daemon daemon.Daemon // Docker daemon configuration + } +) + +// Exec executes the plugin step +func (p Plugin) Exec() error { + if err := daemon.StartDaemon(p.Daemon); err != nil { + return err + } + + if err := utils.CreateWorkflowFile(workflowFile, p.Action.Uses, + p.Action.With, p.Action.Env); err != nil { + return err + } + + cmdArgs := []string{ + "-W", + workflowFile, + "-P", + fmt.Sprintf("ubuntu-latest=%s", p.Action.Image), + "--detect-event", + } + + if p.Action.Verbose { + cmdArgs = append(cmdArgs, "-v") + } + + cmd := exec.Command("act", cmdArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + trace(cmd) + + err := cmd.Run() + if err != nil { + return err + } + return nil +} + +// trace writes each command to stdout with the command wrapped in an xml +// tag so that it can be extracted and displayed in the logs. +func trace(cmd *exec.Cmd) { + fmt.Fprintf(os.Stdout, "+ %s\n", strings.Join(cmd.Args, " ")) +} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..f79429c --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# force go modules +export GOPATH="" + +# disable cgo +export CGO_ENABLED=0 + +set -e +set -x + +# linux +GOOS=linux GOARCH=amd64 go build -o release/linux/amd64/plugin ./cmd \ No newline at end of file diff --git a/scripts/docker.sh b/scripts/docker.sh new file mode 100755 index 0000000..c86d96c --- /dev/null +++ b/scripts/docker.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# force go modules +export GOPATH="" + +# disable cgo +export CGO_ENABLED=0 + +set -e +set -x + +# linux +GOOS=linux GOARCH=amd64 go build -o release/linux/amd64/plugin ./cmd + +docker build -f docker/Dockerfile.linux.amd64 -t plugins/github-actions . \ No newline at end of file diff --git a/utils/workflow.go b/utils/workflow.go new file mode 100644 index 0000000..4f6be52 --- /dev/null +++ b/utils/workflow.go @@ -0,0 +1,75 @@ +package utils + +import ( + "io/ioutil" + "os" + + "github.com/pkg/errors" + "gopkg.in/yaml.v2" +) + +type workflow struct { + Name string `yaml:"name"` + On string `yaml:"on"` + Jobs map[string]job `yaml:"jobs"` +} + +type job struct { + Name string `yaml:"name"` + RunsOn string `yaml:"runs-on"` + Steps []step `yaml:"steps"` +} + +type step struct { + Uses string `yaml:"uses"` + With map[string]string `yaml:"with"` + Env map[string]string `yaml:"env"` +} + +const ( + workflowEvent = "push" + workflowName = "drone-github-action" + jobName = "action" + runsOnImage = "ubuntu-latest" +) + +func CreateWorkflowFile(ymlFile string, action string, + with map[string]string, env map[string]string) error { + j := job{ + Name: jobName, + RunsOn: runsOnImage, + Steps: []step{ + { + Uses: action, + With: with, + Env: env, + }, + }, + } + wf := &workflow{ + Name: workflowName, + On: getWorkflowEvent(), + Jobs: map[string]job{ + jobName: j, + }, + } + + out, err := yaml.Marshal(&wf) + if err != nil { + return errors.Wrap(err, "failed to create action workflow yml") + } + + if err = ioutil.WriteFile(ymlFile, out, 0644); err != nil { + return errors.Wrap(err, "failed to write yml workflow file") + } + + return nil +} + +func getWorkflowEvent() string { + buildEvent := os.Getenv("DRONE_BUILD_EVENT") + if buildEvent == "push" || buildEvent == "pull_request" || buildEvent == "tag" { + return buildEvent + } + return "custom" +}