Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for BitBucket repositories #4

Merged
merged 11 commits into from
Aug 8, 2019
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
vendor/
drone-tree-config
.idea
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ Environment variables:
- `PLUGIN_ADDRESS`: Listen address for the plugins webserver. Defaults to `:3000`.
- `PLUGIN_SECRET`: Shared secret with drone. You can generate the token using `openssl rand -hex 16`.
- `GITHUB_TOKEN`: Github personal access token. Only needs repo rights. See [here][1].
- `GITHUB_SERVER`: Custom Github server for Github Enterprise
- `AUTH_SERVER`: Custom auth server (uses SERVER if empty)
- `SERVER`: Custom SCM server
- `BITBUCKET_CLIENT`: Alternative to GITHUB_TOKEN, same value as DRONE_BITBUCKET_CLIENT.
- `BITBUCKET_SECRET`: Alternative to GITHUB_TOKEN, same value as DRONE_BITBUCKET_SECRET.

If `PLUGIN_CONCAT` is not set, the first `.drone.yml` will be used.

Expand Down
31 changes: 20 additions & 11 deletions cmd/drone-tree-config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ import (

type (
spec struct {
Concat bool `envconfig:"PLUGIN_CONCAT"`
MaxDepth int `envconfig:"PLUGIN_MAXDEPTH" default:"2"`
Fallback bool `envconfig:"PLUGIN_FALLBACK"`
Debug bool `envconfig:"PLUGIN_DEBUG"`
Address string `envconfig:"PLUGIN_ADDRESS" default:":3000"`
Secret string `envconfig:"PLUGIN_SECRET"`
Token string `envconfig:"GITHUB_TOKEN"`
Server string `envconfig:"GITHUB_SERVER"`
Concat bool `envconfig:"PLUGIN_CONCAT"`
MaxDepth int `envconfig:"PLUGIN_MAXDEPTH" default:"2"`
Fallback bool `envconfig:"PLUGIN_FALLBACK"`
Debug bool `envconfig:"PLUGIN_DEBUG"`
Address string `envconfig:"PLUGIN_ADDRESS" default:":3000"`
Secret string `envconfig:"PLUGIN_SECRET"`
GitHubToken string `envconfig:"GITHUB_TOKEN"`
AuthServer string `envconfig:"AUTH_SERVER"`
Server string `envconfig:"SERVER"`
BitBucketClient string `envconfig:"BITBUCKET_CLIENT"`
BitBucketSecret string `envconfig:"BITBUCKET_SECRET"`
}
)

Expand All @@ -35,17 +38,23 @@ func main() {
if spec.Secret == "" {
logrus.Fatalln("missing secret key")
}
if spec.Token == "" {
logrus.Warnln("missing github token")
if spec.GitHubToken == "" && (spec.BitBucketClient == "" || spec.BitBucketSecret == "") {
logrus.Warnln("missing SCM credentials, e.g. GitHub token")
}
if spec.Address == "" {
spec.Address = ":3000"
}
if spec.AuthServer == "" {
spec.AuthServer = spec.Server
}

handler := config.Handler(
plugin.New(
spec.AuthServer,
spec.Server,
spec.Token,
spec.GitHubToken,
spec.BitBucketClient,
spec.BitBucketSecret,
spec.Concat,
spec.Fallback,
spec.MaxDepth,
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/google/uuid v1.1.1
github.com/kelseyhightower/envconfig v1.3.0
github.com/sirupsen/logrus v1.4.1
github.com/wbrefvem/go-bitbucket v0.0.0-20190128183802-fc08fd046abb
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 // indirect
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/wbrefvem/go-bitbucket v0.0.0-20190128183802-fc08fd046abb h1:KrmaSo+FHWBt1H652w/uerwzKvQqh4H7Jgyxm4hz2BQ=
github.com/wbrefvem/go-bitbucket v0.0.0-20190128183802-fc08fd046abb/go.mod h1:Z91j2jYBApRjJ0zlXDCxPrrZR8ohkkd4g0n+Hqs1w0Q=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg=
Expand Down
136 changes: 64 additions & 72 deletions plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,40 @@ import (
"strconv"
"strings"

"github.com/bitsbeats/drone-tree-config/plugin/scm_clients"
"github.com/drone/drone-go/drone"
"github.com/drone/drone-go/plugin/config"

"github.com/google/go-github/github"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"gopkg.in/yaml.v2"
)

// New creates a drone plugin
func New(server, token string, concat bool, fallback bool, maxDepth int) config.Plugin {
return &plugin{
server: server,
token: token,
concat: concat,
fallback: fallback,
maxDepth: maxDepth,
func New(authServer string, server string, gitHubToken string, bitBucketClient string, bitBucketSecret string,
concat bool, fallback bool, maxDepth int) config.Plugin {
return &Plugin{
authServer: authServer,
server: server,
gitHubToken: gitHubToken,
bitBucketClient: bitBucketClient,
bitBucketSecret: bitBucketSecret,
concat: concat,
fallback: fallback,
maxDepth: maxDepth,
}
}

type (
plugin struct {
server string
token string
concat bool
fallback bool
maxDepth int
Plugin struct {
authServer string
server string
gitHubToken string
bitBucketClient string
bitBucketSecret string
concat bool
fallback bool
maxDepth int
}

droneConfig struct {
Expand All @@ -47,35 +53,38 @@ type (
request struct {
*config.Request
UUID uuid.UUID
Client *github.Client
Client scm_clients.ScmClient
}
)

var dedupRegex = regexp.MustCompile(`(?ms)(---[\s]*){2,}`)

// Find is called by dorne
func (p *plugin) Find(ctx context.Context, droneRequest *config.Request) (*drone.Config, error) {
uuid := uuid.New()
logrus.Infof("%s %s/%s started", uuid, droneRequest.Repo.Namespace, droneRequest.Repo.Name)
defer logrus.Infof("%s finished", uuid)

// connect to github
trans := oauth2.NewClient(ctx, oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: p.token},
))
var client *github.Client
if p.server == "" {
client = github.NewClient(trans)
func (p *Plugin) NewScmClient(uuid uuid.UUID, repo drone.Repo, ctx context.Context) scm_clients.ScmClient {
var scmClient scm_clients.ScmClient
var err error
if p.gitHubToken != "" {
scmClient, err = scm_clients.NewGitHubClient(uuid, p.server, p.gitHubToken, repo, ctx)
} else if p.bitBucketClient != "" {
scmClient, err = scm_clients.NewBitBucketClient(uuid, p.authServer, p.server, p.bitBucketClient, p.bitBucketSecret, repo)
} else {
var err error
client, err = github.NewEnterpriseClient(p.server, p.server, trans)
if err != nil {
logrus.Errorf("%s Unable to connect to Github: '%v'", uuid, err)
return nil, err
}
err = fmt.Errorf("no SCM credentials specified")
}
if err != nil {
logrus.Errorf("Unable to connect to SCM server.")
}
return scmClient
}

// Find is called by drone
func (p *Plugin) Find(ctx context.Context, droneRequest *config.Request) (*drone.Config, error) {
someUuid := uuid.New()
logrus.Infof("%s %s/%s started", someUuid, droneRequest.Repo.Namespace, droneRequest.Repo.Name)
defer logrus.Infof("%s finished", someUuid)

// connect to scm
client := p.NewScmClient(someUuid, droneRequest.Repo, ctx)

req := request{droneRequest, uuid, client}
req := request{droneRequest, someUuid, client}

// get changed files
changedFiles, err := p.getGithubChanges(ctx, &req)
Expand All @@ -86,7 +95,7 @@ func (p *plugin) Find(ctx context.Context, droneRequest *config.Request) (*drone
// get drone.yml for changed files or all of them if no changes/cron
configData := ""
if changedFiles != nil {
configData, err = p.getGithubConfigData(ctx, &req, changedFiles)
configData, err = p.getConfigDataForChanges(ctx, &req, changedFiles)
} else if req.Build.Trigger == "@cron" {
logrus.Warnf("%s @cron, rebuilding all", req.UUID)
configData, err = p.getAllConfigData(ctx, &req, "/", 0)
Expand All @@ -111,7 +120,7 @@ func (p *plugin) Find(ctx context.Context, droneRequest *config.Request) (*drone
}

// getGithubChanges tries to get a list of changed files from github
func (p *plugin) getGithubChanges(ctx context.Context, req *request) ([]string, error) {
func (p *Plugin) getGithubChanges(ctx context.Context, req *request) ([]string, error) {
var changedFiles []string

if req.Build.Trigger == "@cron" {
Expand All @@ -124,29 +133,22 @@ func (p *plugin) getGithubChanges(ctx context.Context, req *request) ([]string,
logrus.Errorf("%s unable to get pull request id %v", req.UUID, err)
return nil, err
}
opts := github.ListOptions{}
files, _, err := req.Client.PullRequests.ListFiles(ctx, req.Repo.Namespace, req.Repo.Name, pullRequestID, &opts)
changedFiles, err = req.Client.ChangedFilesInPullRequest(ctx, pullRequestID)
if err != nil {
logrus.Errorf("%s unable to fetch diff for Pull request %v", req.UUID, err)
return nil, err
}
for _, file := range files {
changedFiles = append(changedFiles, *file.Filename)
}
} else {
// use diff to get changed files
before := req.Build.Before
if before == "0000000000000000000000000000000000000000" || before == "" {
before = fmt.Sprintf("%s~1", req.Build.After)
}
changes, _, err := req.Client.Repositories.CompareCommits(ctx, req.Repo.Namespace, req.Repo.Name, before, req.Build.After)
var err error
changedFiles, err = req.Client.ChangedFilesInDiff(ctx, before, req.Build.After)
if err != nil {
logrus.Errorf("%s unable to fetch diff: '%v'", req.UUID, err)
return nil, err
}
for _, file := range changes.Files {
changedFiles = append(changedFiles, *file.Filename)
}
}

if len(changedFiles) > 0 {
Expand All @@ -159,21 +161,13 @@ func (p *plugin) getGithubChanges(ctx context.Context, req *request) ([]string,
}

// getGithubFile downloads a file from github
func (p *plugin) getGithubFile(ctx context.Context, req *request, file string) (content string, err error) {
func (p *Plugin) getGithubFile(ctx context.Context, req *request, file string) (content string, err error) {
logrus.Debugf("%s checking %s/%s %s", req.UUID, req.Repo.Namespace, req.Repo.Name, file)
ref := github.RepositoryContentGetOptions{Ref: req.Build.After}
data, _, _, err := req.Client.Repositories.GetContents(ctx, req.Repo.Namespace, req.Repo.Name, file, &ref)
if data == nil {
err = fmt.Errorf("failed to get %s: is not a file", file)
}
if err != nil {
return "", err
}
return data.GetContent()
return req.Client.GetFileContents(ctx, file, req.Build.After)
}

// getGithubDroneConfig downloads a drone config and validates it
func (p *plugin) getGithubDroneConfig(ctx context.Context, req *request, file string) (configData string, critical bool, err error) {
func (p *Plugin) getGithubDroneConfig(ctx context.Context, req *request, file string) (configData string, critical bool, err error) {
fileContent, err := p.getGithubFile(ctx, req, file)
if err != nil {
logrus.Debugf("%s skipping: unable to load file: %s %v", req.UUID, file, err)
Expand All @@ -196,8 +190,8 @@ func (p *plugin) getGithubDroneConfig(ctx context.Context, req *request, file st
return fileContent, false, nil
}

// getGithubConfigData scans a repository based on the changed files
func (p *plugin) getGithubConfigData(ctx context.Context, req *request, changedFiles []string) (configData string, err error) {
// getConfigDataForChanges scans a repository based on the changed files
func (p *Plugin) getConfigDataForChanges(ctx context.Context, req *request, changedFiles []string) (configData string, err error) {
// collect drone.yml files
configData = ""
cache := map[string]bool{}
Expand Down Expand Up @@ -241,10 +235,9 @@ func (p *plugin) getGithubConfigData(ctx context.Context, req *request, changedF
return configData, nil
}

// getAllConfigData searches for all or fist 'drone.yml' in the repo
func (p *plugin) getAllConfigData(ctx context.Context, req *request, dir string, depth int) (configData string, err error) {
ref := github.RepositoryContentGetOptions{Ref: req.Build.After}
_, ls, _, err := req.Client.Repositories.GetContents(ctx, req.Repo.Namespace, req.Repo.Name, dir, &ref)
// getAllConfigData searches for all or first 'drone.yml' in the repo
func (p *Plugin) getAllConfigData(ctx context.Context, req *request, dir string, depth int) (configData string, err error) {
ls, err := req.Client.GetFileListing(ctx, dir, req.Build.After)
if err != nil {
return "", err
}
Expand All @@ -255,15 +248,15 @@ func (p *plugin) getAllConfigData(ctx context.Context, req *request, dir string,
}
depth += 1

// check recursivly for drone.yml
// check recursively for drone.yml
configData = ""
for _, f := range ls {
var fileContent string
if *f.Type == "dir" {
fileContent, _ = p.getAllConfigData(ctx, req, *f.Path, depth)
} else if *f.Type == "file" && *f.Name == req.Repo.Config {
if f.Type == "dir" {
fileContent, _ = p.getAllConfigData(ctx, req, f.Path, depth)
} else if f.Type == "file" && f.Name == req.Repo.Config {
var critical bool
fileContent, critical, err = p.getGithubDroneConfig(ctx, req, *f.Path)
fileContent, critical, err = p.getGithubDroneConfig(ctx, req, f.Path)
if critical {
return "", err
}
Expand All @@ -277,12 +270,11 @@ func (p *plugin) getAllConfigData(ctx context.Context, req *request, dir string,
}

return configData, nil

}

// droneConfigAppend concats multiple 'drone.yml's to a multi-machine pipeline
// see https://docs.drone.io/user-guide/pipeline/multi-machine/
func (p *plugin) droneConfigAppend(droneConfig string, appends ...string) string {
func (p *Plugin) droneConfigAppend(droneConfig string, appends ...string) string {
for _, a := range appends {
a = strings.Trim(a, " \n")
if a != "" {
Expand Down
Loading