Skip to content

Commit

Permalink
feat: add github_code_owner based on CODEOWNERS files
Browse files Browse the repository at this point in the history
  • Loading branch information
aminvielledebatAtBedrock committed Oct 2, 2022
1 parent 3e8e9f3 commit 790c7c2
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 0 deletions.
52 changes: 52 additions & 0 deletions docs/tables/github_code_owner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Table: github_code_owner

All of your code owners rules defined in your repository, in CODEOWNERS files.

The `github_code_owner` table can be used to query information about **ANY** repository, and **you must specify which repository** in the where or join clause (`where repository_full_name=`, `join github_code_owner on repository_full_name=`).

## Examples

### Get All your CodeOwners rules about a specific repository

```sql
select
line,
repository_full_name,
pattern,
users,
teams,
pre_comments,
line_comment
from
github.github_code_owner
where
repository_full_name = 'my-orga/code-owners'
order by
line asc
```

**CODEOWNERS file content**

```
# fooo Comment
# here
README @super-user
.github @super-user @my-orga/my-team
# another comment
# hello
.cloud @super-user @super-user # line comment here
.cloud/terraform @super-user # line comment here 2
/apps/github @super-user
```

**ResultSet**

| line | repository\_full\_name | pattern | users | teams | pre\_comments | line\_comment |
| ---- |------------------------| ---------------- | ------------------------------- | ---------------------- | --------------------------------- |---------------------|
| 3 | my-orga/code-owners | README | \["@super-user"\] | <null> | \["# fooo Comment","# here"\] | |
| 4 | my-orga/code-owners | .github | \["@super-user"\] | \["@my-orga/my-team"\] | \["# fooo Comment","# here"\] | |
| 8 | my-orga/code-owners | .cloud | \["@super-user","@super-user"\] | <null> | \["# another comment","# hello"\] | line comment here |
| 9 | my-orga/code-owners | .cloud/terraform | \["@super-user"\] | <null> | \["# another comment","# hello"\] | line comment here 2 |
| 11 | my-orga/code-owners | /apps/github | \["@super-user"\] | <null> | \[\] | |
1 change: 1 addition & 0 deletions github/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func Plugin(ctx context.Context) *plugin.Plugin {
"github_branch": tableGitHubBranch(ctx),
"github_commit": tableGitHubCommit(ctx),
"github_community_profile": tableGitHubCommunityProfile(ctx),
"github_code_owner": tableGithubCodeOwner(),
"github_gist": tableGitHubGist(),
"github_gitignore": tableGitHubGitignore(),
"github_issue": tableGitHubIssue(),
Expand Down
162 changes: 162 additions & 0 deletions github/table_github_code_owner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package github

import (
"context"
"github.com/google/go-github/v45/github"
"github.com/turbot/steampipe-plugin-sdk/v4/plugin/transform"
"strings"

"github.com/turbot/steampipe-plugin-sdk/v4/grpc/proto"
"github.com/turbot/steampipe-plugin-sdk/v4/plugin"
)

//// TABLE DEFINITION

func gitHubCodeOwnerColumns() []*plugin.Column {
return []*plugin.Column{
// Top columns
{Name: "repository_full_name", Type: proto.ColumnType_STRING, Description: "The full name of the repository, including the owner and repo name."},
// Other columns
{Name: "line", Type: proto.ColumnType_INT, Description: "The rule's line number in the CODEOWNERS file.", Transform: transform.FromField("LineNumber")},
{Name: "pattern", Type: proto.ColumnType_STRING, Description: "The pattern used to identify what code a team, or an individual is responsible for"},
{Name: "users", Type: proto.ColumnType_JSON, Description: "Users responsible for code in the repo"},
{Name: "teams", Type: proto.ColumnType_JSON, Description: "Teams responsible for code in the repo"},
{Name: "pre_comments", Type: proto.ColumnType_JSON, Description: "Specifies the comments added above a key."},
{Name: "line_comment", Type: proto.ColumnType_STRING, Description: "Specifies the comment following the node and before empty lines."},
}
}

//// TABLE DEFINITION

func tableGithubCodeOwner() *plugin.Table {
return &plugin.Table{
Name: "github_code_owner",
Description: "Individuals or teams that are responsible for code in a repository.",
List: &plugin.ListConfig{
Hydrate: tableGitHubCodeOwnerList,
ShouldIgnoreError: isNotFoundError([]string{"404"}),
KeyColumns: plugin.SingleColumn("repository_full_name"),
},
Columns: gitHubCodeOwnerColumns(),
}
}

// // LIST FUNCTION
type CodeOwnerRule struct {
LineNumber int
Pattern string
Users []string
Teams []string
PreComments []string
LineComment string
}

func tableGitHubCodeOwnerList(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
plugin.Logger(ctx).Trace("tableGitHubCodeOwnerList")
repoFullName := d.KeyColumnQuals["repository_full_name"].GetStringValue()
owner, repoName := parseRepoFullName(repoFullName)

type CodeOwnerRuleResponse struct {
RepositoryFullName string
LineNumber int
Pattern string
Users []string
Teams []string
PreComments []string
LineComment string
}
getCodeOwners := func(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
var fileContent *github.RepositoryContent
var err error

client := connect(ctx, d)
opt := &github.RepositoryContentGetOptions{}
// stop on the first found CODEOWNERS file
var paths = []string{".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"}
for _, path := range paths {
fileContent, _, _, err = client.Repositories.GetContents(ctx, owner, repoName, path, opt)
if err == nil {
break
}
}

// no CODEOWNERS file
if err != nil {
return []CodeOwnerRuleResponse{}, err
}

decodedContent, err := fileContent.GetContent()
if err != nil {
return []CodeOwnerRuleResponse{}, err
}

return decodeCodeOwnerFileContent(decodedContent), err
}

codeOwnersElements, err := retryHydrate(ctx, d, h, getCodeOwners)
if err != nil {
return nil, err
}

for _, codeOwner := range codeOwnersElements.([]*CodeOwnerRule) {
if codeOwner != nil {
d.StreamListItem(ctx, CodeOwnerRuleResponse{
RepositoryFullName: repoFullName,
LineNumber: codeOwner.LineNumber,
Pattern: codeOwner.Pattern,
Users: codeOwner.Users,
Teams: codeOwner.Teams,
PreComments: codeOwner.PreComments,
LineComment: codeOwner.LineComment,
})
}
}
return nil, nil
}

func decodeCodeOwnerFileContent(content string) []*CodeOwnerRule {
var codeOwnerRules []*CodeOwnerRule

var comments []string
for i, line := range strings.Split(content, "\n") {
lineNumber := i + 1
if len(line) == 0 {
comments = []string{}
continue
}
// code block with comments
if strings.HasPrefix(line, "#") {
comments = append(comments, line)
continue
}
// code owners rule
rule := strings.SplitN(line, " ", 2)
if len(rule) < 2 {
comments = []string{}
continue
}

var pattern, lineComment string
pattern = rule[0]

// line comment
ownersAndComment := strings.SplitN(rule[1], "#", 2)
if len(ownersAndComment) == 2 && len(ownersAndComment[1]) > 0 {
lineComment = ownersAndComment[1]
} else {
ownersAndComment = []string{rule[1]}
}

// owners computing
var users, teams []string
for _, owner := range strings.Split(strings.TrimSpace(ownersAndComment[0]), " ") {
if strings.Index(owner, "/") > 0 {
teams = append(teams, owner)
} else {
users = append(users, owner)
}
}
codeOwnerRules = append(codeOwnerRules, &CodeOwnerRule{LineNumber: lineNumber, Pattern: pattern, Users: users, Teams: teams, PreComments: comments, LineComment: lineComment})
}
return codeOwnerRules
}

0 comments on commit 790c7c2

Please sign in to comment.