Skip to content

Commit

Permalink
Merge branch 'main' into fix/pr-fork-logic
Browse files Browse the repository at this point in the history
  • Loading branch information
ecrupper committed Feb 22, 2024
2 parents 85c7a06 + b0449f8 commit 1617c4c
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 1 deletion.
15 changes: 14 additions & 1 deletion api/repo/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ func CreateRepo(c *gin.Context) {
maxBuildLimit := c.Value("maxBuildLimit").(int64)
defaultRepoEvents := c.Value("defaultRepoEvents").([]string)
defaultRepoEventsMask := c.Value("defaultRepoEventsMask").(int64)
defaultRepoApproveBuild := c.Value("defaultRepoApproveBuild").(string)

ctx := c.Request.Context()

Expand Down Expand Up @@ -149,9 +150,21 @@ func CreateRepo(c *gin.Context) {

// set the fork policy field based off the input provided
if len(input.GetApproveBuild()) > 0 {
// ensure the approve build setting matches one of the expected values
if input.GetApproveBuild() != constants.ApproveForkAlways &&
input.GetApproveBuild() != constants.ApproveForkNoWrite &&
input.GetApproveBuild() != constants.ApproveNever &&
input.GetApproveBuild() != constants.ApproveOnce {
retErr := fmt.Errorf("approve_build of %s is invalid", input.GetApproveBuild())

util.HandleError(c, http.StatusBadRequest, retErr)

return
}

r.SetApproveBuild(input.GetApproveBuild())
} else {
r.SetApproveBuild(constants.ApproveForkAlways)
r.SetApproveBuild(defaultRepoApproveBuild)
}

// fields restricted to platform admins
Expand Down
12 changes: 12 additions & 0 deletions api/repo/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,18 @@ func UpdateRepo(c *gin.Context) {
}

if len(input.GetApproveBuild()) > 0 {
// ensure the approve build setting matches one of the expected values
if input.GetApproveBuild() != constants.ApproveForkAlways &&
input.GetApproveBuild() != constants.ApproveForkNoWrite &&
input.GetApproveBuild() != constants.ApproveNever &&
input.GetApproveBuild() != constants.ApproveOnce {
retErr := fmt.Errorf("approve_build of %s is invalid", input.GetApproveBuild())

util.HandleError(c, http.StatusBadRequest, retErr)

return
}

// update fork policy if set
r.SetApproveBuild(input.GetApproveBuild())
}
Expand Down
20 changes: 20 additions & 0 deletions api/webhook/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,26 @@ func PostWebhook(c *gin.Context) {
return
}

fallthrough
case constants.ApproveOnce:
// determine if build sender is in the contributors list for the repo
//
// NOTE: this call is cumbersome for repos with lots of contributors. Potential TODO: improve this if
// GitHub adds a single-contributor API endpoint.
contributor, err := scm.FromContext(c).RepoContributor(ctx, u, b.GetSender(), r.GetOrg(), r.GetName())
if err != nil {
util.HandleError(c, http.StatusInternalServerError, err)
}

if !contributor {
err = gatekeepBuild(c, b, repo, u)
if err != nil {
util.HandleError(c, http.StatusInternalServerError, err)
}

return
}

fallthrough
case constants.ApproveNever:
fallthrough
Expand Down
6 changes: 6 additions & 0 deletions cmd/vela-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ func main() {
Name: "default-repo-events-mask",
Usage: "set default event mask for newly activated repositories",
},
&cli.StringFlag{
EnvVars: []string{"VELA_DEFAULT_REPO_APPROVE_BUILD"},
Name: "default-repo-approve-build",
Usage: "override default approve build for newly activated repositories",
Value: constants.ApproveForkAlways,
},
// Token Manager Flags
&cli.DurationFlag{
EnvVars: []string{"VELA_USER_ACCESS_TOKEN_DURATION", "USER_ACCESS_TOKEN_DURATION"},
Expand Down
1 change: 1 addition & 0 deletions cmd/vela-server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ func server(c *cli.Context) error {
middleware.Worker(c.Duration("worker-active-interval")),
middleware.DefaultRepoEvents(c.StringSlice("default-repo-events")),
middleware.DefaultRepoEventsMask(c.Int64("default-repo-events-mask")),
middleware.DefaultRepoApproveBuild(c.String("default-repo-approve-build")),
middleware.AllowlistSchedule(c.StringSlice("vela-schedule-allowlist")),
middleware.ScheduleFrequency(c.Duration("schedule-minimum-frequency")),
)
Expand Down
7 changes: 7 additions & 0 deletions cmd/vela-server/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ func validateCore(c *cli.Context) error {
}
}

if c.String("default-repo-approve-build") != constants.ApproveForkAlways &&
c.String("default-repo-approve-build") != constants.ApproveNever &&
c.String("default-repo-approve-build") != constants.ApproveForkNoWrite &&
c.String("default-repo-approve-build") != constants.ApproveOnce {
return fmt.Errorf("default-repo-approve-build (VELA_DEFAULT_REPO_APPROVE_BUILD) has the unsupported value of %s", c.String("default-repo-approve-build"))
}

return nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,12 @@ func DefaultRepoEventsMask(defaultRepoEventsMask int64) gin.HandlerFunc {
c.Next()
}
}

// DefaultRepoApproveBuild is a middleware function that attaches the defaultRepoApproveBuild
// to enable the server to override the default repo approve build setting.
func DefaultRepoApproveBuild(defaultRepoApproveBuild string) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("defaultRepoApproveBuild", defaultRepoApproveBuild)
c.Next()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,36 @@ func TestMiddleware_DefaultRepoEventsMask(t *testing.T) {
t.Errorf("DefaultRepoEventsMask is %v, want %v", got, want)
}
}

func TestMiddleware_DefaultRepoApproveBuild(t *testing.T) {
// setup types
var got string

want := "fork-no-write"

// setup context
gin.SetMode(gin.TestMode)

resp := httptest.NewRecorder()
context, engine := gin.CreateTestContext(resp)
context.Request, _ = http.NewRequest(http.MethodGet, "/health", nil)

// setup mock server
engine.Use(DefaultRepoApproveBuild(want))
engine.GET("/health", func(c *gin.Context) {
got = c.Value("defaultRepoApproveBuild").(string)

c.Status(http.StatusOK)
})

// run test
engine.ServeHTTP(context.Writer, context.Request)

if resp.Code != http.StatusOK {
t.Errorf("DefaultRepoApproveBuild returned %v, want %v", resp.Code, http.StatusOK)
}

if !reflect.DeepEqual(got, want) {
t.Errorf("DefaultRepoApproveBuild is %v, want %v", got, want)
}
}
45 changes: 45 additions & 0 deletions scm/github/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,48 @@ func (c *client) ListUsersTeamsForOrg(ctx context.Context, u *library.User, org

return userTeams, nil
}

// RepoContributor lists all contributors from a repository and checks if the sender is one of the contributors.
func (c *client) RepoContributor(ctx context.Context, owner *library.User, sender, org, repo string) (bool, error) {
c.Logger.WithFields(logrus.Fields{
"org": org,
"repo": repo,
"user": sender,
}).Tracef("capturing %s contributor status for repo %s/%s", sender, org, repo)

// create GitHub OAuth client with repo owner's token
client := c.newClientToken(owner.GetToken())

// set the max per page for the options to capture the list of repos
opts := github.ListContributorsOptions{
ListOptions: github.ListOptions{
PerPage: 100, // 100 is max
},
}

for {
// send API call to list all contributors for repository
contributors, resp, err := client.Repositories.ListContributors(ctx, org, repo, &opts)
if err != nil {
return false, err
}

// match login to sender to see if they are a contributor
//
// check this as we page through the results to spare API
for _, contributor := range contributors {
if strings.EqualFold(contributor.GetLogin(), sender) {
return true, nil
}
}

// break the loop if there is no more results to page through
if resp.NextPage == 0 {
break
}

opts.Page = resp.NextPage
}

return false, nil
}
77 changes: 77 additions & 0 deletions scm/github/access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,3 +411,80 @@ func TestGithub_TeamList(t *testing.T) {
t.Errorf("TeamAccess is %v, want %v", got, want)
}
}

func TestGithub_RepoContributor(t *testing.T) {
// setup context
gin.SetMode(gin.TestMode)

resp := httptest.NewRecorder()
_, engine := gin.CreateTestContext(resp)

// setup mock server
engine.GET("/api/v3/repos/:org/:repo/contributors", func(c *gin.Context) {
if c.Param("org") != "github" {
c.Status(http.StatusNotFound)

return
}

c.Header("Content-Type", "application/json")
c.Status(http.StatusOK)
c.File("testdata/list_contributors.json")
})

s := httptest.NewServer(engine)
defer s.Close()

// setup types
u := new(library.User)
u.SetName("foo")
u.SetToken("bar")

tests := []struct {
name string
sender string
org string
repo string
want bool
wantErr bool
}{
{
name: "repo contributor",
sender: "octocat",
org: "github",
repo: "example",
want: true,
},
{
name: "repo non-contributor",
sender: "userA",
org: "github",
repo: "example",
want: false,
},
{
name: "repo not found",
sender: "octocat",
org: "foo",
repo: "example",
wantErr: true,
},
}

client, _ := NewTest(s.URL)

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := client.RepoContributor(context.TODO(), u, tt.sender, tt.org, tt.repo)

if (err != nil) != tt.wantErr {
t.Errorf("RepoContributor() error = %v, wantErr %v", err, tt.wantErr)
return
}

if got != tt.want {
t.Errorf("RepoContributor() = %v, want %v", got, tt.want)
}
})
}
}
44 changes: 44 additions & 0 deletions scm/github/testdata/list_contributors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[
{
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false,
"contributions": 32
},
{
"login": "octokitty",
"id": 2,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false,
"contributions": 32
}
]
3 changes: 3 additions & 0 deletions scm/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ type Service interface {
// TeamAccess defines a function that captures
// the user's access level for a team.
TeamAccess(context.Context, *library.User, string, string) (string, error)
// RepoContributor defines a function that captures
// whether the user is a contributor for a repo.
RepoContributor(context.Context, *library.User, string, string, string) (bool, error)

// Teams SCM Interface Functions

Expand Down

0 comments on commit 1617c4c

Please sign in to comment.