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

gitlab: Add support for pull requests #4641

Merged
merged 6 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/docs/ref/proto.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion internal/providers/gitlab/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,6 @@ func (c *gitlabClient) GetCredential() provifv1.GitLabCredential {

// SupportsEntity implements the Provider interface
func (_ *gitlabClient) SupportsEntity(entType minderv1.Entity) bool {
return entType == minderv1.Entity_ENTITY_REPOSITORIES
return entType == minderv1.Entity_ENTITY_REPOSITORIES ||
entType == minderv1.Entity_ENTITY_PULL_REQUESTS
}
78 changes: 78 additions & 0 deletions internal/providers/gitlab/manager/webhook_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ func (m *providerClassManager) getWebhookEventDispatcher(
return m.handleRepoPush
case gitlablib.EventTypeTagPush:
return m.handleTagPush
case gitlablib.EventTypeMergeRequest:
return m.handleMergeRequest
default:
return m.handleNoop
}
Expand Down Expand Up @@ -98,6 +100,41 @@ func (m *providerClassManager) handleTagPush(l zerolog.Logger, r *http.Request)
return m.publishRefreshAndEvalForGitlabProject(l, rawID)
}

func (m *providerClassManager) handleMergeRequest(l zerolog.Logger, r *http.Request) error {
l.Debug().Msg("handling merge request event")

mergeRequestEvent := gitlablib.MergeEvent{}
if err := decodeJSONSafe(r.Body, &mergeRequestEvent); err != nil {
l.Error().Err(err).Msg("error decoding merge request event")
return fmt.Errorf("error decoding merge request event: %w", err)
}

mrID := mergeRequestEvent.ObjectAttributes.IID
if mrID == 0 {
return fmt.Errorf("merge request event missing IID")
}

rawID := mergeRequestEvent.Project.ID
if rawID == 0 {
return fmt.Errorf("merge request event missing project ID")
}

switch {
case mergeRequestEvent.ObjectAttributes.Action == "open",
mergeRequestEvent.ObjectAttributes.Action == "reopen":
return m.publishMergeRequestMessage(rawID, mrID,
events.TopicQueueOriginatingEntityAdd)
case mergeRequestEvent.ObjectAttributes.Action == "close":
return m.publishMergeRequestMessage(rawID, mrID,
events.TopicQueueOriginatingEntityDelete)
case mergeRequestEvent.ObjectAttributes.Action == "update":
return m.publishMergeRequestMessage(rawID, mrID,
events.TopicQueueRefreshEntityAndEvaluate)
default:
return nil
}
}

func (m *providerClassManager) publishRefreshAndEvalForGitlabProject(
l zerolog.Logger, rawProjectID int) error {
upstreamID := gitlab.FormatRepositoryUpstreamID(rawProjectID)
Expand Down Expand Up @@ -134,6 +171,47 @@ func (m *providerClassManager) publishRefreshAndEvalForGitlabProject(
return nil
}

func (m *providerClassManager) publishMergeRequestMessage(mrID int, rawProjectID int, queueTopic string) error {
mrUpstreamID := gitlab.FormatPullRequestUpstreamID(mrID)
mrProjectID := gitlab.FormatRepositoryUpstreamID(rawProjectID)

// Form identifying properties
identifyingProps, err := properties.NewProperties(map[string]any{
properties.PropertyUpstreamID: mrUpstreamID,
gitlab.PullRequestProjectID: mrProjectID,
})
if err != nil {
return fmt.Errorf("error creating identifying properties: %w", err)
}

repoIdentifyingProps, err := properties.NewProperties(map[string]any{
properties.PropertyUpstreamID: mrProjectID,
})
if err != nil {
return fmt.Errorf("error creating repo identifying properties: %w", err)
}

// Form message to publish
outm := entmsg.NewEntityRefreshAndDoMessage()
outm.WithEntity(minderv1.Entity_ENTITY_PULL_REQUESTS, identifyingProps)
outm.WithOriginator(minderv1.Entity_ENTITY_REPOSITORIES, repoIdentifyingProps)
outm.WithProviderClassHint(gitlab.Class)

// Convert message for publishing
msgID := uuid.New().String()
msg := message.NewMessage(msgID, nil)
if err := outm.ToMessage(msg); err != nil {
return fmt.Errorf("error converting message to protobuf: %w", err)
}

// Publish message
if err := m.pub.Publish(queueTopic, msg); err != nil {
return fmt.Errorf("error publishing refresh and eval message: %w", err)
}

return nil
}

func decodeJSONSafe[T any](r io.ReadCloser, v *T) error {
rs := wrapSafe(r)
defer r.Close()
Expand Down
81 changes: 79 additions & 2 deletions internal/providers/gitlab/properties.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1"
)

// Repository Properties
const (
// RepoPropertyProjectName represents the gitlab project
RepoPropertyProjectName = "gitlab/project_name"
Expand All @@ -43,6 +44,24 @@ const (
RepoPropertyHookURL = "gitlab/hook_url"
)

// Pull Request Properties
const (
// PullRequestProjectID represents the gitlab project ID
PullRequestProjectID = "gitlab/project_id"
// PullRequestNumber represents the gitlab merge request number
PullRequestNumber = "gitlab/merge_request_number"
// PullRequestSourceBranch represents the gitlab source branch
PullRequestSourceBranch = "gitlab/source_branch"
// PullRequestTargetBranch represents the gitlab target branch
PullRequestTargetBranch = "gitlab/target_branch"
// PullRequestAuthor represents the gitlab author
PullRequestAuthor = "gitlab/author"
// PullRequestCommitSHA represents the gitlab commit SHA
PullRequestCommitSHA = "gitlab/commit_sha"
// PullRequestURL represents the gitlab merge request URL
PullRequestURL = "gitlab/merge_request_url"
)

// FetchAllProperties implements the provider interface
func (c *gitlabClient) FetchAllProperties(
ctx context.Context, getByProps *properties.Properties, entType minderv1.Entity, _ *properties.Properties,
Expand All @@ -51,7 +70,15 @@ func (c *gitlabClient) FetchAllProperties(
return nil, fmt.Errorf("entity type %s not supported", entType)
}

return c.getPropertiesForRepo(ctx, getByProps)
//nolint:exhaustive // We only support two entity types for now.
switch entType {
case minderv1.Entity_ENTITY_REPOSITORIES:
return c.getPropertiesForRepo(ctx, getByProps)
case minderv1.Entity_ENTITY_PULL_REQUESTS:
return c.getPropertiesForPullRequest(ctx, getByProps)
default:
return nil, fmt.Errorf("entity type %s not supported", entType)
}
}

// FetchProperty implements the provider interface
Expand All @@ -71,6 +98,18 @@ func (c *gitlabClient) GetEntityName(entityType minderv1.Entity, props *properti
return "", fmt.Errorf("entity type %s not supported", entityType)
}

//nolint:exhaustive // We only support two entity types for now.
switch entityType {
case minderv1.Entity_ENTITY_REPOSITORIES:
return getRepoNameFromProperties(props)
case minderv1.Entity_ENTITY_PULL_REQUESTS:
return getPullRequestNameFromProperties(props)
default:
return "", fmt.Errorf("entity type %s not supported", entityType)
}
}

func getRepoNameFromProperties(props *properties.Properties) (string, error) {
groupName, err := getStringProp(props, RepoPropertyNamespace)
if err != nil {
return "", err
Expand All @@ -84,6 +123,25 @@ func (c *gitlabClient) GetEntityName(entityType minderv1.Entity, props *properti
return formatRepoName(groupName, projectName), nil
}

func getPullRequestNameFromProperties(props *properties.Properties) (string, error) {
groupName, err := getStringProp(props, RepoPropertyNamespace)
if err != nil {
return "", err
}

projectName, err := getStringProp(props, RepoPropertyProjectName)
if err != nil {
return "", err
}

iid, err := getStringProp(props, PullRequestNumber)
if err != nil {
return "", err
}

return formatPullRequestName(groupName, projectName, iid), nil
}

// PropertiesToProtoMessage implements the ProtoMessageConverter interface
func (c *gitlabClient) PropertiesToProtoMessage(
entType minderv1.Entity, props *properties.Properties,
Expand All @@ -92,7 +150,15 @@ func (c *gitlabClient) PropertiesToProtoMessage(
return nil, fmt.Errorf("entity type %s is not supported by the gitlab provider", entType)
}

return repoV1FromProperties(props)
//nolint:exhaustive // We only support two entity types for now.
switch entType {
case minderv1.Entity_ENTITY_REPOSITORIES:
return repoV1FromProperties(props)
case minderv1.Entity_ENTITY_PULL_REQUESTS:
return pullRequestV1FromProperties(props)
default:
return nil, fmt.Errorf("entity type %s not supported", entType)
}
}

// FormatRepositoryUpstreamID returns the upstream ID for a gitlab project
Expand All @@ -102,6 +168,13 @@ func FormatRepositoryUpstreamID(id int) string {
return fmt.Sprintf("%d", id)
}

// FormatPullRequestUpstreamID returns the upstream ID for a gitlab merge request
// This is done so we don't have to deal with conversions in the provider
// when dealing with entities
func FormatPullRequestUpstreamID(id int) string {
return fmt.Sprintf("%d", id)
}

func getStringProp(props *properties.Properties, key string) (string, error) {
value, err := props.GetProperty(key).AsString()
if err != nil {
Expand All @@ -114,3 +187,7 @@ func getStringProp(props *properties.Properties, key string) (string, error) {
func formatRepoName(groupName, projectName string) string {
return groupName + "/" + projectName
}

func formatPullRequestName(groupName, projectName, iid string) string {
return fmt.Sprintf("%s/%s/%s", groupName, projectName, iid)
}
Loading