Skip to content

Commit

Permalink
Move repository create to RepositoryService (mindersec#2632)
Browse files Browse the repository at this point in the history
This completes the current refactoring of repository logic.

One small behaviour change is introduced: when the create logic is
unable to insert the new repo into the DB, it will attempts to delete
the webhook which it created. The previous logic would just leave the
webhook in the customer's github repo.
  • Loading branch information
dmjb authored Mar 14, 2024
1 parent 49b1977 commit d68e8e6
Show file tree
Hide file tree
Showing 7 changed files with 483 additions and 372 deletions.
86 changes: 0 additions & 86 deletions internal/controlplane/handlers_githubwebhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,92 +213,6 @@ func handleParseError(typ string, parseErr error) *metrics.WebhookEventState {
return state
}

// registerWebhookForRepository registers a set repository and sets up the webhook for each of them
// and returns the registration result for each repository.
// If an error occurs, the registration is aborted and the error is returned.
// https://docs.github.com/en/rest/reference/repos#create-a-repository-webhook

// The actual logic for webhook creation lives in the WebhookManager interface
// TODO: the remaining logic should be refactored into a repository
// registration interface
func (s *Server) registerWebhookForRepository(
ctx context.Context,
pbuild *providers.ProviderBuilder,
projectID uuid.UUID,
repo *pb.UpstreamRepositoryRef,
) (*pb.RegisterRepoResult, error) {
logger := zerolog.Ctx(ctx).With().
Str("repoName", repo.Name).
Str("repoOwner", repo.Owner).
Logger()
ctx = logger.WithContext(ctx)

if !pbuild.Implements(db.ProviderTypeGithub) {
return nil, fmt.Errorf("provider %s is not supported for github webhook", pbuild.GetName())
}

client, err := pbuild.GetGitHub()
if err != nil {
return nil, fmt.Errorf("error creating github provider: %w", err)
}

regResult := &pb.RegisterRepoResult{
// We will overwrite this later when we've looked it up from the provider,
// but existing clients expect a message here, so let's add one.
Repository: &pb.Repository{
Name: repo.Name, // Not normalized, from client
Owner: repo.Owner, // Not normalized, from client
},
Status: &pb.RegisterRepoResult_Status{
Success: false,
},
}

// let's verify that the repository actually exists.
repoGet, err := client.GetRepository(ctx, repo.Owner, repo.Name)
if err != nil {
errorStr := err.Error()
regResult.Status.Error = &errorStr
return regResult, nil
}

// skip if we try to register a private repository
if repoGet.GetPrivate() && !features.ProjectAllowsPrivateRepos(ctx, s.store, projectID) {
errorStr := "repository is private"
regResult.Status.Error = &errorStr
return regResult, nil
}

hookUUID, githubHook, err := s.webhookManager.CreateWebhook(ctx, client, repo.Owner, repo.Name)
if err != nil {
logger.Error().Msgf("error while creating webhook: %v", err)
errorStr := err.Error()
regResult.Status.Error = &errorStr
return regResult, nil
}

regResult.Status.Success = true

regResult.Repository = &pb.Repository{
Name: repoGet.GetName(),
Owner: repoGet.GetOwner().GetLogin(),
RepoId: repoGet.GetID(),
HookId: githubHook.GetID(),
HookUrl: githubHook.GetURL(),
DeployUrl: repoGet.GetDeploymentsURL(),
CloneUrl: repoGet.GetCloneURL(),
HookType: githubHook.GetType(),
HookName: githubHook.GetName(),
HookUuid: hookUUID,
IsPrivate: repoGet.GetPrivate(),
IsFork: repoGet.GetFork(),
DefaultBranch: repoGet.GetDefaultBranch(),
License: repoGet.GetLicense().GetSPDXID(),
}

return regResult, nil
}

func (s *Server) parseGithubEventForProcessing(
rawWHPayload []byte,
msg *message.Message,
Expand Down
111 changes: 23 additions & 88 deletions internal/controlplane/handlers_repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ import (
"github.com/stacklok/minder/internal/projects/features"
"github.com/stacklok/minder/internal/providers"
github "github.com/stacklok/minder/internal/providers/github"
"github.com/stacklok/minder/internal/reconcilers"
"github.com/stacklok/minder/internal/util"
cursorutil "github.com/stacklok/minder/internal/util/cursor"
"github.com/stacklok/minder/internal/util/ptr"
pb "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1"
v1 "github.com/stacklok/minder/pkg/providers/v1"
)
Expand All @@ -46,106 +46,41 @@ const maxFetchLimit = 100
// Once a user had enrolled in a project (they have a valid token), they can register
// repositories to be monitored by the minder by provisioning a webhook on the
// repository(ies).
func (s *Server) RegisterRepository(ctx context.Context,
in *pb.RegisterRepositoryRequest) (*pb.RegisterRepositoryResponse, error) {
entityCtx := engine.EntityFromContext(ctx)
projectID := entityCtx.Project.ID

provider, err := getProviderFromRequestOrDefault(ctx, s.store, in, projectID)
if err != nil {
return nil, providerError(err)
}

pbOpts := []providers.ProviderBuilderOption{
providers.WithProviderMetrics(s.provMt),
providers.WithRestClientCache(s.restClientCache),
}
p, err := providers.GetProviderBuilder(ctx, provider, s.store, s.cryptoEngine, pbOpts...)
func (s *Server) RegisterRepository(
ctx context.Context,
in *pb.RegisterRepositoryRequest,
) (*pb.RegisterRepositoryResponse, error) {
projectID, client, err := s.getProjectIDAndClient(ctx, in)
if err != nil {
return nil, status.Errorf(codes.Internal, "cannot get provider builder: %v", err)
}

// Unmarshal the in.GetRepositories() into a struct Repository
if in.GetRepository() == nil || in.GetRepository().Name == "" {
return nil, util.UserVisibleError(codes.InvalidArgument, "no repository provided")
return nil, err
}

repo := in.GetRepository()

result, err := s.registerWebhookForRepository(ctx, p, projectID, repo)
if err != nil {
return nil, util.UserVisibleError(codes.Internal, "cannot register webhook: %v", err)
// Validate that the Repository struct in the request
repoReference := in.GetRepository()
if repoReference == nil || repoReference.Name == "" || repoReference.Owner == "" {
return nil, util.UserVisibleError(codes.InvalidArgument, "missing repository owner and/or name")
}

r := result.Repository

response := &pb.RegisterRepositoryResponse{
Result: result,
}

// Convert each result to a pb.Repository object
if result.Status.Error != nil {
return response, nil
}

// update the database
dbRepo, err := s.store.CreateRepository(ctx, db.CreateRepositoryParams{
Provider: provider.Name,
ProviderID: provider.ID,
ProjectID: projectID,
RepoOwner: r.Owner,
RepoName: r.Name,
RepoID: r.RepoId,
IsPrivate: r.IsPrivate,
IsFork: r.IsFork,
WebhookID: sql.NullInt64{
Int64: r.HookId,
Valid: true,
},
CloneUrl: r.CloneUrl,
WebhookUrl: r.HookUrl,
DeployUrl: r.DeployUrl,
DefaultBranch: sql.NullString{
String: r.DefaultBranch,
Valid: true,
},
License: sql.NullString{
String: r.License,
Valid: true,
Result: &pb.RegisterRepoResult{
Status: &pb.RegisterRepoResult_Status{
Success: false,
},
},
})
// even if we set the webhook, if we couldn't create it in the database, we'll return an error
if err != nil {
log.Printf("error creating repository '%s/%s' in database: %v", r.Owner, r.Name, err)

result.Status.Success = false
errorStr := "error creating repository in database"
result.Status.Error = &errorStr
return response, nil
}

repoDBID := dbRepo.ID.String()
r.Id = &repoDBID

// publish a reconciling event for the registered repositories
log.Printf("publishing register event for repository: %s/%s", r.Owner, r.Name)

msg, err := reconcilers.NewRepoReconcilerMessage(provider.Name, r.RepoId, projectID)
// To be backwards compatible with the existing implementation, we return
// an 200-type response with any errors inside the response body once we
// validate the response body
newRepo, err := s.repos.CreateRepository(ctx, client, projectID, repoReference)
if err != nil {
log.Printf("error creating reconciler event: %v", err)
log.Printf("error while registering repository: %v", err)
response.Result.Status.Error = ptr.Ptr(err.Error())
return response, nil
}

// This is a non-fatal error, so we'll just log it and continue with the next ones
if err := s.evt.Publish(reconcilers.InternalReconcilerEventTopic, msg); err != nil {
log.Printf("error publishing reconciler event: %v", err)
}

// Telemetry logging
logger.BusinessRecord(ctx).Provider = provider.Name
logger.BusinessRecord(ctx).Project = projectID
logger.BusinessRecord(ctx).Repository = dbRepo.ID

response.Result.Status.Success = true
response.Result.Repository = newRepo
return response, nil
}

Expand Down
Loading

0 comments on commit d68e8e6

Please sign in to comment.