-
Notifications
You must be signed in to change notification settings - Fork 43
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
Reconcile entity registration #3562
Merged
Merged
Changes from 15 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
0d50969
wip: reconcile entity registration
teodor-yanev f9236fc
update: add tests and update path
teodor-yanev 66ad31b
Merge branch 'main' into reconcile-entity-registration
teodor-yanev a7a9548
add: license to test file
teodor-yanev 8180220
wip: reconcile entity registration
teodor-yanev 7887952
update: add tests and update path
teodor-yanev 17e1f96
add: license to test file
teodor-yanev afefa20
Merge branch 'reconcile-entity-registration' of https://github.com/st…
teodor-yanev 0bd81b7
Run make gen
rdimitrov 6047294
Merge branch 'main' into reconcile-entity-registration
teodor-yanev 58bd3b5
run make gen
teodor-yanev f17955f
update: clean-up and logging
teodor-yanev e9e4826
update: generate
teodor-yanev fac0404
Merge branch 'main' into reconcile-entity-registration
teodor-yanev 703a1e7
update: date
teodor-yanev db3e7cf
update: move reconcile rpc to provider service
teodor-yanev 6dd8a19
update: proto && fga relation
teodor-yanev 74bb67b
update: check for entity type; update tests
teodor-yanev 64a7505
generate
teodor-yanev 271269f
update: error code
teodor-yanev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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,137 @@ | ||
// Copyright 2024 Stacklok, Inc | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package controlplane | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
|
||
"github.com/ThreeDotsLabs/watermill/message" | ||
"github.com/google/uuid" | ||
"github.com/rs/zerolog" | ||
"google.golang.org/grpc/codes" | ||
|
||
"github.com/stacklok/minder/internal/db" | ||
"github.com/stacklok/minder/internal/engine" | ||
"github.com/stacklok/minder/internal/events" | ||
"github.com/stacklok/minder/internal/logger" | ||
"github.com/stacklok/minder/internal/providers" | ||
"github.com/stacklok/minder/internal/reconcilers/messages" | ||
"github.com/stacklok/minder/internal/util" | ||
pb "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" | ||
) | ||
|
||
// ReconcileEntityRegistration reconciles the registration of an entity. | ||
// | ||
// Currently, this method only supports repositories but is intended to be | ||
// generic and handle all types of entities. | ||
// Todo: Utilise for other entities when such are supported. | ||
func (s *Server) ReconcileEntityRegistration( | ||
ctx context.Context, | ||
in *pb.ReconcileEntityRegistrationRequest, | ||
) (*pb.ReconcileEntityRegistrationResponse, error) { | ||
l := zerolog.Ctx(ctx).With().Logger() | ||
|
||
entityCtx := engine.EntityFromContext(ctx) | ||
projectID := entityCtx.Project.ID | ||
|
||
logger.BusinessRecord(ctx).Project = projectID | ||
|
||
providerName := in.GetContext().GetProvider() | ||
provs, errorProvs, err := s.providerManager.BulkInstantiateByTrait(ctx, projectID, db.ProviderTypeRepoLister, providerName) | ||
if err != nil { | ||
pErr := providers.ErrProviderNotFoundBy{} | ||
if errors.As(err, &pErr) { | ||
return nil, util.UserVisibleError(codes.NotFound, "no suitable provider found, please enroll a provider") | ||
} | ||
return nil, providerError(err) | ||
} | ||
|
||
for providerName, provider := range provs { | ||
// Explicitly fetch the provider here as we need its ID for posting the event. | ||
pvr, err := s.providerStore.GetByName(ctx, projectID, providerName) | ||
if err != nil { | ||
errorProvs = append(errorProvs, providerName) | ||
continue | ||
} | ||
|
||
repos, err := s.fetchRepositoriesForProvider(ctx, projectID, providerName, provider) | ||
if err != nil { | ||
l.Error(). | ||
Str("providerName", providerName). | ||
Str("projectID", projectID.String()). | ||
Err(err). | ||
Msg("error fetching repositories for provider") | ||
errorProvs = append(errorProvs, providerName) | ||
continue | ||
} | ||
|
||
for _, repo := range repos { | ||
if repo.Registered { | ||
continue | ||
} | ||
|
||
msg, err := createEntityMessage(ctx, &l, projectID, pvr.ID, repo.GetName(), repo.GetOwner()) | ||
if err != nil { | ||
l.Error().Err(err). | ||
Int64("repoID", repo.RepoId). | ||
Str("providerName", providerName). | ||
Msg("error creating registration entity message") | ||
// This message will not be sent, but we can continue with the rest. | ||
continue | ||
} | ||
|
||
if err := s.publishEntityMessage(&l, msg); err != nil { | ||
l.Error().Err(err).Str("messageID", msg.UUID).Msg("error publishing register entities message") | ||
} | ||
} | ||
} | ||
|
||
// If all providers failed, return an error | ||
if len(errorProvs) > 0 && len(provs) == len(errorProvs) { | ||
return nil, util.UserVisibleError(codes.Internal, "cannot register entities for providers: %v", errorProvs) | ||
} | ||
|
||
return &pb.ReconcileEntityRegistrationResponse{}, nil | ||
} | ||
|
||
func (s *Server) publishEntityMessage(l *zerolog.Logger, msg *message.Message) error { | ||
l.Info().Str("messageID", msg.UUID).Msg("publishing register entities message for execution") | ||
return s.evt.Publish(events.TopicQueueReconcileEntityAdd, msg) | ||
} | ||
|
||
func createEntityMessage( | ||
ctx context.Context, | ||
l *zerolog.Logger, | ||
projectID, providerID uuid.UUID, | ||
repoName, repoOwner string, | ||
) (*message.Message, error) { | ||
msg := message.NewMessage(uuid.New().String(), nil) | ||
msg.SetContext(ctx) | ||
|
||
event := messages.NewRepoEvent(). | ||
WithProjectID(projectID). | ||
WithProviderID(providerID). | ||
WithRepoName(repoName). | ||
WithRepoOwner(repoOwner) | ||
|
||
err := event.ToMessage(msg) | ||
if err != nil { | ||
l.Error().Err(err).Msg("error marshalling register entities message") | ||
return nil, err | ||
} | ||
|
||
return msg, 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,132 @@ | ||
// Copyright 2024 Stacklok, Inc | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package controlplane | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
"go.uber.org/mock/gomock" | ||
|
||
"github.com/stacklok/minder/internal/db" | ||
"github.com/stacklok/minder/internal/engine" | ||
mockevents "github.com/stacklok/minder/internal/events/mock" | ||
mockgh "github.com/stacklok/minder/internal/providers/github/mock" | ||
mockmanager "github.com/stacklok/minder/internal/providers/manager/mock" | ||
rf "github.com/stacklok/minder/internal/repositories/github/mock/fixtures" | ||
pb "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" | ||
provinfv1 "github.com/stacklok/minder/pkg/providers/v1" | ||
) | ||
|
||
func TestServer_ReconcileEntityRegistration(t *testing.T) { | ||
t.Parallel() | ||
|
||
scenarios := []struct { | ||
Name string | ||
RepoServiceSetup repoMockBuilder | ||
GitHubSetup githubMockBuilder | ||
ProviderSetup func(ctrl *gomock.Controller) *mockmanager.MockProviderManager | ||
EventerSetup func(ctrl *gomock.Controller) *mockevents.MockInterface | ||
ProviderFails bool | ||
ExpectedResults []*pb.UpstreamRepositoryRef | ||
ExpectedError string | ||
}{ | ||
{ | ||
Name: "[positive] successful reconciliation", | ||
GitHubSetup: newGitHub(withSuccessfulListAllRepositories), | ||
RepoServiceSetup: rf.NewRepoService( | ||
rf.WithSuccessfulListRepositories( | ||
simpleDbRepository(repoName, remoteRepoId), | ||
), | ||
), | ||
ProviderSetup: func(ctrl *gomock.Controller) *mockmanager.MockProviderManager { | ||
providerManager := mockmanager.NewMockProviderManager(ctrl) | ||
providerManager.EXPECT().BulkInstantiateByTrait( | ||
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), | ||
).Return(map[string]provinfv1.Provider{provider.Name: mockgh.NewMockGitHub(ctrl)}, []string{}, nil).Times(1) | ||
return providerManager | ||
}, | ||
EventerSetup: func(ctrl *gomock.Controller) *mockevents.MockInterface { | ||
events := mockevents.NewMockInterface(ctrl) | ||
events.EXPECT().Publish(gomock.Any(), gomock.Any()).Times(1) | ||
return events | ||
}, | ||
}, | ||
{ | ||
Name: "[negative] failed to list repositories", | ||
GitHubSetup: newGitHub(withFailedListAllRepositories(errDefault)), | ||
RepoServiceSetup: rf.NewRepoService( | ||
rf.WithSuccessfulListRepositories( | ||
simpleDbRepository(repoName, remoteRepoId), | ||
), | ||
), | ||
ProviderSetup: func(ctrl *gomock.Controller) *mockmanager.MockProviderManager { | ||
providerManager := mockmanager.NewMockProviderManager(ctrl) | ||
providerManager.EXPECT().BulkInstantiateByTrait( | ||
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), | ||
).Return(map[string]provinfv1.Provider{provider.Name: mockgh.NewMockGitHub(ctrl)}, []string{}, nil).Times(1) | ||
return providerManager | ||
}, | ||
ExpectedError: "cannot register entities for providers: [github]", | ||
}, | ||
} | ||
|
||
for _, scenario := range scenarios { | ||
t.Run(scenario.Name, func(t *testing.T) { | ||
t.Parallel() | ||
ctrl := gomock.NewController(t) | ||
defer ctrl.Finish() | ||
|
||
ctx := engine.WithEntityContext(context.Background(), &engine.EntityContext{ | ||
Project: engine.Project{ID: projectID}, | ||
}) | ||
|
||
prov := scenario.GitHubSetup(ctrl) | ||
manager := mockmanager.NewMockProviderManager(ctrl) | ||
manager.EXPECT().BulkInstantiateByTrait( | ||
gomock.Any(), | ||
gomock.Eq(projectID), | ||
gomock.Eq(db.ProviderTypeRepoLister), | ||
gomock.Eq(""), | ||
).Return(map[string]provinfv1.Provider{provider.Name: prov}, []string{}, nil) | ||
|
||
server := createServer( | ||
ctrl, | ||
scenario.RepoServiceSetup, | ||
scenario.ProviderFails, | ||
manager, | ||
) | ||
|
||
if scenario.EventerSetup != nil { | ||
server.evt = scenario.EventerSetup(ctrl) | ||
} | ||
|
||
projectIDStr := projectID.String() | ||
req := &pb.ReconcileEntityRegistrationRequest{ | ||
Context: &pb.Context{ | ||
Project: &projectIDStr, | ||
}, | ||
} | ||
res, err := server.ReconcileEntityRegistration(ctx, req) | ||
if scenario.ExpectedError == "" { | ||
require.NoError(t, err) | ||
} else { | ||
require.Nil(t, res) | ||
require.Contains(t, err.Error(), scenario.ExpectedError) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
since the handler only errors out if all providers fail could we
zerolog
the error message? (I guess there would realistically be only one provider though)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.
Line 75 already ships the error with the log as a structured field, do you mean something more detailed than that?