-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support the release entity for the GitHub provider (#4921)
* 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
Showing
6 changed files
with
342 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters