• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mindersec / minder / 14376078638

10 Apr 2025 08:39AM UTC coverage: 56.857%. First build
14376078638

Pull #5515

github

web-flow
Merge ea220ae51 into 0f5ecd80e
Pull Request #5515: Don't return errors when getting properties

27 of 49 new or added lines in 22 files covered. (55.1%)

18305 of 32195 relevant lines covered (56.86%)

37.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.2
/internal/controlplane/handlers_repositories.go
1
// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors
2
// SPDX-License-Identifier: Apache-2.0
3

4
package controlplane
5

6
import (
7
        "context"
8
        "database/sql"
9
        "errors"
10
        "fmt"
11
        "slices"
12
        "strings"
13

14
        "github.com/google/uuid"
15
        "github.com/rs/zerolog"
16
        "google.golang.org/grpc/codes"
17
        "google.golang.org/grpc/status"
18
        "google.golang.org/protobuf/types/known/structpb"
19

20
        "github.com/mindersec/minder/internal/db"
21
        "github.com/mindersec/minder/internal/engine/engcontext"
22
        "github.com/mindersec/minder/internal/logger"
23
        "github.com/mindersec/minder/internal/projects/features"
24
        "github.com/mindersec/minder/internal/providers"
25
        "github.com/mindersec/minder/internal/providers/github"
26
        "github.com/mindersec/minder/internal/repositories"
27
        "github.com/mindersec/minder/internal/util"
28
        cursorutil "github.com/mindersec/minder/internal/util/cursor"
29
        "github.com/mindersec/minder/internal/util/ptr"
30
        pb "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
31
        "github.com/mindersec/minder/pkg/entities/properties"
32
        v1 "github.com/mindersec/minder/pkg/providers/v1"
33
)
34

35
// maxFetchLimit is the maximum number of repositories that can be fetched from the database in one call
36
const maxFetchLimit = 100
37

38
// RegisterRepository adds repositories to the database and registers a webhook
39
// Once a user had enrolled in a project (they have a valid token), they can register
40
// repositories to be monitored by the minder by provisioning a webhook on the
41
// repository(ies).
42
func (s *Server) RegisterRepository(
43
        ctx context.Context,
44
        in *pb.RegisterRepositoryRequest,
45
) (*pb.RegisterRepositoryResponse, error) {
7✔
46
        projectID := GetProjectID(ctx)
7✔
47
        providerName := GetProviderName(ctx)
7✔
48

7✔
49
        var fetchByProps *properties.Properties
7✔
50
        var provider *db.Provider
7✔
51
        var err error
7✔
52
        if in.GetEntity() != nil {
7✔
53
                fetchByProps, provider, err = s.repoCreateInfoFromUpstreamEntityRef(
×
54
                        ctx, projectID, providerName, in.GetEntity())
×
55
        } else if in.GetRepository() != nil {
14✔
56
                fetchByProps, provider, err = s.repoCreateInfoFromUpstreamRepositoryRef(
7✔
57
                        ctx, projectID, providerName, in.GetRepository())
7✔
58
        } else {
7✔
59
                return nil, util.UserVisibleError(codes.InvalidArgument, "missing entity or repository field")
×
60
        }
×
61

62
        if err != nil {
9✔
63
                return nil, err
2✔
64
        }
2✔
65

66
        l := zerolog.Ctx(ctx).With().
5✔
67
                Dict("properties", fetchByProps.ToLogDict()).
5✔
68
                Str("projectID", projectID.String()).
5✔
69
                Logger()
5✔
70
        ctx = l.WithContext(ctx)
5✔
71

5✔
72
        newRepo, err := s.repos.CreateRepository(ctx, provider, projectID, fetchByProps)
5✔
73
        if err != nil {
9✔
74
                if errors.Is(err, repositories.ErrPrivateRepoForbidden) || errors.Is(err, repositories.ErrArchivedRepoForbidden) {
6✔
75
                        return nil, util.UserVisibleError(codes.InvalidArgument, "%s", err.Error())
2✔
76
                }
2✔
77
                return nil, util.UserVisibleError(codes.Internal, "unable to register repository: %v", err)
2✔
78
        }
79

80
        return &pb.RegisterRepositoryResponse{
1✔
81
                Result: &pb.RegisterRepoResult{
1✔
82
                        Status: &pb.RegisterRepoResult_Status{
1✔
83
                                Success: true,
1✔
84
                        },
1✔
85
                        Repository: newRepo,
1✔
86
                },
1✔
87
        }, nil
1✔
88
}
89

90
func (s *Server) repoCreateInfoFromUpstreamRepositoryRef(
91
        ctx context.Context,
92
        projectID uuid.UUID,
93
        providerName string,
94
        rep *pb.UpstreamRepositoryRef,
95
) (*properties.Properties, *db.Provider, error) {
7✔
96
        // If the repo owner is missing, GitHub will assume a default value based
7✔
97
        // on the user's credentials. An explicit check for owner is left out to
7✔
98
        // avoid breaking backwards compatibility.
7✔
99
        if rep.GetName() == "" {
8✔
100
                return nil, nil, util.UserVisibleError(codes.InvalidArgument, "missing repository name")
1✔
101
        }
1✔
102

103
        fetchByProps := properties.NewProperties(map[string]any{
6✔
104
                properties.PropertyUpstreamID: fmt.Sprintf("%d", rep.GetRepoId()),
6✔
105
                properties.PropertyName:       fmt.Sprintf("%s/%s", rep.GetOwner(), rep.GetName()),
6✔
106
        })
6✔
107

6✔
108
        provider, err := s.inferProviderByOwner(ctx, rep.GetOwner(), projectID, providerName)
6✔
109
        if err != nil {
7✔
110
                pErr := providers.ErrProviderNotFoundBy{}
1✔
111
                if errors.As(err, &pErr) {
1✔
112
                        return nil, nil, util.UserVisibleError(codes.NotFound, "no suitable provider found, please enroll a provider")
×
113
                }
×
114
                return nil, nil, status.Errorf(codes.Internal, "cannot get provider: %v", err)
1✔
115
        }
116

117
        return fetchByProps, provider, nil
5✔
118
}
119

120
func (s *Server) repoCreateInfoFromUpstreamEntityRef(
121
        ctx context.Context,
122
        projectID uuid.UUID,
123
        providerName string,
124
        entity *pb.UpstreamEntityRef,
125
) (*properties.Properties, *db.Provider, error) {
×
126
        inPropsMap := entity.GetProperties().AsMap()
×
NEW
127
        fetchByProps := properties.NewProperties(inPropsMap)
×
128

×
129
        provider, err := s.providerStore.GetByName(ctx, projectID, providerName)
×
130
        if err != nil {
×
131
                if errors.Is(err, sql.ErrNoRows) {
×
132
                        return nil, nil, util.UserVisibleError(codes.NotFound, "provider not found")
×
133
                }
×
134
                return nil, nil, status.Errorf(codes.Internal, "cannot get provider: %v", err)
×
135
        }
136

137
        return fetchByProps, provider, nil
×
138
}
139

140
// ListRepositories returns a list of repositories for a given project
141
// This function will typically be called by the client to get a list of
142
// repositories that are registered present in the minder database
143
func (s *Server) ListRepositories(ctx context.Context,
144
        in *pb.ListRepositoriesRequest) (*pb.ListRepositoriesResponse, error) {
×
145
        entityCtx := engcontext.EntityFromContext(ctx)
×
146
        projectID := entityCtx.Project.ID
×
147
        providerName := entityCtx.Provider.Name
×
148

×
149
        logger.BusinessRecord(ctx).Provider = providerName
×
150
        logger.BusinessRecord(ctx).Project = projectID
×
151

×
152
        providerFilter := getNameFilterParam(providerName)
×
153

×
154
        reqRepoCursor, err := cursorutil.NewRepoCursor(in.GetCursor())
×
155
        if err != nil {
×
156
                return nil, util.UserVisibleError(codes.InvalidArgument, "%s", err.Error())
×
157
        }
×
158

159
        repoId := sql.NullInt64{}
×
160
        if reqRepoCursor.ProjectId == projectID.String() && reqRepoCursor.Provider == providerName {
×
161
                repoId = sql.NullInt64{Valid: true, Int64: reqRepoCursor.RepoId}
×
162
        }
×
163

164
        limit := sql.NullInt64{Valid: false, Int64: 0}
×
165
        reqLimit := in.GetLimit()
×
166
        if reqLimit > 0 {
×
167
                if reqLimit > maxFetchLimit {
×
168
                        return nil, util.UserVisibleError(codes.InvalidArgument, "limit too high, max is %d", maxFetchLimit)
×
169
                }
×
170
                limit = sql.NullInt64{Valid: true, Int64: reqLimit + 1}
×
171
        }
172

173
        repos, err := s.store.ListRepositoriesByProjectID(ctx, db.ListRepositoriesByProjectIDParams{
×
174
                Provider:  providerFilter,
×
175
                ProjectID: projectID,
×
176
                RepoID:    repoId,
×
177
                Limit:     limit,
×
178
        })
×
179

×
180
        if err != nil {
×
181
                return nil, err
×
182
        }
×
183

184
        var resp pb.ListRepositoriesResponse
×
185
        var results []*pb.Repository
×
186

×
187
        for _, repo := range repos {
×
188
                projID := repo.ProjectID.String()
×
189
                r := repositories.PBRepositoryFromDB(repo)
×
190
                r.Context = &pb.Context{
×
191
                        Project:  &projID,
×
192
                        Provider: &repo.Provider,
×
193
                }
×
194
                results = append(results, r)
×
195
        }
×
196

197
        var respRepoCursor *cursorutil.RepoCursor
×
198
        if limit.Valid && int64(len(repos)) == limit.Int64 {
×
199
                lastRepo := repos[len(repos)-1]
×
200
                respRepoCursor = &cursorutil.RepoCursor{
×
201
                        ProjectId: projectID.String(),
×
202
                        Provider:  providerName,
×
203
                        RepoId:    lastRepo.RepoID,
×
204
                }
×
205

×
206
                // remove the (limit + 1)th element from the results
×
207
                results = results[:len(results)-1]
×
208
        }
×
209

210
        resp.Results = results
×
211
        resp.Cursor = respRepoCursor.String()
×
212

×
213
        return &resp, nil
×
214
}
215

216
// GetRepositoryById returns a repository for a given repository id
217
func (s *Server) GetRepositoryById(ctx context.Context,
218
        in *pb.GetRepositoryByIdRequest) (*pb.GetRepositoryByIdResponse, error) {
×
219
        parsedRepositoryID, err := uuid.Parse(in.RepositoryId)
×
220
        if err != nil {
×
221
                return nil, util.UserVisibleError(codes.InvalidArgument, "invalid repository ID")
×
222
        }
×
223
        projectID := GetProjectID(ctx)
×
224

×
225
        // read the repository
×
226
        repo, err := s.store.GetRepositoryByIDAndProject(ctx, db.GetRepositoryByIDAndProjectParams{
×
227
                ID:        parsedRepositoryID,
×
228
                ProjectID: projectID,
×
229
        })
×
230
        if errors.Is(err, sql.ErrNoRows) {
×
231
                return nil, status.Errorf(codes.NotFound, "repository not found")
×
232
        } else if err != nil {
×
233
                return nil, status.Errorf(codes.Internal, "cannot read repository: %v", err)
×
234
        }
×
235

236
        projID := repo.ProjectID.String()
×
237
        r := repositories.PBRepositoryFromDB(repo)
×
238
        r.Context = &pb.Context{
×
239
                Project:  &projID,
×
240
                Provider: &repo.Provider,
×
241
        }
×
242

×
243
        // Telemetry logging
×
244
        logger.BusinessRecord(ctx).ProviderID = repo.ProviderID
×
245
        logger.BusinessRecord(ctx).Project = repo.ProjectID
×
246
        logger.BusinessRecord(ctx).Repository = repo.ID
×
247

×
248
        return &pb.GetRepositoryByIdResponse{Repository: r}, nil
×
249
}
250

251
// GetRepositoryByName returns information about a repository.
252
// This function will typically be called by the client to get a
253
// repository which is already registered and present in the minder database
254
// The API is called with a project id
255
func (s *Server) GetRepositoryByName(ctx context.Context,
256
        in *pb.GetRepositoryByNameRequest) (*pb.GetRepositoryByNameResponse, error) {
×
257
        // split repo name in owner and name
×
258
        fragments := strings.Split(in.Name, "/")
×
259
        if len(fragments) != 2 {
×
260
                return nil, util.UserVisibleError(codes.InvalidArgument, "invalid repository name, needs to have the format: owner/name")
×
261
        }
×
262

263
        entityCtx := engcontext.EntityFromContext(ctx)
×
264
        projectID := entityCtx.Project.ID
×
265

×
266
        // TODO: move this lookup logic out of the controlplane
×
267
        providerFilter := getNameFilterParam(entityCtx.Provider.Name)
×
268
        repo, err := s.store.GetRepositoryByRepoName(ctx, db.GetRepositoryByRepoNameParams{
×
269
                Provider:  providerFilter,
×
270
                RepoOwner: fragments[0],
×
271
                RepoName:  fragments[1],
×
272
                ProjectID: projectID,
×
273
        })
×
274

×
275
        if errors.Is(err, sql.ErrNoRows) {
×
276
                return nil, status.Errorf(codes.NotFound, "repository not found")
×
277
        } else if err != nil {
×
278
                return nil, err
×
279
        }
×
280

281
        projID := repo.ProjectID.String()
×
282
        r := repositories.PBRepositoryFromDB(repo)
×
283
        r.Context = &pb.Context{
×
284
                Project:  &projID,
×
285
                Provider: &repo.Provider,
×
286
        }
×
287

×
288
        // Telemetry logging
×
289
        logger.BusinessRecord(ctx).ProviderID = repo.ProviderID
×
290
        logger.BusinessRecord(ctx).Project = repo.ProjectID
×
291
        logger.BusinessRecord(ctx).Repository = repo.ID
×
292

×
293
        return &pb.GetRepositoryByNameResponse{Repository: r}, nil
×
294
}
295

296
// DeleteRepositoryById deletes a repository by its UUID
297
func (s *Server) DeleteRepositoryById(
298
        ctx context.Context,
299
        in *pb.DeleteRepositoryByIdRequest,
300
) (*pb.DeleteRepositoryByIdResponse, error) {
4✔
301
        parsedRepositoryID, err := uuid.Parse(in.RepositoryId)
4✔
302
        if err != nil {
5✔
303
                return nil, util.UserVisibleError(codes.InvalidArgument, "invalid repository ID")
1✔
304
        }
1✔
305

306
        projectID := GetProjectID(ctx)
3✔
307

3✔
308
        err = s.repos.DeleteByID(ctx, parsedRepositoryID, projectID)
3✔
309
        if errors.Is(err, sql.ErrNoRows) {
4✔
310
                return nil, status.Errorf(codes.NotFound, "repository not found")
1✔
311
        } else if err != nil {
4✔
312
                return nil, status.Errorf(codes.Internal, "unexpected error deleting repo: %v", err)
1✔
313
        }
1✔
314

315
        // return the response with the id of the deleted repository
316
        return &pb.DeleteRepositoryByIdResponse{
1✔
317
                RepositoryId: in.RepositoryId,
1✔
318
        }, nil
1✔
319
}
320

321
// DeleteRepositoryByName deletes a repository by name
322
func (s *Server) DeleteRepositoryByName(
323
        ctx context.Context,
324
        in *pb.DeleteRepositoryByNameRequest,
325
) (*pb.DeleteRepositoryByNameResponse, error) {
4✔
326
        // split repo name in owner and name
4✔
327
        fragments := strings.Split(in.Name, "/")
4✔
328
        if len(fragments) != 2 {
5✔
329
                return nil, util.UserVisibleError(codes.InvalidArgument, "invalid repository name, needs to have the format: owner/name")
1✔
330
        }
1✔
331

332
        projectID := GetProjectID(ctx)
3✔
333
        providerName := GetProviderName(ctx)
3✔
334

3✔
335
        err := s.repos.DeleteByName(ctx, fragments[0], fragments[1], projectID, providerName)
3✔
336
        if errors.Is(err, sql.ErrNoRows) {
4✔
337
                return nil, status.Errorf(codes.NotFound, "repository not found")
1✔
338
        } else if err != nil {
4✔
339
                return nil, status.Errorf(codes.Internal, "unexpected error deleting repo: %v", err)
1✔
340
        }
1✔
341

342
        // return the response with the name of the deleted repository
343
        return &pb.DeleteRepositoryByNameResponse{
1✔
344
                Name: in.Name,
1✔
345
        }, nil
1✔
346
}
347

348
// ListRemoteRepositoriesFromProvider returns a list of repositories from a provider
349
func (s *Server) ListRemoteRepositoriesFromProvider(
350
        ctx context.Context,
351
        in *pb.ListRemoteRepositoriesFromProviderRequest,
352
) (*pb.ListRemoteRepositoriesFromProviderResponse, error) {
3✔
353
        entityCtx := engcontext.EntityFromContext(ctx)
3✔
354
        projectID := entityCtx.Project.ID
3✔
355

3✔
356
        // Telemetry logging
3✔
357
        logger.BusinessRecord(ctx).Project = projectID
3✔
358

3✔
359
        providerName := in.GetContext().GetProvider()
3✔
360
        provs, errorProvs, err := s.providerManager.BulkInstantiateByTrait(
3✔
361
                ctx, projectID, db.ProviderTypeRepoLister, providerName)
3✔
362
        if err != nil {
3✔
363
                pErr := providers.ErrProviderNotFoundBy{}
×
364
                if errors.As(err, &pErr) {
×
365
                        return nil, util.UserVisibleError(codes.NotFound, "no suitable provider found, please enroll a provider")
×
366
                }
×
367
                return nil, providerError(err)
×
368
        }
369

370
        out := &pb.ListRemoteRepositoriesFromProviderResponse{
3✔
371
                Results:  []*pb.UpstreamRepositoryRef{},
3✔
372
                Entities: []*pb.RegistrableUpstreamEntityRef{},
3✔
373
        }
3✔
374

3✔
375
        for providerID, providerT := range provs {
6✔
376
                results, err := s.fetchRepositoriesForProvider(
3✔
377
                        ctx, projectID, providerID, providerT.Name, providerT.Provider)
3✔
378
                if err != nil {
5✔
379
                        zerolog.Ctx(ctx).Error().Err(err).
2✔
380
                                Msgf("error listing repositories for provider %s in project %s", providerT.Name, projectID)
2✔
381
                        errorProvs = append(errorProvs, providerT.Name)
2✔
382
                        continue
2✔
383
                }
384
                for _, result := range results {
3✔
385
                        out.Results = append(out.Results, result.Repo)
2✔
386
                        out.Entities = append(out.Entities, result.Entity)
2✔
387
                }
2✔
388
        }
389

390
        // If all providers failed, return an error
391
        if len(errorProvs) > 0 && len(out.Results) == 0 {
5✔
392
                return nil, util.UserVisibleError(codes.Internal, "cannot list repositories for providers: %v", errorProvs)
2✔
393
        }
2✔
394

395
        return out, nil
1✔
396
}
397

398
// fetchRepositoriesForProvider fetches repositories for a given provider
399
//
400
// Returns a list of repositories that with an up-to-date status of whether they are registered
401
func (s *Server) fetchRepositoriesForProvider(
402
        ctx context.Context,
403
        projectID uuid.UUID,
404
        providerID uuid.UUID,
405
        providerName string,
406
        provider v1.Provider,
407
) ([]*UpstreamRepoAndEntityRef, error) {
5✔
408
        zerolog.Ctx(ctx).Trace().
5✔
409
                Str("provider_id", providerID.String()).
5✔
410
                Str("project_id", projectID.String()).
5✔
411
                Msg("listing repositories")
5✔
412

5✔
413
        repoLister, err := v1.As[v1.RepoLister](provider)
5✔
414
        if err != nil {
5✔
415
                zerolog.Ctx(ctx).Error().Err(err).Msg("error instantiating repo lister")
×
416
                return nil, err
×
417
        }
×
418

419
        results, err := s.listRemoteRepositoriesForProvider(ctx, providerName, repoLister, projectID)
5✔
420
        if err != nil {
7✔
421
                zerolog.Ctx(ctx).Error().Err(err).Msg("cannot list repositories for provider")
2✔
422
                return nil, err
2✔
423
        }
2✔
424

425
        registeredRepos, err := s.repos.ListRepositories(
3✔
426
                ctx,
3✔
427
                projectID,
3✔
428
                providerID,
3✔
429
        )
3✔
430
        if err != nil {
4✔
431
                zerolog.Ctx(ctx).Error().
1✔
432
                        Str("project_id", projectID.String()).
1✔
433
                        Str("provider_id", providerID.String()).
1✔
434
                        Err(err).Msg("cannot list registered repositories")
1✔
435
                return nil, util.UserVisibleError(
1✔
436
                        codes.Internal,
1✔
437
                        "cannot list registered repositories",
1✔
438
                )
1✔
439
        }
1✔
440

441
        registered := make(map[string]bool)
2✔
442
        for _, repo := range registeredRepos {
4✔
443
                uidP := repo.Properties.GetProperty(properties.PropertyUpstreamID)
2✔
444
                if uidP == nil {
2✔
445
                        zerolog.Ctx(ctx).Warn().
×
446
                                Str("entity_id", repo.Entity.ID.String()).
×
447
                                Str("entity_name", repo.Entity.Name).
×
448
                                Str("provider_id", providerID.String()).
×
449
                                Str("project_id", projectID.String()).
×
450
                                Msg("repository has no upstream ID")
×
451
                        continue
×
452
                }
453
                registered[uidP.GetString()] = true
2✔
454
        }
455

456
        for _, result := range results {
6✔
457
                uprops := result.Entity.GetEntity().GetProperties()
4✔
458
                upropsMap := uprops.AsMap()
4✔
459
                if upropsMap == nil {
4✔
460
                        zerolog.Ctx(ctx).Warn().
×
461
                                Str("provider_id", providerID.String()).
×
462
                                Str("project_id", projectID.String()).
×
463
                                Msg("upstream repository entry has no properties")
×
464
                        continue
×
465
                }
466
                uidAny, ok := upropsMap[properties.PropertyUpstreamID]
4✔
467
                if !ok {
4✔
468
                        zerolog.Ctx(ctx).Warn().
×
469
                                Str("provider_id", providerID.String()).
×
470
                                Str("project_id", projectID.String()).
×
471
                                Msg("upstream repository entry has no upstream ID")
×
472
                        continue
×
473
                }
474

475
                uid, ok := uidAny.(string)
4✔
476
                if !ok {
4✔
477
                        zerolog.Ctx(ctx).Warn().
×
478
                                Str("provider_id", providerID.String()).
×
479
                                Str("project_id", projectID.String()).
×
480
                                Msg("upstream repository entry has invalid upstream ID")
×
481
                        continue
×
482
                }
483

484
                result.Repo.Registered = registered[uid]
4✔
485
                result.Entity.Registered = registered[uid]
4✔
486
        }
487

488
        return results, nil
2✔
489
}
490

491
func (s *Server) listRemoteRepositoriesForProvider(
492
        ctx context.Context,
493
        provName string,
494
        repoLister v1.RepoLister,
495
        projectID uuid.UUID,
496
) ([]*UpstreamRepoAndEntityRef, error) {
5✔
497
        tmoutCtx, cancel := context.WithTimeout(ctx, github.ExpensiveRestCallTimeout)
5✔
498
        defer cancel()
5✔
499

5✔
500
        remoteRepos, err := repoLister.ListAllRepositories(tmoutCtx)
5✔
501
        if err != nil {
7✔
502
                return nil, fmt.Errorf("cannot list repositories: %v", err)
2✔
503
        }
2✔
504

505
        allowsPrivateRepos := features.ProjectAllowsPrivateRepos(ctx, s.store, projectID)
3✔
506
        if !allowsPrivateRepos {
3✔
507
                zerolog.Ctx(ctx).Info().Msg("filtering out private repositories")
×
508
        } else {
3✔
509
                zerolog.Ctx(ctx).Info().Msg("including private repositories")
3✔
510
        }
3✔
511

512
        results := make([]*UpstreamRepoAndEntityRef, 0, len(remoteRepos))
3✔
513

3✔
514
        for idx, rem := range remoteRepos {
9✔
515
                // Skip private repositories
6✔
516
                if rem.IsPrivate && !allowsPrivateRepos {
6✔
517
                        continue
×
518
                }
519
                remoteRepo := remoteRepos[idx]
6✔
520

6✔
521
                var props *structpb.Struct
6✔
522
                if remoteRepo.Properties != nil {
12✔
523
                        props = remoteRepo.Properties
6✔
524
                }
6✔
525

526
                repo := &UpstreamRepoAndEntityRef{
6✔
527
                        Repo: &pb.UpstreamRepositoryRef{
6✔
528
                                Context: &pb.Context{
6✔
529
                                        Provider: &provName,
6✔
530
                                        Project:  ptr.Ptr(projectID.String()),
6✔
531
                                },
6✔
532
                                Owner:  remoteRepo.Owner,
6✔
533
                                Name:   remoteRepo.Name,
6✔
534
                                RepoId: remoteRepo.RepoId,
6✔
535
                        },
6✔
536
                        Entity: &pb.RegistrableUpstreamEntityRef{
6✔
537
                                Entity: &pb.UpstreamEntityRef{
6✔
538
                                        Context: &pb.ContextV2{
6✔
539
                                                Provider:  provName,
6✔
540
                                                ProjectId: projectID.String(),
6✔
541
                                        },
6✔
542
                                        Type:       pb.Entity_ENTITY_REPOSITORIES,
6✔
543
                                        Properties: props,
6✔
544
                                },
6✔
545
                        },
6✔
546
                }
6✔
547
                results = append(results, repo)
6✔
548
        }
549

550
        return results, nil
3✔
551
}
552

553
// TODO: move out of controlplane
554
// inferProviderByOwner returns the provider to use for a given repo owner
555
func (s *Server) inferProviderByOwner(ctx context.Context, owner string, projectID uuid.UUID, providerName string,
556
) (*db.Provider, error) {
6✔
557
        if providerName != "" {
12✔
558
                return s.providerStore.GetByName(ctx, projectID, providerName)
6✔
559
        }
6✔
560
        opts, err := s.providerStore.GetByTraitInHierarchy(ctx, projectID, providerName, db.ProviderTypeGithub)
×
561
        if err != nil {
×
562
                return nil, fmt.Errorf("error getting providers: %v", err)
×
563
        }
×
564

565
        slices.SortFunc(opts, func(a, b db.Provider) int {
×
566
                // Sort GitHub OAuth provider after all GitHub App providers
×
567
                if a.Class == db.ProviderClassGithub && b.Class == db.ProviderClassGithubApp {
×
568
                        return 1
×
569
                }
×
570
                if a.Class == db.ProviderClassGithubApp && b.Class == db.ProviderClassGithub {
×
571
                        return -1
×
572
                }
×
573
                return 0
×
574
        })
575

576
        for _, prov := range opts {
×
577
                if github.CanHandleOwner(ctx, prov, owner) {
×
578
                        return &prov, nil
×
579
                }
×
580
        }
581

582
        return nil, fmt.Errorf("no providers can handle repo owned by %s", owner)
×
583
}
584

585
// UpstreamRepoAndEntityRef is a pair of upstream repository and entity references
586
type UpstreamRepoAndEntityRef struct {
587
        Repo   *pb.UpstreamRepositoryRef
588
        Entity *pb.RegistrableUpstreamEntityRef
589
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc