-
Notifications
You must be signed in to change notification settings - Fork 42
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
Support the release entity for the GitHub provider #4921
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 == "" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like a weird case -- does it actually happen, or could we put in a complaint? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll be honest, I cargo-culted this. I'm not sure 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's make this an error and/or log at warning/error -- I think it's valid input to the GitHub API, but we shouldn't be getting output in this non-normalized form. |
||
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 | ||
} |
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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm assuming this will grow over time as we find the other useful properties.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's right. This applies to all of our entity properties.