From 47d0010fb2aacd4fcfc1c331c8fd64e4e5e8b883 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Fri, 13 Feb 2015 00:23:01 -0800 Subject: [PATCH] initial commit --- .drone.yml | 5 ++ .gitignore | 3 + drone/build.go | 226 +++++++++++++++++++++++++++++++++++++++++++++++ drone/delete.go | 30 +++++++ drone/disable.go | 30 +++++++ drone/enable.go | 30 +++++++ drone/handle.go | 32 +++++++ drone/keys.go | 48 ++++++++++ drone/main.go | 48 ++++++++++ drone/repos.go | 43 +++++++++ drone/restart.go | 39 ++++++++ drone/status.go | 52 +++++++++++ drone/util.go | 112 +++++++++++++++++++++++ drone/whoami.go | 32 +++++++ 14 files changed, 730 insertions(+) create mode 100644 .drone.yml create mode 100644 drone/build.go create mode 100644 drone/delete.go create mode 100644 drone/disable.go create mode 100644 drone/enable.go create mode 100644 drone/handle.go create mode 100644 drone/keys.go create mode 100644 drone/main.go create mode 100644 drone/repos.go create mode 100644 drone/restart.go create mode 100644 drone/status.go create mode 100644 drone/util.go create mode 100644 drone/whoami.go diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..308dc24 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,5 @@ +image: go +script: + - cd drone + - go build + - go test diff --git a/.gitignore b/.gitignore index daf913b..fede3ac 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ _testmain.go *.exe *.test *.prof + +# Binary file(s) +drone/drone \ No newline at end of file diff --git a/drone/build.go b/drone/build.go new file mode 100644 index 0000000..c14fb09 --- /dev/null +++ b/drone/build.go @@ -0,0 +1,226 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "time" + + "github.com/drone/drone/shared/build" + "github.com/drone/drone/shared/build/docker" + "github.com/drone/drone/shared/build/log" + "github.com/drone/drone/shared/build/repo" + "github.com/drone/drone/shared/build/script" + + "github.com/codegangsta/cli" +) + +const EXIT_STATUS = 1 + +// NewBuildCommand returns the CLI command for "build". +func NewBuildCommand() cli.Command { + return cli.Command{ + Name: "build", + Usage: "run a local build", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "i", + Value: "", + Usage: "identify file injected in the container", + }, + cli.BoolFlag{ + Name: "p", + Usage: "runs drone build in a privileged container", + }, + cli.BoolFlag{ + Name: "deploy", + Usage: "runs drone build with deployments enabled", + }, + cli.BoolFlag{ + Name: "publish", + Usage: "runs drone build with publishing enabled", + }, + cli.StringFlag{ + Name: "docker-host", + Value: getHost(), + Usage: "docker daemon address", + }, + cli.StringFlag{ + Name: "docker-cert", + Value: getCert(), + Usage: "docker daemon tls certificate", + }, + cli.StringFlag{ + Name: "docker-key", + Value: getKey(), + Usage: "docker daemon tls key", + }, + }, + Action: func(c *cli.Context) { + buildCommandFunc(c) + }, + } +} + +// buildCommandFunc executes the "build" command. +func buildCommandFunc(c *cli.Context) { + var privileged = c.Bool("p") + var identity = c.String("i") + var deploy = c.Bool("deploy") + var publish = c.Bool("publish") + var path string + + var dockerhost = c.String("docker-host") + var dockercert = c.String("docker-cert") + var dockerkey = c.String("docker-key") + + // the path is provided as an optional argument that + // will otherwise default to $PWD/.drone.yml + if len(c.Args()) > 0 { + path = c.Args()[0] + } + + switch len(path) { + case 0: + path, _ = os.Getwd() + path = filepath.Join(path, ".drone.yml") + default: + path = filepath.Clean(path) + path, _ = filepath.Abs(path) + path = filepath.Join(path, ".drone.yml") + } + + // this configures the default Docker logging levels, + // and suffix and prefix values. + log.SetPrefix("\033[2m[DRONE] ") + log.SetSuffix("\033[0m\n") + log.SetOutput(os.Stdout) + log.SetPriority(log.LOG_DEBUG) //LOG_NOTICE + docker.Logging = false + + var exit, _ = run(path, identity, dockerhost, dockercert, dockerkey, publish, deploy, privileged) + os.Exit(exit) +} + +// TODO this has gotten a bit out of hand. refactor input params +func run(path, identity, dockerhost, dockercert, dockerkey string, publish, deploy, privileged bool) (int, error) { + dockerClient, err := docker.NewHostCertFile(dockerhost, dockercert, dockerkey) + if err != nil { + log.Err(err.Error()) + return EXIT_STATUS, err + } + + // parse the private environment variables + envs := getParamMap("DRONE_ENV_") + + // parse the Drone yml file + s, err := script.ParseBuildFile(path, envs) + if err != nil { + log.Err(err.Error()) + return EXIT_STATUS, err + } + + // inject private environment variables into build script + for key, val := range envs { + s.Env = append(s.Env, key+"="+val) + } + + if deploy == false { + s.Deploy = nil + } + if publish == false { + s.Publish = nil + } + + // get the repository root directory + dir := filepath.Dir(path) + code := repo.Repo{ + Name: filepath.Base(dir), + Branch: "HEAD", // should we do this? + Path: dir, + } + + // does the local repository match the + // $GOPATH/src/{package} pattern? This is + // important so we know the target location + // where the code should be copied inside + // the container. + if gopath, ok := getRepoPath(dir); ok { + code.Dir = gopath + + } else if gopath, ok := getGoPath(dir); ok { + // in this case we found a GOPATH and + // reverse engineered the package path + code.Dir = gopath + + } else { + // otherwise just use directory name + code.Dir = filepath.Base(dir) + } + + // this is where the code gets uploaded to the container + // TODO move this code to the build package + code.Dir = filepath.Join("/var/cache/drone/src", filepath.Clean(code.Dir)) + + // ssh key to import into container + var key []byte + if len(identity) != 0 { + key, err = ioutil.ReadFile(identity) + if err != nil { + fmt.Printf("[Error] Could not find or read identity file %s\n", identity) + return EXIT_STATUS, err + } + } + + // loop through and create builders + builder := build.New(dockerClient) + builder.Build = s + builder.Repo = &code + builder.Key = key + builder.Stdout = os.Stdout + builder.Timeout = 300 * time.Minute + builder.Privileged = privileged + + // execute the build + if err := builder.Run(); err != nil { + log.Errf("Error executing build: %s", err.Error()) + return EXIT_STATUS, err + } + + fmt.Printf("\nDrone Build Results \033[90m(%s)\033[0m\n", dir) + + // loop through and print results + + build := builder.Build + res := builder.BuildState + duration := time.Duration(res.Finished - res.Started) + switch { + case builder.BuildState.ExitCode == 0: + fmt.Printf(" \033[32m\u2713\033[0m %v \033[90m(%v)\033[0m\n", build.Name, humanizeDuration(duration*time.Second)) + case builder.BuildState.ExitCode != 0: + fmt.Printf(" \033[31m\u2717\033[0m %v \033[90m(%v)\033[0m\n", build.Name, humanizeDuration(duration*time.Second)) + } + + return builder.BuildState.ExitCode, nil +} + +func getHost() string { + return os.Getenv("DOCKER_HOST") +} + +func getCert() string { + if os.Getenv("DOCKER_CERT_PATH") != "" && os.Getenv("DOCKER_TLS_VERIFY") == "1" { + return filepath.Join(os.Getenv("DOCKER_CERT_PATH"), "cert.pem") + } else { + return "" + } +} + +func getKey() string { + if os.Getenv("DOCKER_CERT_PATH") != "" && os.Getenv("DOCKER_TLS_VERIFY") == "1" { + return filepath.Join(os.Getenv("DOCKER_CERT_PATH"), "key.pem") + } else { + return "" + } +} diff --git a/drone/delete.go b/drone/delete.go new file mode 100644 index 0000000..0174baa --- /dev/null +++ b/drone/delete.go @@ -0,0 +1,30 @@ +package main + +import ( + "github.com/codegangsta/cli" + "github.com/drone/drone-go/drone" +) + +// NewDeleteCommand returns the CLI command for "delete". +func NewDeleteCommand() cli.Command { + return cli.Command{ + Name: "delete", + Usage: "delete a repository", + Flags: []cli.Flag{}, + Action: func(c *cli.Context) { + handle(c, deleteCommandFunc) + }, + } +} + +// deleteCommandFunc executes the "delete" command. +func deleteCommandFunc(c *cli.Context, client *drone.Client) error { + var host, owner, name string + var args = c.Args() + + if len(args) != 0 { + host, owner, name = parseRepo(args[0]) + } + + return client.Repos.Delete(host, owner, name) +} diff --git a/drone/disable.go b/drone/disable.go new file mode 100644 index 0000000..b972f47 --- /dev/null +++ b/drone/disable.go @@ -0,0 +1,30 @@ +package main + +import ( + "github.com/codegangsta/cli" + "github.com/drone/drone-go/drone" +) + +// NewDisableCommand returns the CLI command for "disable". +func NewDisableCommand() cli.Command { + return cli.Command{ + Name: "disable", + Usage: "disable a repository", + Flags: []cli.Flag{}, + Action: func(c *cli.Context) { + handle(c, disableCommandFunc) + }, + } +} + +// disableCommandFunc executes the "disable" command. +func disableCommandFunc(c *cli.Context, client *drone.Client) error { + var host, owner, name string + var args = c.Args() + + if len(args) != 0 { + host, owner, name = parseRepo(args[0]) + } + + return client.Repos.Disable(host, owner, name) +} diff --git a/drone/enable.go b/drone/enable.go new file mode 100644 index 0000000..2e23c00 --- /dev/null +++ b/drone/enable.go @@ -0,0 +1,30 @@ +package main + +import ( + "github.com/codegangsta/cli" + "github.com/drone/drone-go/drone" +) + +// NewEnableCommand returns the CLI command for "enable". +func NewEnableCommand() cli.Command { + return cli.Command{ + Name: "enable", + Usage: "enable a repository", + Flags: []cli.Flag{}, + Action: func(c *cli.Context) { + handle(c, enableCommandFunc) + }, + } +} + +// enableCommandFunc executes the "enable" command. +func enableCommandFunc(c *cli.Context, client *drone.Client) error { + var host, owner, name string + var args = c.Args() + + if len(args) != 0 { + host, owner, name = parseRepo(args[0]) + } + + return client.Repos.Enable(host, owner, name) +} diff --git a/drone/handle.go b/drone/handle.go new file mode 100644 index 0000000..9c10048 --- /dev/null +++ b/drone/handle.go @@ -0,0 +1,32 @@ +package main + +import ( + "os" + + "github.com/codegangsta/cli" + "github.com/drone/drone-go/drone" +) + +type handlerFunc func(*cli.Context, *drone.Client) error + +// handle wraps the command function handlers and +// sets up the environment. +func handle(c *cli.Context, fn handlerFunc) { + var token = c.GlobalString("token") + var server = c.GlobalString("server") + + // if no server url is provided we can default + // to the hosted Drone service. + if len(server) == 0 { + server = "http://test.drone.io" + } + + // create the drone client + client := drone.NewClient(token, server) + + // handle the function + if err := fn(c, client); err != nil { + println(err.Error()) + os.Exit(1) + } +} diff --git a/drone/keys.go b/drone/keys.go new file mode 100644 index 0000000..70bb38f --- /dev/null +++ b/drone/keys.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "io/ioutil" + + "github.com/codegangsta/cli" + "github.com/drone/drone-go/drone" +) + +// NewSetKeyCommand returns the CLI command for "set-key". +func NewSetKeyCommand() cli.Command { + return cli.Command{ + Name: "set-key", + Usage: "sets the SSH private key used to clone", + Flags: []cli.Flag{}, + Action: func(c *cli.Context) { + handle(c, setKeyCommandFunc) + }, + } +} + +// setKeyCommandFunc executes the "set-key" command. +func setKeyCommandFunc(c *cli.Context, client *drone.Client) error { + var host, owner, name, path string + var args = c.Args() + + if len(args) != 0 { + host, owner, name = parseRepo(args[0]) + } + + if len(args) == 2 { + path = args[1] + } + + pub, err := ioutil.ReadFile(path) + if err != nil { + return fmt.Errorf("Could not find private RSA key %s. %s", path, err) + } + + path_pub := path + ".pub" + priv, err := ioutil.ReadFile(path_pub) + if err != nil { + return fmt.Errorf("Could not find public RSA key %s. %s", path_pub, err) + } + + return client.Repos.SetKey(host, owner, name, string(pub), string(priv)) +} diff --git a/drone/main.go b/drone/main.go new file mode 100644 index 0000000..78935e8 --- /dev/null +++ b/drone/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "os" + + "github.com/codegangsta/cli" +) + +var ( + // commit sha for the current build. + version string + revision string +) + +func main() { + app := cli.NewApp() + app.Name = "drone" + app.Version = version + app.Usage = "command line utility" + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "t, token", + Value: "", + Usage: "server auth token", + EnvVar: "DRONE_TOKEN", + }, + cli.StringFlag{ + Name: "s, server", + Value: "", + Usage: "server location", + EnvVar: "DRONE_SERVER", + }, + } + + app.Commands = []cli.Command{ + NewBuildCommand(), + NewReposCommand(), + NewStatusCommand(), + NewEnableCommand(), + NewDisableCommand(), + NewRestartCommand(), + NewWhoamiCommand(), + NewSetKeyCommand(), + NewDeleteCommand(), + } + + app.Run(os.Args) +} diff --git a/drone/repos.go b/drone/repos.go new file mode 100644 index 0000000..2c6aae2 --- /dev/null +++ b/drone/repos.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + + "github.com/codegangsta/cli" + "github.com/drone/drone-go/drone" +) + +// NewReposCommand returns the CLI command for "repos". +func NewReposCommand() cli.Command { + return cli.Command{ + Name: "repos", + Usage: "lists active remote repositories", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "a, all", + Usage: "list all repositories", + }, + }, + Action: func(c *cli.Context) { + handle(c, reposCommandFunc) + }, + } +} + +// reposCommandFunc executes the "repos" command. +func reposCommandFunc(c *cli.Context, client *drone.Client) error { + repos, err := client.Repos.List() + if err != nil { + return err + } + + var all = c.Bool("a") + for _, repo := range repos { + if !all && !repo.Active { + continue + } + + fmt.Printf("%s/%s/%s\n", repo.Host, repo.Owner, repo.Name) + } + return nil +} diff --git a/drone/restart.go b/drone/restart.go new file mode 100644 index 0000000..67f6d01 --- /dev/null +++ b/drone/restart.go @@ -0,0 +1,39 @@ +package main + +import ( + "github.com/codegangsta/cli" + "github.com/drone/drone-go/drone" +) + +// NewRestartCommand returns the CLI command for "restart". +func NewRestartCommand() cli.Command { + return cli.Command{ + Name: "restart", + Usage: "restarts a build on the server", + Flags: []cli.Flag{}, + Action: func(c *cli.Context) { + handle(c, restartCommandFunc) + }, + } +} + +// restartCommandFunc executes the "restart" command. +func restartCommandFunc(c *cli.Context, client *drone.Client) error { + var host, owner, repo, branch, sha string + var args = c.Args() + + if len(args) != 0 { + host, owner, repo = parseRepo(args[0]) + } + + switch len(args) { + case 2: + branch = "master" + sha = args[1] + case 3, 4, 5: + branch = args[1] + sha = args[2] + } + + return client.Commits.Rebuild(host, owner, repo, branch, sha) +} diff --git a/drone/status.go b/drone/status.go new file mode 100644 index 0000000..ce52298 --- /dev/null +++ b/drone/status.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + + "github.com/codegangsta/cli" + "github.com/drone/drone-go/drone" +) + +// NewStatusCommand returns the CLI command for "status". +func NewStatusCommand() cli.Command { + return cli.Command{ + Name: "status", + Usage: "display a repository build status", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "b, branch", + Usage: "branch to display", + }, + }, + Action: func(c *cli.Context) { + handle(c, statusCommandFunc) + }, + } +} + +// statusCommandFunc executes the "status" command. +func statusCommandFunc(c *cli.Context, client *drone.Client) error { + var host, owner, repo, branch string + var args = c.Args() + + if len(args) != 0 { + host, owner, repo = parseRepo(args[0]) + } + + if c.IsSet("branch") { + branch = c.String("branch") + } else { + branch = "master" + } + + builds, err := client.Commits.ListBranch(host, owner, repo, branch) + if err != nil { + return err + } else if len(builds) == 0 { + return nil + } + + var build = builds[len(builds)-1] + fmt.Printf("%s\t%s\t%s\t%s\t%v", build.Status, build.ShaShort(), build.Timestamp, build.Author, build.Message) + return nil +} diff --git a/drone/util.go b/drone/util.go new file mode 100644 index 0000000..ec4f2de --- /dev/null +++ b/drone/util.go @@ -0,0 +1,112 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "time" +) + +func parseRepo(str string) (host, owner, repo string) { + var parts = strings.Split(str, "/") + if len(parts) != 3 { + return + } + host = parts[0] + owner = parts[1] + repo = parts[2] + return +} + +// getGoPath checks the source codes absolute path +// in reference to the host operating system's GOPATH +// to correctly determine the code's package path. This +// is Go-specific, since Go code must exist in +// $GOPATH/src/github.com/{owner}/{name} +func getGoPath(dir string) (string, bool) { + path := os.Getenv("GOPATH") + if len(path) == 0 { + return "", false + } + // append src to the GOPATH, since + // the code will be stored in the src dir + path = filepath.Join(path, "src") + if !filepath.HasPrefix(dir, path) { + return "", false + } + + // remove the prefix from the directory + // this should leave us with the go package name + return dir[len(path):], true +} + +var gopathExp = regexp.MustCompile("./src/(github.com/[^/]+/[^/]+|bitbucket.org/[^/]+/[^/]+|code.google.com/[^/]+/[^/]+)") + +// getRepoPath checks the source codes absolute path +// on the host operating system in an attempt +// to correctly determine the code's package path. This +// is Go-specific, since Go code must exist in +// $GOPATH/src/github.com/{owner}/{name} +func getRepoPath(dir string) (path string, ok bool) { + // let's get the package directory based + // on the path in the host OS + indexes := gopathExp.FindStringIndex(dir) + if len(indexes) == 0 { + return + } + + index := indexes[len(indexes)-1] + + // if the dir is /home/ubuntu/go/src/github.com/foo/bar + // the index will start at /src/github.com/foo/bar. + // We'll need to strip "/src/" which is where the + // magic number 5 comes from. + index = strings.LastIndex(dir, "/src/") + return dir[index+5:], true +} + +// GetRepoMap returns a map of enivronment variables that +// should be injected into the .drone.yml +func getParamMap(prefix string) map[string]string { + envs := map[string]string{} + + for _, item := range os.Environ() { + env := strings.SplitN(item, "=", 2) + if len(env) != 2 { + continue + } + + key := env[0] + val := env[1] + if strings.HasPrefix(key, prefix) { + envs[strings.TrimPrefix(key, prefix)] = val + } + } + return envs +} + +// prints the time as a human readable string +func humanizeDuration(d time.Duration) string { + if seconds := int(d.Seconds()); seconds < 1 { + return "Less than a second" + } else if seconds < 60 { + return fmt.Sprintf("%d seconds", seconds) + } else if minutes := int(d.Minutes()); minutes == 1 { + return "About a minute" + } else if minutes < 60 { + return fmt.Sprintf("%d minutes", minutes) + } else if hours := int(d.Hours()); hours == 1 { + return "About an hour" + } else if hours < 48 { + return fmt.Sprintf("%d hours", hours) + } else if hours < 24*7*2 { + return fmt.Sprintf("%d days", hours/24) + } else if hours < 24*30*3 { + return fmt.Sprintf("%d weeks", hours/24/7) + } else if hours < 24*365*2 { + return fmt.Sprintf("%d months", hours/24/30) + } + return fmt.Sprintf("%f years", d.Hours()/24/365) +} diff --git a/drone/whoami.go b/drone/whoami.go new file mode 100644 index 0000000..8a8d0b3 --- /dev/null +++ b/drone/whoami.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + + "github.com/codegangsta/cli" + "github.com/drone/drone-go/drone" +) + +// NewWhoamiCommand returns the CLI command for "whoami". +func NewWhoamiCommand() cli.Command { + return cli.Command{ + Name: "whoami", + Usage: "outputs the current user", + Flags: []cli.Flag{}, + Action: func(c *cli.Context) { + handle(c, whoamiCommandFunc) + }, + } +} + +// whoamiCommandFunc communicates with the server and echoes +// the currently authenticated user. +func whoamiCommandFunc(c *cli.Context, client *drone.Client) error { + user, err := client.Users.GetCurrent() + if err != nil { + return err + } + + fmt.Println(user.Login) + return nil +}