Skip to content

Commit

Permalink
Support the release entity for the GitHub provider (#4921)
Browse files Browse the repository at this point in the history
* Support the release entity for the GitHub provider

Signed-off-by: Juan Antonio Osorio <[email protected]>

* Promote Release properties

Signed-off-by: Juan Antonio Osorio <[email protected]>

* Handle commit and branch

Signed-off-by: Juan Antonio Osorio <[email protected]>

* Fix getting commit

Signed-off-by: Juan Antonio Osorio <[email protected]>

---------

Signed-off-by: Juan Antonio Osorio <[email protected]>
  • Loading branch information
JAORMX authored Dec 17, 2024
1 parent 5169b22 commit c1f1acd
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 0 deletions.
10 changes: 10 additions & 0 deletions internal/entities/properties/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,13 @@ const (
// ArtifactPropertyType represents the type of the artifact (e.g 'container')
ArtifactPropertyType = "type"
)

// Release property keys
const (
// ReleasePropertyTag represents the release tag name.
ReleasePropertyTag = "tag"
// ReleasePropertyBranch represents the release branch
ReleasePropertyBranch = "branch"
// ReleaseCommitSHA represents the commit SHA of the release
ReleaseCommitSHA = "commit_sha"
)
2 changes: 2 additions & 0 deletions internal/providers/github/entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ func (c *GitHub) PropertiesToProtoMessage(
return ghprop.ArtifactV1FromProperties(props)
case minderv1.Entity_ENTITY_PULL_REQUESTS:
return ghprop.PullRequestV1FromProperties(props)
case minderv1.Entity_ENTITY_RELEASE:
return ghprop.EntityInstanceV1FromReleaseProperties(props)
}

return nil, fmt.Errorf("conversion of entity type %s is not handled by the github provider", entType)
Expand Down
2 changes: 2 additions & 0 deletions internal/providers/github/properties/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ func (_ ghEntityFetcher) EntityPropertyFetcher(entType minderv1.Entity) GhProper
return NewRepositoryFetcher()
case minderv1.Entity_ENTITY_ARTIFACTS:
return NewArtifactFetcher()
case minderv1.Entity_ENTITY_RELEASE:
return NewReleaseFetcher()
}

return nil
Expand Down
178 changes: 178 additions & 0 deletions internal/providers/github/properties/release.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors
// SPDX-License-Identifier: Apache-2.0

package properties

import (
"context"
"fmt"
"net/http"

go_github "github.com/google/go-github/v63/github"

"github.com/mindersec/minder/internal/entities/properties"
minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
v1 "github.com/mindersec/minder/pkg/providers/v1"
)

// Release Properties
const (
// ReleasePropertyOwner represents the github owner
ReleasePropertyOwner = "github/owner"
// ReleasePropertyRepo represents the github repo
ReleasePropertyRepo = "github/repo"
)

// ReleaseFetcher is a property fetcher for releases
type ReleaseFetcher struct {
propertyFetcherBase
}

// NewReleaseFetcher creates a new ReleaseFetcher
func NewReleaseFetcher() *ReleaseFetcher {
return &ReleaseFetcher{
propertyFetcherBase: propertyFetcherBase{
propertyOrigins: []propertyOrigin{
{
keys: []string{
// general entity
properties.PropertyName,
properties.PropertyUpstreamID,
// general release
properties.ReleasePropertyTag,
properties.ReleasePropertyBranch,
ReleasePropertyOwner,
ReleasePropertyRepo,
},
wrapper: getReleaseWrapper,
},
},
operationalProperties: []string{},
},
}
}

// GetName returns the name of the release
func (_ *ReleaseFetcher) GetName(props *properties.Properties) (string, error) {
owner := props.GetProperty(ReleasePropertyOwner).GetString()
repo, err := props.GetProperty(ReleasePropertyRepo).AsString()
if err != nil {
return "", fmt.Errorf("failed to get repo name: %w", err)
}

tag, err := props.GetProperty(properties.ReleasePropertyTag).AsString()
if err != nil {
return "", fmt.Errorf("failed to get tag name: %w", err)
}

return getReleaseNameFromParams(owner, repo, tag), nil
}

func getReleaseNameFromParams(owner, repo, tag string) string {
if owner == "" {
return fmt.Sprintf("%s/%s", repo, tag)
}

return fmt.Sprintf("%s/%s/%s", owner, repo, tag)
}

func getReleaseWrapper(
ctx context.Context, ghCli *go_github.Client, _ bool, getByProps *properties.Properties,
) (map[string]any, error) {
upstreamID, err := getByProps.GetProperty(properties.PropertyUpstreamID).AsInt64()
if err != nil {
return nil, fmt.Errorf("upstream ID not found or invalid: %w", err)
}

owner, err := getByProps.GetProperty(ReleasePropertyOwner).AsString()
if err != nil {
return nil, fmt.Errorf("owner not found or invalid: %w", err)
}

repo, err := getByProps.GetProperty(ReleasePropertyRepo).AsString()
if err != nil {
return nil, fmt.Errorf("repo not found or invalid: %w", err)
}

var fetchErr error
var release *go_github.RepositoryRelease
var result *go_github.Response
release, result, fetchErr = ghCli.Repositories.GetRelease(ctx, owner, repo,
upstreamID)
if fetchErr != nil {
if result != nil && result.StatusCode == http.StatusNotFound {
return nil, v1.ErrEntityNotFound
}
return nil, fmt.Errorf("failed to fetch release: %w", fetchErr)
}

branch, commitSha, err := getBranchAndCommit(ctx, owner, repo, release.GetTargetCommitish(), ghCli)
if err != nil {
return nil, fmt.Errorf("failed to get branch and commit SHA: %w", err)
}

return map[string]any{
properties.PropertyUpstreamID: properties.NumericalValueToUpstreamID(release.GetID()),
properties.PropertyName: getReleaseNameFromParams(owner, repo, release.GetTagName()),
ReleasePropertyOwner: owner,
ReleasePropertyRepo: repo,
properties.ReleasePropertyTag: release.GetTagName(),
properties.ReleaseCommitSHA: commitSha,
properties.ReleasePropertyBranch: branch,
}, nil
}

func getBranchAndCommit(
ctx context.Context,
owner string,
repo string,
commitish string,
ghCli *go_github.Client,
) (branch string, commitSha string, err error) {
if commitish == "" {
// We have no info, but this is not an error. We simply don't fill this
// information just yet. We'll get it on entity refresh.
return "", "", nil
}

// check if the target commitish is a branch
br, res, err := ghCli.Repositories.GetBranch(ctx, owner, repo, commitish, 1)
if err == nil {
return br.GetName(), br.GetCommit().GetSHA(), nil
}

if res == nil || res.StatusCode != http.StatusNotFound {
return "", "", fmt.Errorf("failed to fetch branch: %w", err)
}

// The commitish is a commit SHA without a branch
return "", commitish, nil
}

// EntityInstanceV1FromReleaseProperties creates a new EntityInstance from the given properties
func EntityInstanceV1FromReleaseProperties(props *properties.Properties) (*minderv1.EntityInstance, error) {
_, err := props.GetProperty(properties.PropertyUpstreamID).AsString()
if err != nil {
return nil, fmt.Errorf("upstream ID not found or invalid: %w", err)
}

tag, err := props.GetProperty(properties.ReleasePropertyTag).AsString()
if err != nil {
return nil, fmt.Errorf("tag not found or invalid: %w", err)
}

owner := props.GetProperty(ReleasePropertyOwner).GetString()

repo, err := props.GetProperty(ReleasePropertyRepo).AsString()
if err != nil {
return nil, fmt.Errorf("repo not found or invalid: %w", err)
}

name := getReleaseNameFromParams(owner, repo, tag)

return &minderv1.EntityInstance{
Type: minderv1.Entity_ENTITY_RELEASE,
Name: name,
Properties: props.ToProtoStruct(),
}, nil
}
147 changes: 147 additions & 0 deletions internal/providers/github/webhook/handlers_releases.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors
// SPDX-License-Identifier: Apache-2.0

package webhook

import (
"context"
"encoding/json"
"errors"
"fmt"

"github.com/mindersec/minder/internal/db"
entityMessage "github.com/mindersec/minder/internal/entities/handlers/message"
"github.com/mindersec/minder/internal/entities/properties"
ghprop "github.com/mindersec/minder/internal/providers/github/properties"
pb "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
"github.com/mindersec/minder/pkg/eventer/constants"
)

type releaseEvent struct {
Action *string `json:"action,omitempty"`
Release *release `json:"release,omitempty"`
Repo *repo `json:"repository,omitempty"`
}

func (r *releaseEvent) GetAction() string {
if r.Action != nil {
return *r.Action
}
return ""
}

func (r *releaseEvent) GetRelease() *release {
return r.Release
}

func (r *releaseEvent) GetRepo() *repo {
return r.Repo
}

type release struct {
ID *int64 `json:"id,omitempty"`
TagName *string `json:"tag_name,omitempty"`
Target *string `json:"target_commitish,omitempty"`
}

func (r *release) GetID() int64 {
if r.ID != nil {
return *r.ID
}
return 0
}

func (r *release) GetTagName() string {
if r.TagName != nil {
return *r.TagName
}
return ""
}

func (r *release) GetTarget() string {
if r.Target != nil {
return *r.Target
}
return ""
}

func processReleaseEvent(
ctx context.Context,
payload []byte,
) (*processingResult, error) {
var event *releaseEvent
if err := json.Unmarshal(payload, &event); err != nil {
return nil, fmt.Errorf("failed to unmarshal release event: %w", err)
}

if event.GetAction() == "" {
return nil, errors.New("release event action not found")
}

if event.GetRelease() == nil {
return nil, errors.New("release event release not found")
}

if event.GetRepo() == nil {
return nil, errors.New("release event repository not found")
}

if event.GetRelease().GetTagName() == "" {
return nil, errors.New("release event tag name not found")
}

if event.GetRelease().GetTarget() == "" {
return nil, errors.New("release event target not found")
}

return sendReleaseEvent(ctx, event)
}

func sendReleaseEvent(
_ context.Context,
event *releaseEvent,
) (*processingResult, error) {
lookByProps, err := properties.NewProperties(map[string]any{
properties.PropertyUpstreamID: properties.NumericalValueToUpstreamID(event.GetRelease().GetID()),
ghprop.ReleasePropertyOwner: event.GetRepo().GetOwner(),
ghprop.ReleasePropertyRepo: event.GetRepo().GetName(),
})
if err != nil {
return nil, fmt.Errorf("error creating release properties: %w", err)
}

originatorProps, err := properties.NewProperties(map[string]any{
properties.PropertyUpstreamID: properties.NumericalValueToUpstreamID(event.GetRepo().GetID()),
})
if err != nil {
return nil, fmt.Errorf("error creating repository properties for release origination: %w", err)
}

switch event.GetAction() {
case "published":
return &processingResult{
topic: constants.TopicQueueOriginatingEntityAdd,
wrapper: entityMessage.NewEntityRefreshAndDoMessage().
WithEntity(pb.Entity_ENTITY_RELEASE, lookByProps).
WithProviderImplementsHint(string(db.ProviderTypeGithub)).
WithOriginator(pb.Entity_ENTITY_REPOSITORIES, originatorProps),
}, nil
case "unpublished", "deleted":
return &processingResult{
topic: constants.TopicQueueOriginatingEntityDelete,
wrapper: entityMessage.NewEntityRefreshAndDoMessage().
WithEntity(pb.Entity_ENTITY_RELEASE, lookByProps).
WithProviderImplementsHint(string(db.ProviderTypeGithub)).
WithOriginator(pb.Entity_ENTITY_REPOSITORIES, originatorProps),
}, nil
case "edited":
return &processingResult{
topic: constants.TopicQueueRefreshEntityAndEvaluate,
wrapper: entityMessage.NewEntityRefreshAndDoMessage().
WithEntity(pb.Entity_ENTITY_RELEASE, lookByProps).
WithProviderImplementsHint(string(db.ProviderTypeGithub)).
WithOriginator(pb.Entity_ENTITY_REPOSITORIES, originatorProps),
}, nil
}
return nil, nil
}
3 changes: 3 additions & 0 deletions internal/providers/github/webhook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ func HandleWebhookEvent(
case "pull_request":
wes.Accepted = true
res, processingErr = processPullRequestEvent(ctx, rawWBPayload)
case "release":
wes.Accepted = true
res, processingErr = processReleaseEvent(ctx, rawWBPayload)
case "ping":
// For ping events, we do not set wes.Accepted
// to true because they're not relevant
Expand Down

0 comments on commit c1f1acd

Please sign in to comment.