From ec7b12f2742d91976633a5dac937aedabc080038 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Fri, 8 Nov 2024 16:30:02 +0200 Subject: [PATCH] Support the release entity for the GitHub provider Signed-off-by: Juan Antonio Osorio --- internal/providers/github/entities.go | 2 + .../providers/github/properties/fetcher.go | 2 + .../providers/github/properties/release.go | 154 ++++++++++++++++++ .../github/webhook/handlers_releases.go | 147 +++++++++++++++++ internal/providers/github/webhook/hook.go | 3 + 5 files changed, 308 insertions(+) create mode 100644 internal/providers/github/properties/release.go create mode 100644 internal/providers/github/webhook/handlers_releases.go diff --git a/internal/providers/github/entities.go b/internal/providers/github/entities.go index 8be40fec26..405ca72c28 100644 --- a/internal/providers/github/entities.go +++ b/internal/providers/github/entities.go @@ -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) diff --git a/internal/providers/github/properties/fetcher.go b/internal/providers/github/properties/fetcher.go index 169c2a84e4..c6c55a9a50 100644 --- a/internal/providers/github/properties/fetcher.go +++ b/internal/providers/github/properties/fetcher.go @@ -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 diff --git a/internal/providers/github/properties/release.go b/internal/providers/github/properties/release.go new file mode 100644 index 0000000000..957119f316 --- /dev/null +++ b/internal/providers/github/properties/release.go @@ -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 +} diff --git a/internal/providers/github/webhook/handlers_releases.go b/internal/providers/github/webhook/handlers_releases.go new file mode 100644 index 0000000000..70feec9223 --- /dev/null +++ b/internal/providers/github/webhook/handlers_releases.go @@ -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 +} diff --git a/internal/providers/github/webhook/hook.go b/internal/providers/github/webhook/hook.go index 14e3236ec2..a4d42d8f37 100644 --- a/internal/providers/github/webhook/hook.go +++ b/internal/providers/github/webhook/hook.go @@ -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