Skip to content

Commit

Permalink
Support the release entity for the GitHub provider
Browse files Browse the repository at this point in the history
Signed-off-by: Juan Antonio Osorio <ozz@stacklok.com>
  • Loading branch information
JAORMX committed Dec 12, 2024
1 parent 8502c32 commit eb64d08
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 0 deletions.
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
154 changes: 154 additions & 0 deletions internal/providers/github/properties/release.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// 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"
// ReleasePropertyTag represents the github release tag name.
ReleasePropertyTag = "github/tag"
// ReleasePropertyBranch represents the github release branch
ReleasePropertyBranch = "github/branch"
)

// 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
ReleasePropertyTag,
ReleasePropertyBranch,
},
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(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) {
// TODO: Should I be parsing this as string or int64?
// if string, then I should convert it to int64
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)
}

return map[string]any{
properties.PropertyUpstreamID: properties.NumericalValueToUpstreamID(release.GetID()),
properties.PropertyName: getReleaseNameFromParams(owner, repo, release.GetTagName()),
ReleasePropertyOwner: owner,
ReleasePropertyRepo: repo,
ReleasePropertyTag: release.GetTagName(),
ReleasePropertyBranch: release.GetTargetCommitish(),
}, 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(ReleasePropertyTag).AsString()
if err != nil {
return nil, fmt.Errorf("tag not found or invalid: %w", err)
}

_, err = props.GetProperty(ReleasePropertyBranch).AsString()
if err != nil {
return nil, fmt.Errorf("branch 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 eb64d08

Please sign in to comment.