From c80162a5b2ad6fcf8def201e0f4ee42fc9b5562f Mon Sep 17 00:00:00 2001 From: Billy Keyes Date: Wed, 11 Jul 2018 17:04:12 -0700 Subject: [PATCH] Add support for multiple config file paths (#44) Based on server configuration, the application will now look for the repository configuration file in multiple paths until it finds one. If no paths are specified, the existing .bulldozer.yml is used. This commit makes the minimal amount of change necessary to get this working. Ideally, there would be more refactoring to standardize config passing and client construction, but I'm not ready to do that yet. --- github/github.go | 70 ++++++++++++++++++++++++++++++++-------- github/github_test.go | 38 +++++++++++++++++++++- server/config/config.go | 11 ++++--- server/endpoints/hook.go | 4 +-- server/server.go | 2 +- 5 files changed, 102 insertions(+), 23 deletions(-) diff --git a/github/github.go b/github/github.go index 51e358857..5204a84a7 100644 --- a/github/github.go +++ b/github/github.go @@ -56,6 +56,7 @@ const ( var ( AcceptedPermLevels = []string{"write", "admin"} + DefaultConfigPaths = []string{".bulldozer.yml"} ) type UpdateStrategy string @@ -70,11 +71,21 @@ const ( UpdateStrategyAlways UpdateStrategy = "always" ) +type Option func(c *Client) + +func WithConfigPaths(paths []string) Option { + return func(c *Client) { + c.configPaths = paths + } +} + type Client struct { Logger *logrus.Entry Ctx context.Context *github.Client + + configPaths []string } type BulldozerFile struct { @@ -84,21 +95,35 @@ type BulldozerFile struct { UpdateStrategy UpdateStrategy `yaml:"updateStrategy"` } -func FromToken(c echo.Context, token string) *Client { +func FromToken(c echo.Context, token string, opts ...Option) *Client { ts := oauth2.StaticTokenSource( &oauth2.Token{AccessToken: token}, ) tc := oauth2.NewClient(context.TODO(), ts) - client := github.NewClient(tc) + gh := github.NewClient(tc) + + gh.BaseURL, _ = url.Parse(config.Instance.Github.APIURL) + gh.UserAgent = "bulldozer/" + version.Version() - client.BaseURL, _ = url.Parse(config.Instance.Github.APIURL) - client.UserAgent = "bulldozer/" + version.Version() + client := &Client{ + Logger: log.FromContext(c), + Ctx: context.TODO(), + Client: gh, + } - return &Client{log.FromContext(c), context.TODO(), client} + for _, opt := range opts { + opt(client) + } + + if len(client.configPaths) == 0 { + client.configPaths = DefaultConfigPaths + } + + return client } -func FromAuthHeader(c echo.Context, authHeader string) (*Client, error) { +func FromAuthHeader(c echo.Context, authHeader string, opts ...Option) (*Client, error) { if authHeader == "" { return nil, errors.New("authorization header not present") } @@ -109,18 +134,13 @@ func FromAuthHeader(c echo.Context, authHeader string) (*Client, error) { } token := parts[1] - return FromToken(c, token), nil + return FromToken(c, token, opts...), nil } func (client *Client) ConfigFile(repo *github.Repository, ref string) (*BulldozerFile, error) { - owner := repo.Owner.GetLogin() - name := repo.GetName() - - repositoryContent, _, _, err := client.Repositories.GetContents(client.Ctx, owner, name, ".bulldozer.yml", &github.RepositoryContentGetOptions{ - Ref: ref, - }) + repositoryContent, err := client.findConfigFile(repo, ref) if err != nil { - return nil, errors.Wrapf(err, "cannot get .bulldozer.yml for %s on ref %s", repo.GetFullName(), ref) + return nil, err } content, err := repositoryContent.GetContent() @@ -155,6 +175,28 @@ func (client *Client) ConfigFile(repo *github.Repository, ref string) (*Bulldoze return &bulldozerFile, nil } +func (client *Client) findConfigFile(repo *github.Repository, ref string) (*github.RepositoryContent, error) { + owner := repo.Owner.GetLogin() + name := repo.GetName() + + opts := github.RepositoryContentGetOptions{ + Ref: ref, + } + + for _, p := range client.configPaths { + content, _, _, err := client.Repositories.GetContents(client.Ctx, owner, name, p, &opts) + if err != nil { + if rerr, ok := err.(*github.ErrorResponse); ok && rerr.Response.StatusCode == http.StatusNotFound { + continue + } + return nil, errors.Wrapf(err, "cannot get %s for %s on ref %s", p, repo.GetFullName(), ref) + } + return content, nil + } + + return nil, errors.Errorf("no configuration found for %s on ref %s", repo.GetFullName(), ref) +} + func (client *Client) MergeMethod(branch *github.PullRequestBranch) (string, error) { logger := client.Logger diff --git a/github/github_test.go b/github/github_test.go index bba618213..6f0767b6e 100644 --- a/github/github_test.go +++ b/github/github_test.go @@ -42,7 +42,14 @@ func setup() { server = httptest.NewServer(mux) logger := logrus.New().WithField("deliveryID", "randomDelivery") - client = &Client{logger, context.TODO(), github.NewClient(nil)} + client = &Client{ + Logger: logger, + Ctx: context.TODO(), + Client: github.NewClient(nil), + + configPaths: []string{".bulldozer.yml", ".palantir/bulldozer.yml"}, + } + url, _ := url.Parse(server.URL + "/") client.BaseURL = url client.UploadURL = url @@ -529,6 +536,35 @@ func TestConfigFileSuccess(t *testing.T) { } } +func TestConfigFileAlternatePathSuccess(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/contents/.palantir/bulldozer.yml", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "type": "file", + "encoding": "base64", + "content": "bW9kZTogd2hpdGVsaXN0CnN0cmF0ZWd5OiBzcXVhc2gKZGVsZXRlQWZ0ZXJNZXJnZTogdHJ1ZQp1cGRhdGVTdHJhdGVneTogbGFiZWwK", + "name": ".palantir/bulldozer.yml", + "path": ".palantir/bulldozer.yml" + }`) + }) + + want := &BulldozerFile{ + Mode: "whitelist", + MergeStrategy: "squash", + UpdateStrategy: UpdateStrategyLabel, + DeleteAfterMerge: true, + } + configFile, err := client.ConfigFile(fakeRepository("r"), "develop") + require.Nil(t, err) + + if !reflect.DeepEqual(configFile, want) { + t.Errorf("ConfigFile returned %+v, want %+v", configFile, want) + } +} + func TestConfigFileInvalid(t *testing.T) { setup() defer teardown() diff --git a/server/config/config.go b/server/config/config.go index 7c6f593fb..d628899a5 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -39,11 +39,12 @@ var ( ) type Startup struct { - Server Rest `yaml:"rest" validate:"required"` - Logging LoggingConfig `yaml:"logging" validate:"required,dive"` - Database *DatabaseConfig `yaml:"database" validate:"required,dive"` - Github *GithubConfig `yaml:"github" validate:"required,dive"` - AssetDir string `yaml:"assetDir" validate:"required"` + Server Rest `yaml:"rest" validate:"required"` + Logging LoggingConfig `yaml:"logging" validate:"required,dive"` + Database *DatabaseConfig `yaml:"database" validate:"required,dive"` + Github *GithubConfig `yaml:"github" validate:"required,dive"` + AssetDir string `yaml:"assetDir" validate:"required"` + ConfigPaths []string `yaml:"configPaths"` } type Rest struct { diff --git a/server/endpoints/hook.go b/server/endpoints/hook.go index e8189c30b..db5643f47 100644 --- a/server/endpoints/hook.go +++ b/server/endpoints/hook.go @@ -31,7 +31,7 @@ import ( "github.com/palantir/bulldozer/server/config" ) -func Hook(db *sqlx.DB, secret string) echo.HandlerFunc { +func Hook(db *sqlx.DB, secret string, configPaths []string) echo.HandlerFunc { return func(c echo.Context) error { logger := log.FromContext(c) @@ -56,7 +56,7 @@ func Hook(db *sqlx.DB, secret string) echo.HandlerFunc { return errors.Wrapf(err, "cannot get user %s from database", dbRepo.EnabledBy) } - ghClient := gh.FromToken(c, user.Token) + ghClient := gh.FromToken(c, user.Token, gh.WithConfigPaths(configPaths)) if !(result.Update || result.Merge) { return c.String(http.StatusOK, "Not taking action") diff --git a/server/server.go b/server/server.go index 8b6766b65..90600aa82 100644 --- a/server/server.go +++ b/server/server.go @@ -74,7 +74,7 @@ func registerEndpoints(startup *config.Startup, e *echo.Echo, db *sqlx.DB) { e.POST("/api/repo/:owner/:name", endpoints.RepositoryEnable(db, startup.Github.WebHookURL, startup.Github.WebhookSecret)) e.DELETE("/api/repo/:owner/:name", endpoints.RepositoryDisable(db)) - e.POST("/api/github/hook", endpoints.Hook(db, startup.Github.WebhookSecret)) + e.POST("/api/github/hook", endpoints.Hook(db, startup.Github.WebhookSecret, startup.ConfigPaths)) e.GET("/api/auth/github/token", endpoints.Token(db)) }