Skip to content
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

Revert "Move repository create to RepositoryService (#2632)" #2645

Merged
merged 1 commit into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions internal/controlplane/handlers_githubwebhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,92 @@ 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: 88 additions & 23 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,41 +46,106 @@ 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) {
projectID, client, err := s.getProjectIDAndClient(ctx, in)
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, err
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...)
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")
}

// 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")
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)
}

r := result.Repository

response := &pb.RegisterRepositoryResponse{
Result: &pb.RegisterRepoResult{
Status: &pb.RegisterRepoResult_Status{
Success: false,
},
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,
},
})
// 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
}

// 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)
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)
if err != nil {
log.Printf("error while registering repository: %v", err)
response.Result.Status.Error = ptr.Ptr(err.Error())
log.Printf("error creating reconciler event: %v", err)
return response, nil
}

response.Result.Status.Success = true
response.Result.Repository = newRepo
// 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

return response, nil
}

Expand Down
Loading
Loading