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

Reconcile entity registration #3562

Merged
merged 20 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
19 changes: 19 additions & 0 deletions docs/docs/ref/proto.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

137 changes: 137 additions & 0 deletions internal/controlplane/handlers_entities.go
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)
Copy link
Contributor

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)

Copy link
Contributor

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?

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
}
132 changes: 132 additions & 0 deletions internal/controlplane/handlers_entities_test.go
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)
}
})
}
}
Loading