Add a rudimentary filter option when listing entities

This change adds the ability to filter the list of entities returned
by the API by entity owner, name or endpoint, depending on the entity
type.

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
Gabriel Adrian Samfira 2025-06-17 22:37:18 +00:00
parent 2fd0e720e6
commit 499fbde60c
32 changed files with 879 additions and 73 deletions

View file

@ -1524,9 +1524,9 @@ func (_m *Store) ListAllScaleSets(ctx context.Context) ([]params.ScaleSet, error
return r0, r1
}
// ListEnterprises provides a mock function with given fields: ctx
func (_m *Store) ListEnterprises(ctx context.Context) ([]params.Enterprise, error) {
ret := _m.Called(ctx)
// ListEnterprises provides a mock function with given fields: ctx, filter
func (_m *Store) ListEnterprises(ctx context.Context, filter params.EnterpriseFilter) ([]params.Enterprise, error) {
ret := _m.Called(ctx, filter)
if len(ret) == 0 {
panic("no return value specified for ListEnterprises")
@ -1534,19 +1534,19 @@ func (_m *Store) ListEnterprises(ctx context.Context) ([]params.Enterprise, erro
var r0 []params.Enterprise
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) ([]params.Enterprise, error)); ok {
return rf(ctx)
if rf, ok := ret.Get(0).(func(context.Context, params.EnterpriseFilter) ([]params.Enterprise, error)); ok {
return rf(ctx, filter)
}
if rf, ok := ret.Get(0).(func(context.Context) []params.Enterprise); ok {
r0 = rf(ctx)
if rf, ok := ret.Get(0).(func(context.Context, params.EnterpriseFilter) []params.Enterprise); ok {
r0 = rf(ctx, filter)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]params.Enterprise)
}
}
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
if rf, ok := ret.Get(1).(func(context.Context, params.EnterpriseFilter) error); ok {
r1 = rf(ctx, filter)
} else {
r1 = ret.Error(1)
}
@ -1824,9 +1824,9 @@ func (_m *Store) ListJobsByStatus(ctx context.Context, status params.JobStatus)
return r0, r1
}
// ListOrganizations provides a mock function with given fields: ctx
func (_m *Store) ListOrganizations(ctx context.Context) ([]params.Organization, error) {
ret := _m.Called(ctx)
// ListOrganizations provides a mock function with given fields: ctx, filter
func (_m *Store) ListOrganizations(ctx context.Context, filter params.OrganizationFilter) ([]params.Organization, error) {
ret := _m.Called(ctx, filter)
if len(ret) == 0 {
panic("no return value specified for ListOrganizations")
@ -1834,19 +1834,19 @@ func (_m *Store) ListOrganizations(ctx context.Context) ([]params.Organization,
var r0 []params.Organization
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) ([]params.Organization, error)); ok {
return rf(ctx)
if rf, ok := ret.Get(0).(func(context.Context, params.OrganizationFilter) ([]params.Organization, error)); ok {
return rf(ctx, filter)
}
if rf, ok := ret.Get(0).(func(context.Context) []params.Organization); ok {
r0 = rf(ctx)
if rf, ok := ret.Get(0).(func(context.Context, params.OrganizationFilter) []params.Organization); ok {
r0 = rf(ctx, filter)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]params.Organization)
}
}
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
if rf, ok := ret.Get(1).(func(context.Context, params.OrganizationFilter) error); ok {
r1 = rf(ctx, filter)
} else {
r1 = ret.Error(1)
}
@ -1884,9 +1884,9 @@ func (_m *Store) ListPoolInstances(ctx context.Context, poolID string) ([]params
return r0, r1
}
// ListRepositories provides a mock function with given fields: ctx
func (_m *Store) ListRepositories(ctx context.Context) ([]params.Repository, error) {
ret := _m.Called(ctx)
// ListRepositories provides a mock function with given fields: ctx, filter
func (_m *Store) ListRepositories(ctx context.Context, filter params.RepositoryFilter) ([]params.Repository, error) {
ret := _m.Called(ctx, filter)
if len(ret) == 0 {
panic("no return value specified for ListRepositories")
@ -1894,19 +1894,19 @@ func (_m *Store) ListRepositories(ctx context.Context) ([]params.Repository, err
var r0 []params.Repository
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) ([]params.Repository, error)); ok {
return rf(ctx)
if rf, ok := ret.Get(0).(func(context.Context, params.RepositoryFilter) ([]params.Repository, error)); ok {
return rf(ctx, filter)
}
if rf, ok := ret.Get(0).(func(context.Context) []params.Repository); ok {
r0 = rf(ctx)
if rf, ok := ret.Get(0).(func(context.Context, params.RepositoryFilter) []params.Repository); ok {
r0 = rf(ctx, filter)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]params.Repository)
}
}
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
if rf, ok := ret.Get(1).(func(context.Context, params.RepositoryFilter) error); ok {
r1 = rf(ctx, filter)
} else {
r1 = ret.Error(1)
}

View file

@ -41,7 +41,7 @@ type RepoStore interface {
CreateRepository(ctx context.Context, owner, name string, credentials params.ForgeCredentials, webhookSecret string, poolBalancerType params.PoolBalancerType) (param params.Repository, err error)
GetRepository(ctx context.Context, owner, name, endpointName string) (params.Repository, error)
GetRepositoryByID(ctx context.Context, repoID string) (params.Repository, error)
ListRepositories(ctx context.Context) ([]params.Repository, error)
ListRepositories(ctx context.Context, filter params.RepositoryFilter) ([]params.Repository, error)
DeleteRepository(ctx context.Context, repoID string) error
UpdateRepository(ctx context.Context, repoID string, param params.UpdateEntityParams) (params.Repository, error)
}
@ -50,7 +50,7 @@ type OrgStore interface {
CreateOrganization(ctx context.Context, name string, credentials params.ForgeCredentials, webhookSecret string, poolBalancerType params.PoolBalancerType) (org params.Organization, err error)
GetOrganization(ctx context.Context, name, endpointName string) (params.Organization, error)
GetOrganizationByID(ctx context.Context, orgID string) (params.Organization, error)
ListOrganizations(ctx context.Context) ([]params.Organization, error)
ListOrganizations(ctx context.Context, filter params.OrganizationFilter) ([]params.Organization, error)
DeleteOrganization(ctx context.Context, orgID string) error
UpdateOrganization(ctx context.Context, orgID string, param params.UpdateEntityParams) (params.Organization, error)
}
@ -59,7 +59,7 @@ type EnterpriseStore interface {
CreateEnterprise(ctx context.Context, name string, credentialsName params.ForgeCredentials, webhookSecret string, poolBalancerType params.PoolBalancerType) (params.Enterprise, error)
GetEnterprise(ctx context.Context, name, endpointName string) (params.Enterprise, error)
GetEnterpriseByID(ctx context.Context, enterpriseID string) (params.Enterprise, error)
ListEnterprises(ctx context.Context) ([]params.Enterprise, error)
ListEnterprises(ctx context.Context, filter params.EnterpriseFilter) ([]params.Enterprise, error)
DeleteEnterprise(ctx context.Context, enterpriseID string) error
UpdateEnterprise(ctx context.Context, enterpriseID string, param params.UpdateEntityParams) (params.Enterprise, error)
}

View file

@ -111,13 +111,19 @@ func (s *sqlDatabase) GetEnterpriseByID(ctx context.Context, enterpriseID string
return param, nil
}
func (s *sqlDatabase) ListEnterprises(_ context.Context) ([]params.Enterprise, error) {
func (s *sqlDatabase) ListEnterprises(_ context.Context, filter params.EnterpriseFilter) ([]params.Enterprise, error) {
var enterprises []Enterprise
q := s.conn.
Preload("Credentials").
Preload("Credentials.Endpoint").
Preload("Endpoint").
Find(&enterprises)
Preload("Endpoint")
if filter.Name != "" {
q = q.Where("name = ?", filter.Name)
}
if filter.Endpoint != "" {
q = q.Where("endpoint_name = ?", filter.Endpoint)
}
q = q.Find(&enterprises)
if q.Error != nil {
return []params.Enterprise{}, errors.Wrap(q.Error, "fetching enterprises")
}

View file

@ -54,8 +54,10 @@ type EnterpriseTestSuite struct {
adminUserID string
testCreds params.ForgeCredentials
ghesCreds params.ForgeCredentials
secondaryTestCreds params.ForgeCredentials
githubEndpoint params.ForgeEndpoint
ghesEndpoint params.ForgeEndpoint
}
func (s *EnterpriseTestSuite) equalInstancesByName(expected, actual []params.Instance) {
@ -90,7 +92,9 @@ func (s *EnterpriseTestSuite) SetupTest() {
s.Require().NotEmpty(s.adminUserID)
s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T())
s.ghesEndpoint = garmTesting.CreateGHESEndpoint(adminCtx, db, s.T())
s.testCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), s.githubEndpoint)
s.ghesCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "ghes-creds", db, s.T(), s.ghesEndpoint)
s.secondaryTestCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "secondary-creds", db, s.T(), s.githubEndpoint)
// create some enterprise objects in the database, for testing purposes
@ -272,18 +276,68 @@ func (s *EnterpriseTestSuite) TestGetEnterpriseDBDecryptingErr() {
}
func (s *EnterpriseTestSuite) TestListEnterprises() {
enterprises, err := s.Store.ListEnterprises(s.adminCtx)
enterprises, err := s.Store.ListEnterprises(s.adminCtx, params.EnterpriseFilter{})
s.Require().Nil(err)
garmTesting.EqualDBEntityByName(s.T(), s.Fixtures.Enterprises, enterprises)
}
func (s *EnterpriseTestSuite) TestListEnterprisesWithFilter() {
enterprise, err := s.Store.CreateEnterprise(
s.adminCtx,
"test-enterprise",
s.ghesCreds,
"test-secret",
params.PoolBalancerTypeRoundRobin,
)
s.Require().NoError(err)
enterprise2, err := s.Store.CreateEnterprise(
s.adminCtx,
"test-enterprise",
s.testCreds,
"test-secret",
params.PoolBalancerTypeRoundRobin,
)
s.Require().NoError(err)
enterprise3, err := s.Store.CreateEnterprise(
s.adminCtx,
"test-enterprise2",
s.testCreds,
"test-secret",
params.PoolBalancerTypeRoundRobin,
)
s.Require().NoError(err)
enterprises, err := s.Store.ListEnterprises(s.adminCtx, params.EnterpriseFilter{
Name: "test-enterprise",
})
s.Require().Nil(err)
garmTesting.EqualDBEntityByName(s.T(), []params.Enterprise{enterprise, enterprise2}, enterprises)
enterprises, err = s.Store.ListEnterprises(s.adminCtx, params.EnterpriseFilter{
Name: "test-enterprise",
Endpoint: s.ghesEndpoint.Name,
})
s.Require().Nil(err)
garmTesting.EqualDBEntityByName(s.T(), []params.Enterprise{enterprise}, enterprises)
enterprises, err = s.Store.ListEnterprises(s.adminCtx, params.EnterpriseFilter{
Name: "test-enterprise2",
})
s.Require().Nil(err)
garmTesting.EqualDBEntityByName(s.T(), []params.Enterprise{enterprise3}, enterprises)
}
func (s *EnterpriseTestSuite) TestListEnterprisesDBFetchErr() {
s.Fixtures.SQLMock.
ExpectQuery(regexp.QuoteMeta("SELECT * FROM `enterprises` WHERE `enterprises`.`deleted_at` IS NULL")).
WillReturnError(fmt.Errorf("fetching user from database mock error"))
_, err := s.StoreSQLMocked.ListEnterprises(s.adminCtx)
_, err := s.StoreSQLMocked.ListEnterprises(s.adminCtx, params.EnterpriseFilter{})
s.assertSQLMockExpectations()
s.Require().NotNil(err)

View file

@ -92,15 +92,23 @@ func (s *sqlDatabase) GetOrganization(ctx context.Context, name, endpointName st
return param, nil
}
func (s *sqlDatabase) ListOrganizations(_ context.Context) ([]params.Organization, error) {
func (s *sqlDatabase) ListOrganizations(_ context.Context, filter params.OrganizationFilter) ([]params.Organization, error) {
var orgs []Organization
q := s.conn.
Preload("Credentials").
Preload("GiteaCredentials").
Preload("Credentials.Endpoint").
Preload("GiteaCredentials.Endpoint").
Preload("Endpoint").
Find(&orgs)
Preload("Endpoint")
if filter.Name != "" {
q = q.Where("name = ?", filter.Name)
}
if filter.Endpoint != "" {
q = q.Where("endpoint_name = ?", filter.Endpoint)
}
q = q.Find(&orgs)
if q.Error != nil {
return []params.Organization{}, errors.Wrap(q.Error, "fetching org from database")
}

View file

@ -333,18 +333,74 @@ func (s *OrgTestSuite) TestGetOrganizationDBDecryptingErr() {
}
func (s *OrgTestSuite) TestListOrganizations() {
orgs, err := s.Store.ListOrganizations(s.adminCtx)
orgs, err := s.Store.ListOrganizations(s.adminCtx, params.OrganizationFilter{})
s.Require().Nil(err)
garmTesting.EqualDBEntityByName(s.T(), s.Fixtures.Orgs, orgs)
}
func (s *OrgTestSuite) TestListOrganizationsWithFilters() {
org, err := s.Store.CreateOrganization(
s.adminCtx,
"test-org",
s.testCreds,
"super secret",
params.PoolBalancerTypeRoundRobin,
)
s.Require().NoError(err)
org2, err := s.Store.CreateOrganization(
s.adminCtx,
"test-org",
s.testCredsGitea,
"super secret",
params.PoolBalancerTypeRoundRobin,
)
s.Require().NoError(err)
org3, err := s.Store.CreateOrganization(
s.adminCtx,
"test-org2",
s.testCreds,
"super secret",
params.PoolBalancerTypeRoundRobin,
)
s.Require().NoError(err)
orgs, err := s.Store.ListOrganizations(
s.adminCtx,
params.OrganizationFilter{
Name: "test-org",
})
s.Require().Nil(err)
garmTesting.EqualDBEntityByName(s.T(), []params.Organization{org, org2}, orgs)
orgs, err = s.Store.ListOrganizations(
s.adminCtx,
params.OrganizationFilter{
Name: "test-org",
Endpoint: s.giteaEndpoint.Name,
})
s.Require().Nil(err)
garmTesting.EqualDBEntityByName(s.T(), []params.Organization{org2}, orgs)
orgs, err = s.Store.ListOrganizations(
s.adminCtx,
params.OrganizationFilter{
Name: "test-org2",
})
s.Require().Nil(err)
garmTesting.EqualDBEntityByName(s.T(), []params.Organization{org3}, orgs)
}
func (s *OrgTestSuite) TestListOrganizationsDBFetchErr() {
s.Fixtures.SQLMock.
ExpectQuery(regexp.QuoteMeta("SELECT * FROM `organizations` WHERE `organizations`.`deleted_at` IS NULL")).
WillReturnError(fmt.Errorf("fetching user from database mock error"))
_, err := s.StoreSQLMocked.ListOrganizations(s.adminCtx)
_, err := s.StoreSQLMocked.ListOrganizations(s.adminCtx, params.OrganizationFilter{})
s.assertSQLMockExpectations()
s.Require().NotNil(err)

View file

@ -93,15 +93,24 @@ func (s *sqlDatabase) GetRepository(ctx context.Context, owner, name, endpointNa
return param, nil
}
func (s *sqlDatabase) ListRepositories(_ context.Context) ([]params.Repository, error) {
func (s *sqlDatabase) ListRepositories(_ context.Context, filter params.RepositoryFilter) ([]params.Repository, error) {
var repos []Repository
q := s.conn.
Preload("Credentials").
Preload("GiteaCredentials").
Preload("Credentials.Endpoint").
Preload("GiteaCredentials.Endpoint").
Preload("Endpoint").
Find(&repos)
Preload("Endpoint")
if filter.Owner != "" {
q = q.Where("owner = ?", filter.Owner)
}
if filter.Name != "" {
q = q.Where("name = ?", filter.Name)
}
if filter.Endpoint != "" {
q = q.Where("endpoint_name = ?", filter.Endpoint)
}
q = q.Find(&repos)
if q.Error != nil {
return []params.Repository{}, errors.Wrap(q.Error, "fetching user from database")
}

View file

@ -376,18 +376,99 @@ func (s *RepoTestSuite) TestGetRepositoryDBDecryptingErr() {
}
func (s *RepoTestSuite) TestListRepositories() {
repos, err := s.Store.ListRepositories(s.adminCtx)
repos, err := s.Store.ListRepositories(s.adminCtx, params.RepositoryFilter{})
s.Require().Nil(err)
s.equalReposByName(s.Fixtures.Repos, repos)
}
func (s *RepoTestSuite) TestListRepositoriesWithFilters() {
repo, err := s.Store.CreateRepository(
s.adminCtx,
"test-owner",
"test-repo",
s.testCreds,
"super secret",
params.PoolBalancerTypeRoundRobin,
)
s.Require().NoError(err)
repo2, err := s.Store.CreateRepository(
s.adminCtx,
"test-owner",
"test-repo",
s.testCredsGitea,
"super secret",
params.PoolBalancerTypeRoundRobin,
)
s.Require().NoError(err)
repo3, err := s.Store.CreateRepository(
s.adminCtx,
"test-owner",
"test-repo2",
s.testCreds,
"super secret",
params.PoolBalancerTypeRoundRobin,
)
s.Require().NoError(err)
repo4, err := s.Store.CreateRepository(
s.adminCtx,
"test-owner2",
"test-repo",
s.testCreds,
"super secret",
params.PoolBalancerTypeRoundRobin,
)
s.Require().NoError(err)
repos, err := s.Store.ListRepositories(
s.adminCtx,
params.RepositoryFilter{
Name: "test-repo",
})
s.Require().Nil(err)
s.equalReposByName([]params.Repository{repo, repo2, repo4}, repos)
repos, err = s.Store.ListRepositories(
s.adminCtx,
params.RepositoryFilter{
Name: "test-repo",
Owner: "test-owner",
})
s.Require().Nil(err)
s.equalReposByName([]params.Repository{repo, repo2}, repos)
repos, err = s.Store.ListRepositories(
s.adminCtx,
params.RepositoryFilter{
Name: "test-repo",
Owner: "test-owner",
Endpoint: s.giteaEndpoint.Name,
})
s.Require().Nil(err)
s.equalReposByName([]params.Repository{repo2}, repos)
repos, err = s.Store.ListRepositories(
s.adminCtx,
params.RepositoryFilter{
Name: "test-repo2",
})
s.Require().Nil(err)
s.equalReposByName([]params.Repository{repo3}, repos)
}
func (s *RepoTestSuite) TestListRepositoriesDBFetchErr() {
s.Fixtures.SQLMock.
ExpectQuery(regexp.QuoteMeta("SELECT * FROM `repositories` WHERE `repositories`.`deleted_at` IS NULL")).
WillReturnError(fmt.Errorf("fetching user from database mock error"))
_, err := s.StoreSQLMocked.ListRepositories(s.adminCtx)
_, err := s.StoreSQLMocked.ListRepositories(s.adminCtx, params.RepositoryFilter{})
s.Require().NotNil(err)
s.Require().Equal("fetching user from database: fetching user from database mock error", err.Error())
@ -401,7 +482,7 @@ func (s *RepoTestSuite) TestListRepositoriesDBDecryptingErr() {
ExpectQuery(regexp.QuoteMeta("SELECT * FROM `repositories` WHERE `repositories`.`deleted_at` IS NULL")).
WillReturnRows(sqlmock.NewRows([]string{"id", "webhook_secret"}).AddRow(s.Fixtures.Repos[0].ID, s.Fixtures.Repos[0].WebhookSecret))
_, err := s.StoreSQLMocked.ListRepositories(s.adminCtx)
_, err := s.StoreSQLMocked.ListRepositories(s.adminCtx, params.RepositoryFilter{})
s.Require().NotNil(err)
s.Require().Equal("fetching repositories: decrypting secret: invalid passphrase length (expected length 32 characters)", err.Error())