From 499fbde60cfa0eaddf4d5901d873545bba52fe36 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Tue, 17 Jun 2025 22:37:18 +0000 Subject: [PATCH] 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 --- apiserver/controllers/enterprises.go | 19 +++- apiserver/controllers/organizations.go | 19 +++- apiserver/controllers/repositories.go | 26 ++++- apiserver/swagger.yaml | 31 ++++++ .../list_enterprises_parameters.go | 69 ++++++++++++ client/organizations/list_orgs_parameters.go | 69 ++++++++++++ client/repositories/list_repos_parameters.go | 103 ++++++++++++++++++ cmd/garm-cli/cmd/enterprise.go | 5 + cmd/garm-cli/cmd/organization.go | 6 + cmd/garm-cli/cmd/repository.go | 7 ++ database/common/mocks/Store.go | 54 ++++----- database/common/store.go | 6 +- database/sql/enterprise.go | 12 +- database/sql/enterprise_test.go | 58 +++++++++- database/sql/organizations.go | 14 ++- database/sql/organizations_test.go | 60 +++++++++- database/sql/repositories.go | 15 ++- database/sql/repositories_test.go | 87 ++++++++++++++- internal/testing/testing.go | 25 +++++ params/params.go | 16 +++ runner/enterprises.go | 4 +- runner/enterprises_test.go | 68 +++++++++++- runner/metrics/enterprise.go | 3 +- runner/metrics/organization.go | 3 +- runner/metrics/repository.go | 3 +- runner/organizations.go | 4 +- runner/organizations_test.go | 68 +++++++++++- runner/repositories.go | 4 +- runner/repositories_test.go | 75 ++++++++++++- runner/runner.go | 6 +- workers/cache/cache.go | 6 +- workers/entity/controller.go | 7 +- 32 files changed, 879 insertions(+), 73 deletions(-) diff --git a/apiserver/controllers/enterprises.go b/apiserver/controllers/enterprises.go index 9ce278cd..b4b3e528 100644 --- a/apiserver/controllers/enterprises.go +++ b/apiserver/controllers/enterprises.go @@ -66,13 +66,30 @@ func (a *APIController) CreateEnterpriseHandler(w http.ResponseWriter, r *http.R // // List all enterprises. // +// Parameters: +// + name: name +// description: Exact enterprise name to filter by +// type: string +// in: query +// required: false +// +// + name: endpoint +// description: Exact endpoint name to filter by +// type: string +// in: query +// required: false +// // Responses: // 200: Enterprises // default: APIErrorResponse func (a *APIController) ListEnterprisesHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - enterprise, err := a.r.ListEnterprises(ctx) + filter := runnerParams.EnterpriseFilter{ + Name: r.URL.Query().Get("name"), + Endpoint: r.URL.Query().Get("endpoint"), + } + enterprise, err := a.r.ListEnterprises(ctx, filter) if err != nil { slog.With(slog.Any("error", err)).ErrorContext(ctx, "listing enterprise") handleError(ctx, w, err) diff --git a/apiserver/controllers/organizations.go b/apiserver/controllers/organizations.go index 86f3c5d6..9089f440 100644 --- a/apiserver/controllers/organizations.go +++ b/apiserver/controllers/organizations.go @@ -67,13 +67,30 @@ func (a *APIController) CreateOrgHandler(w http.ResponseWriter, r *http.Request) // // List organizations. // +// Parameters: +// + name: name +// description: Exact organization name to filter by +// type: string +// in: query +// required: false +// +// + name: endpoint +// description: Exact endpoint name to filter by +// type: string +// in: query +// required: false +// // Responses: // 200: Organizations // default: APIErrorResponse func (a *APIController) ListOrgsHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - orgs, err := a.r.ListOrganizations(ctx) + filter := runnerParams.OrganizationFilter{ + Name: r.URL.Query().Get("name"), + Endpoint: r.URL.Query().Get("endpoint"), + } + orgs, err := a.r.ListOrganizations(ctx, filter) if err != nil { slog.With(slog.Any("error", err)).ErrorContext(ctx, "listing orgs") handleError(ctx, w, err) diff --git a/apiserver/controllers/repositories.go b/apiserver/controllers/repositories.go index 2eea0001..f3675790 100644 --- a/apiserver/controllers/repositories.go +++ b/apiserver/controllers/repositories.go @@ -67,13 +67,37 @@ func (a *APIController) CreateRepoHandler(w http.ResponseWriter, r *http.Request // // List repositories. // +// Parameters: +// + name: owner +// description: Exact owner name to filter by +// type: string +// in: query +// required: false +// +// + name: name +// description: Exact repository name to filter by +// type: string +// in: query +// required: false +// +// + name: endpoint +// description: Exact endpoint name to filter by +// type: string +// in: query +// required: false +// // Responses: // 200: Repositories // default: APIErrorResponse func (a *APIController) ListReposHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - repos, err := a.r.ListRepositories(ctx) + filter := runnerParams.RepositoryFilter{ + Name: r.URL.Query().Get("name"), + Owner: r.URL.Query().Get("owner"), + Endpoint: r.URL.Query().Get("endpoint"), + } + repos, err := a.r.ListRepositories(ctx, filter) if err != nil { slog.With(slog.Any("error", err)).ErrorContext(ctx, "listing repositories") handleError(ctx, w, err) diff --git a/apiserver/swagger.yaml b/apiserver/swagger.yaml index 66e7a655..bf02a2d7 100644 --- a/apiserver/swagger.yaml +++ b/apiserver/swagger.yaml @@ -418,6 +418,15 @@ paths: /enterprises: get: operationId: ListEnterprises + parameters: + - description: Exact enterprise name to filter by + in: query + name: name + type: string + - description: Exact endpoint name to filter by + in: query + name: endpoint + type: string responses: "200": description: Enterprises @@ -1254,6 +1263,15 @@ paths: /organizations: get: operationId: ListOrgs + parameters: + - description: Exact organization name to filter by + in: query + name: name + type: string + - description: Exact endpoint name to filter by + in: query + name: endpoint + type: string responses: "200": description: Organizations @@ -1754,6 +1772,19 @@ paths: /repositories: get: operationId: ListRepos + parameters: + - description: Exact owner name to filter by + in: query + name: owner + type: string + - description: Exact repository name to filter by + in: query + name: name + type: string + - description: Exact endpoint name to filter by + in: query + name: endpoint + type: string responses: "200": description: Repositories diff --git a/client/enterprises/list_enterprises_parameters.go b/client/enterprises/list_enterprises_parameters.go index 83291c5f..44ba108b 100644 --- a/client/enterprises/list_enterprises_parameters.go +++ b/client/enterprises/list_enterprises_parameters.go @@ -60,6 +60,19 @@ ListEnterprisesParams contains all the parameters to send to the API endpoint Typically these are written to a http.Request. */ type ListEnterprisesParams struct { + + /* Endpoint. + + Exact endpoint name to filter by + */ + Endpoint *string + + /* Name. + + Exact enterprise name to filter by + */ + Name *string + timeout time.Duration Context context.Context HTTPClient *http.Client @@ -113,6 +126,28 @@ func (o *ListEnterprisesParams) SetHTTPClient(client *http.Client) { o.HTTPClient = client } +// WithEndpoint adds the endpoint to the list enterprises params +func (o *ListEnterprisesParams) WithEndpoint(endpoint *string) *ListEnterprisesParams { + o.SetEndpoint(endpoint) + return o +} + +// SetEndpoint adds the endpoint to the list enterprises params +func (o *ListEnterprisesParams) SetEndpoint(endpoint *string) { + o.Endpoint = endpoint +} + +// WithName adds the name to the list enterprises params +func (o *ListEnterprisesParams) WithName(name *string) *ListEnterprisesParams { + o.SetName(name) + return o +} + +// SetName adds the name to the list enterprises params +func (o *ListEnterprisesParams) SetName(name *string) { + o.Name = name +} + // WriteToRequest writes these params to a swagger request func (o *ListEnterprisesParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { @@ -121,6 +156,40 @@ func (o *ListEnterprisesParams) WriteToRequest(r runtime.ClientRequest, reg strf } var res []error + if o.Endpoint != nil { + + // query param endpoint + var qrEndpoint string + + if o.Endpoint != nil { + qrEndpoint = *o.Endpoint + } + qEndpoint := qrEndpoint + if qEndpoint != "" { + + if err := r.SetQueryParam("endpoint", qEndpoint); err != nil { + return err + } + } + } + + if o.Name != nil { + + // query param name + var qrName string + + if o.Name != nil { + qrName = *o.Name + } + qName := qrName + if qName != "" { + + if err := r.SetQueryParam("name", qName); err != nil { + return err + } + } + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } diff --git a/client/organizations/list_orgs_parameters.go b/client/organizations/list_orgs_parameters.go index 1441722f..af4c19c8 100644 --- a/client/organizations/list_orgs_parameters.go +++ b/client/organizations/list_orgs_parameters.go @@ -60,6 +60,19 @@ ListOrgsParams contains all the parameters to send to the API endpoint Typically these are written to a http.Request. */ type ListOrgsParams struct { + + /* Endpoint. + + Exact endpoint name to filter by + */ + Endpoint *string + + /* Name. + + Exact organization name to filter by + */ + Name *string + timeout time.Duration Context context.Context HTTPClient *http.Client @@ -113,6 +126,28 @@ func (o *ListOrgsParams) SetHTTPClient(client *http.Client) { o.HTTPClient = client } +// WithEndpoint adds the endpoint to the list orgs params +func (o *ListOrgsParams) WithEndpoint(endpoint *string) *ListOrgsParams { + o.SetEndpoint(endpoint) + return o +} + +// SetEndpoint adds the endpoint to the list orgs params +func (o *ListOrgsParams) SetEndpoint(endpoint *string) { + o.Endpoint = endpoint +} + +// WithName adds the name to the list orgs params +func (o *ListOrgsParams) WithName(name *string) *ListOrgsParams { + o.SetName(name) + return o +} + +// SetName adds the name to the list orgs params +func (o *ListOrgsParams) SetName(name *string) { + o.Name = name +} + // WriteToRequest writes these params to a swagger request func (o *ListOrgsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { @@ -121,6 +156,40 @@ func (o *ListOrgsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Regi } var res []error + if o.Endpoint != nil { + + // query param endpoint + var qrEndpoint string + + if o.Endpoint != nil { + qrEndpoint = *o.Endpoint + } + qEndpoint := qrEndpoint + if qEndpoint != "" { + + if err := r.SetQueryParam("endpoint", qEndpoint); err != nil { + return err + } + } + } + + if o.Name != nil { + + // query param name + var qrName string + + if o.Name != nil { + qrName = *o.Name + } + qName := qrName + if qName != "" { + + if err := r.SetQueryParam("name", qName); err != nil { + return err + } + } + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } diff --git a/client/repositories/list_repos_parameters.go b/client/repositories/list_repos_parameters.go index f4e17d79..9998a1ba 100644 --- a/client/repositories/list_repos_parameters.go +++ b/client/repositories/list_repos_parameters.go @@ -60,6 +60,25 @@ ListReposParams contains all the parameters to send to the API endpoint Typically these are written to a http.Request. */ type ListReposParams struct { + + /* Endpoint. + + Exact endpoint name to filter by + */ + Endpoint *string + + /* Name. + + Exact repository name to filter by + */ + Name *string + + /* Owner. + + Exact owner name to filter by + */ + Owner *string + timeout time.Duration Context context.Context HTTPClient *http.Client @@ -113,6 +132,39 @@ func (o *ListReposParams) SetHTTPClient(client *http.Client) { o.HTTPClient = client } +// WithEndpoint adds the endpoint to the list repos params +func (o *ListReposParams) WithEndpoint(endpoint *string) *ListReposParams { + o.SetEndpoint(endpoint) + return o +} + +// SetEndpoint adds the endpoint to the list repos params +func (o *ListReposParams) SetEndpoint(endpoint *string) { + o.Endpoint = endpoint +} + +// WithName adds the name to the list repos params +func (o *ListReposParams) WithName(name *string) *ListReposParams { + o.SetName(name) + return o +} + +// SetName adds the name to the list repos params +func (o *ListReposParams) SetName(name *string) { + o.Name = name +} + +// WithOwner adds the owner to the list repos params +func (o *ListReposParams) WithOwner(owner *string) *ListReposParams { + o.SetOwner(owner) + return o +} + +// SetOwner adds the owner to the list repos params +func (o *ListReposParams) SetOwner(owner *string) { + o.Owner = owner +} + // WriteToRequest writes these params to a swagger request func (o *ListReposParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { @@ -121,6 +173,57 @@ func (o *ListReposParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Reg } var res []error + if o.Endpoint != nil { + + // query param endpoint + var qrEndpoint string + + if o.Endpoint != nil { + qrEndpoint = *o.Endpoint + } + qEndpoint := qrEndpoint + if qEndpoint != "" { + + if err := r.SetQueryParam("endpoint", qEndpoint); err != nil { + return err + } + } + } + + if o.Name != nil { + + // query param name + var qrName string + + if o.Name != nil { + qrName = *o.Name + } + qName := qrName + if qName != "" { + + if err := r.SetQueryParam("name", qName); err != nil { + return err + } + } + } + + if o.Owner != nil { + + // query param owner + var qrOwner string + + if o.Owner != nil { + qrOwner = *o.Owner + } + qOwner := qrOwner + if qOwner != "" { + + if err := r.SetQueryParam("owner", qOwner); err != nil { + return err + } + } + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } diff --git a/cmd/garm-cli/cmd/enterprise.go b/cmd/garm-cli/cmd/enterprise.go index 1e6c3930..b8850e1b 100644 --- a/cmd/garm-cli/cmd/enterprise.go +++ b/cmd/garm-cli/cmd/enterprise.go @@ -28,6 +28,7 @@ import ( var ( enterpriseName string + enterpriseEndpoint string enterpriseWebhookSecret string enterpriseCreds string ) @@ -85,6 +86,8 @@ var enterpriseListCmd = &cobra.Command{ } listEnterprisesReq := apiClientEnterprises.NewListEnterprisesParams() + listEnterprisesReq.Name = &enterpriseName + listEnterprisesReq.Endpoint = &enterpriseEndpoint response, err := apiCli.Enterprises.ListEnterprises(listEnterprisesReq, authToken) if err != nil { return err @@ -185,6 +188,8 @@ func init() { enterpriseAddCmd.Flags().StringVar(&poolBalancerType, "pool-balancer-type", string(params.PoolBalancerTypeRoundRobin), "The balancing strategy to use when creating runners in pools matching requested labels.") enterpriseListCmd.Flags().BoolVarP(&long, "long", "l", false, "Include additional info.") + enterpriseListCmd.Flags().StringVarP(&enterpriseName, "name", "n", "", "Exact enterprise name to filter by.") + enterpriseListCmd.Flags().StringVarP(&enterpriseEndpoint, "endpoint", "e", "", "Exact endpoint name to filter by.") enterpriseAddCmd.MarkFlagRequired("credentials") //nolint enterpriseAddCmd.MarkFlagRequired("name") //nolint diff --git a/cmd/garm-cli/cmd/organization.go b/cmd/garm-cli/cmd/organization.go index a95f912f..9f23888a 100644 --- a/cmd/garm-cli/cmd/organization.go +++ b/cmd/garm-cli/cmd/organization.go @@ -29,6 +29,7 @@ import ( var ( orgName string + orgEndpoint string orgWebhookSecret string orgCreds string orgRandomWebhookSecret bool @@ -243,6 +244,8 @@ var orgListCmd = &cobra.Command{ } listOrgsReq := apiClientOrgs.NewListOrgsParams() + listOrgsReq.Name = &orgName + listOrgsReq.Endpoint = &orgEndpoint response, err := apiCli.Organizations.ListOrgs(listOrgsReq, authToken) if err != nil { return err @@ -314,7 +317,10 @@ func init() { orgAddCmd.Flags().BoolVar(&installOrgWebhook, "install-webhook", false, "Install the webhook as part of the add operation.") orgAddCmd.MarkFlagsMutuallyExclusive("webhook-secret", "random-webhook-secret") orgAddCmd.MarkFlagsOneRequired("webhook-secret", "random-webhook-secret") + orgListCmd.Flags().BoolVarP(&long, "long", "l", false, "Include additional info.") + orgListCmd.Flags().StringVarP(&orgName, "name", "n", "", "Exact org name to filter by.") + orgListCmd.Flags().StringVarP(&orgEndpoint, "endpoint", "e", "", "Exact endpoint name to filter by.") orgAddCmd.MarkFlagRequired("credentials") //nolint orgAddCmd.MarkFlagRequired("name") //nolint diff --git a/cmd/garm-cli/cmd/repository.go b/cmd/garm-cli/cmd/repository.go index 5bf588c5..91db23ea 100644 --- a/cmd/garm-cli/cmd/repository.go +++ b/cmd/garm-cli/cmd/repository.go @@ -30,6 +30,7 @@ import ( var ( repoOwner string repoName string + repoEndpoint string repoWebhookSecret string repoCreds string forgeType string @@ -213,6 +214,9 @@ var repoListCmd = &cobra.Command{ } listReposReq := apiClientRepos.NewListReposParams() + listReposReq.Name = &repoName + listReposReq.Owner = &repoOwner + listReposReq.Endpoint = &repoEndpoint response, err := apiCli.Repositories.ListRepos(listReposReq, authToken) if err != nil { return err @@ -321,6 +325,9 @@ func init() { repoAddCmd.MarkFlagsOneRequired("webhook-secret", "random-webhook-secret") repoListCmd.Flags().BoolVarP(&long, "long", "l", false, "Include additional info.") + repoListCmd.Flags().StringVarP(&repoName, "name", "n", "", "Exact repo name to filter by.") + repoListCmd.Flags().StringVarP(&repoOwner, "owner", "o", "", "Exact repo owner to filter by.") + repoListCmd.Flags().StringVarP(&repoEndpoint, "endpoint", "e", "", "Exact endpoint name to filter by.") repoAddCmd.MarkFlagRequired("credentials") //nolint repoAddCmd.MarkFlagRequired("owner") //nolint diff --git a/database/common/mocks/Store.go b/database/common/mocks/Store.go index 97da1c06..ec107854 100644 --- a/database/common/mocks/Store.go +++ b/database/common/mocks/Store.go @@ -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) } diff --git a/database/common/store.go b/database/common/store.go index db5fbb04..8b3c4f7c 100644 --- a/database/common/store.go +++ b/database/common/store.go @@ -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) } diff --git a/database/sql/enterprise.go b/database/sql/enterprise.go index 41d95b26..fc273165 100644 --- a/database/sql/enterprise.go +++ b/database/sql/enterprise.go @@ -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") } diff --git a/database/sql/enterprise_test.go b/database/sql/enterprise_test.go index 79b298d5..056bb7fa 100644 --- a/database/sql/enterprise_test.go +++ b/database/sql/enterprise_test.go @@ -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) diff --git a/database/sql/organizations.go b/database/sql/organizations.go index 73456362..3b1a05fa 100644 --- a/database/sql/organizations.go +++ b/database/sql/organizations.go @@ -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") } diff --git a/database/sql/organizations_test.go b/database/sql/organizations_test.go index 5c053cec..df876ba1 100644 --- a/database/sql/organizations_test.go +++ b/database/sql/organizations_test.go @@ -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) diff --git a/database/sql/repositories.go b/database/sql/repositories.go index 03452df6..a18eb001 100644 --- a/database/sql/repositories.go +++ b/database/sql/repositories.go @@ -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") } diff --git a/database/sql/repositories_test.go b/database/sql/repositories_test.go index f593ddce..4609a357 100644 --- a/database/sql/repositories_test.go +++ b/database/sql/repositories_test.go @@ -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()) diff --git a/internal/testing/testing.go b/internal/testing/testing.go index 84b4d48c..98bfd34c 100644 --- a/internal/testing/testing.go +++ b/internal/testing/testing.go @@ -85,6 +85,31 @@ func CreateGARMTestUser(ctx context.Context, username string, db common.Store, s return user } +func CreateGHESEndpoint(ctx context.Context, db common.Store, s *testing.T) params.ForgeEndpoint { + endpointParams := params.CreateGithubEndpointParams{ + Name: "ghes.example.com", + Description: "GHES endpoint", + APIBaseURL: "https://ghes.example.com", + UploadBaseURL: "https://upload.ghes.example.com/", + BaseURL: "https://ghes.example.com", + } + + ep, err := db.GetGithubEndpoint(ctx, endpointParams.Name) + if err != nil { + if !errors.Is(err, runnerErrors.ErrNotFound) { + s.Fatalf("failed to get database object (%s): %v", endpointParams.Name, err) + } + ep, err = db.CreateGithubEndpoint(ctx, endpointParams) + if err != nil { + if !errors.Is(err, runnerErrors.ErrDuplicateEntity) { + s.Fatalf("failed to create database object (%s): %v", endpointParams.Name, err) + } + } + } + + return ep +} + func CreateDefaultGithubEndpoint(ctx context.Context, db common.Store, s *testing.T) params.ForgeEndpoint { endpointParams := params.CreateGithubEndpointParams{ Name: "github.com", diff --git a/params/params.go b/params/params.go index 2a7fdef9..c9d4fb94 100644 --- a/params/params.go +++ b/params/params.go @@ -1192,3 +1192,19 @@ type ForgeEndpoint struct { EndpointType EndpointType `json:"endpoint_type,omitempty"` } + +type RepositoryFilter struct { + Owner string + Name string + Endpoint string +} + +type OrganizationFilter struct { + Name string + Endpoint string +} + +type EnterpriseFilter struct { + Name string + Endpoint string +} diff --git a/runner/enterprises.go b/runner/enterprises.go index f192c7cd..341cf5b9 100644 --- a/runner/enterprises.go +++ b/runner/enterprises.go @@ -86,12 +86,12 @@ func (r *Runner) CreateEnterprise(ctx context.Context, param params.CreateEnterp return enterprise, nil } -func (r *Runner) ListEnterprises(ctx context.Context) ([]params.Enterprise, error) { +func (r *Runner) ListEnterprises(ctx context.Context, filter params.EnterpriseFilter) ([]params.Enterprise, error) { if !auth.IsAdmin(ctx) { return nil, runnerErrors.ErrUnauthorized } - enterprises, err := r.store.ListEnterprises(ctx) + enterprises, err := r.store.ListEnterprises(ctx, filter) if err != nil { return nil, errors.Wrap(err, "listing enterprises") } diff --git a/runner/enterprises_test.go b/runner/enterprises_test.go index d5eef463..ce791e55 100644 --- a/runner/enterprises_test.go +++ b/runner/enterprises_test.go @@ -59,6 +59,8 @@ type EnterpriseTestSuite struct { testCreds params.ForgeCredentials secondaryTestCreds params.ForgeCredentials forgeEndpoint params.ForgeEndpoint + ghesEndpoint params.ForgeEndpoint + ghesCreds params.ForgeCredentials } func (s *EnterpriseTestSuite) SetupTest() { @@ -71,8 +73,10 @@ func (s *EnterpriseTestSuite) SetupTest() { adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T()) s.forgeEndpoint = 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.forgeEndpoint) s.secondaryTestCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "secondary-creds", db, s.T(), s.forgeEndpoint) + s.ghesCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "ghes-creds", db, s.T(), s.ghesEndpoint) // create some organization objects in the database, for testing purposes enterprises := map[string]params.Enterprise{} @@ -224,14 +228,74 @@ func (s *EnterpriseTestSuite) TestCreateEnterpriseStartPoolMgrFailed() { func (s *EnterpriseTestSuite) TestListEnterprises() { s.Fixtures.PoolMgrCtrlMock.On("GetEnterprisePoolManager", mock.AnythingOfType("params.Enterprise")).Return(s.Fixtures.PoolMgrMock, nil) s.Fixtures.PoolMgrMock.On("Status").Return(params.PoolManagerStatus{IsRunning: true}, nil) - orgs, err := s.Runner.ListEnterprises(s.Fixtures.AdminContext) + orgs, err := s.Runner.ListEnterprises(s.Fixtures.AdminContext, params.EnterpriseFilter{}) s.Require().Nil(err) garmTesting.EqualDBEntityByName(s.T(), garmTesting.DBEntityMapToSlice(s.Fixtures.StoreEnterprises), orgs) } +func (s *EnterpriseTestSuite) TestListEnterprisesWithFilters() { + s.Fixtures.PoolMgrCtrlMock.On("GetEnterprisePoolManager", mock.AnythingOfType("params.Enterprise")).Return(s.Fixtures.PoolMgrMock, nil) + s.Fixtures.PoolMgrMock.On("Status").Return(params.PoolManagerStatus{IsRunning: true}, nil) + + enterprise, err := s.Fixtures.Store.CreateEnterprise( + s.Fixtures.AdminContext, + "test-enterprise", + s.testCreds, + "super secret", + params.PoolBalancerTypeRoundRobin, + ) + s.Require().NoError(err) + enterprise2, err := s.Fixtures.Store.CreateEnterprise( + s.Fixtures.AdminContext, + "test-enterprise2", + s.testCreds, + "super secret", + params.PoolBalancerTypeRoundRobin, + ) + s.Require().NoError(err) + enterprise3, err := s.Fixtures.Store.CreateEnterprise( + s.Fixtures.AdminContext, + "test-enterprise", + s.ghesCreds, + "super secret", + params.PoolBalancerTypeRoundRobin, + ) + s.Require().NoError(err) + orgs, err := s.Runner.ListEnterprises( + s.Fixtures.AdminContext, + params.EnterpriseFilter{ + Name: "test-enterprise", + }, + ) + + s.Require().Nil(err) + garmTesting.EqualDBEntityByName(s.T(), []params.Enterprise{enterprise, enterprise3}, orgs) + + orgs, err = s.Runner.ListEnterprises( + s.Fixtures.AdminContext, + params.EnterpriseFilter{ + Name: "test-enterprise", + Endpoint: s.ghesEndpoint.Name, + }, + ) + + s.Require().Nil(err) + garmTesting.EqualDBEntityByName(s.T(), []params.Enterprise{enterprise3}, orgs) + + orgs, err = s.Runner.ListEnterprises( + s.Fixtures.AdminContext, + params.EnterpriseFilter{ + Name: "test-enterprise2", + }, + ) + + s.Require().Nil(err) + garmTesting.EqualDBEntityByName(s.T(), []params.Enterprise{enterprise2}, orgs) +} + func (s *EnterpriseTestSuite) TestListEnterprisesErrUnauthorized() { - _, err := s.Runner.ListEnterprises(context.Background()) + _, err := s.Runner.ListEnterprises(context.Background(), params.EnterpriseFilter{}) s.Require().Equal(runnerErrors.ErrUnauthorized, err) } diff --git a/runner/metrics/enterprise.go b/runner/metrics/enterprise.go index 3ab9003c..be6eba66 100644 --- a/runner/metrics/enterprise.go +++ b/runner/metrics/enterprise.go @@ -19,6 +19,7 @@ import ( "strconv" "github.com/cloudbase/garm/metrics" + "github.com/cloudbase/garm/params" "github.com/cloudbase/garm/runner" //nolint:typecheck ) @@ -28,7 +29,7 @@ func CollectEnterpriseMetric(ctx context.Context, r *runner.Runner) error { metrics.EnterpriseInfo.Reset() metrics.EnterprisePoolManagerStatus.Reset() - enterprises, err := r.ListEnterprises(ctx) + enterprises, err := r.ListEnterprises(ctx, params.EnterpriseFilter{}) if err != nil { return err } diff --git a/runner/metrics/organization.go b/runner/metrics/organization.go index 3716cca1..6bf6d9e5 100644 --- a/runner/metrics/organization.go +++ b/runner/metrics/organization.go @@ -19,6 +19,7 @@ import ( "strconv" "github.com/cloudbase/garm/metrics" + "github.com/cloudbase/garm/params" "github.com/cloudbase/garm/runner" ) @@ -28,7 +29,7 @@ func CollectOrganizationMetric(ctx context.Context, r *runner.Runner) error { metrics.OrganizationInfo.Reset() metrics.OrganizationPoolManagerStatus.Reset() - organizations, err := r.ListOrganizations(ctx) + organizations, err := r.ListOrganizations(ctx, params.OrganizationFilter{}) if err != nil { return err } diff --git a/runner/metrics/repository.go b/runner/metrics/repository.go index 36e07bf0..a2e8fa57 100644 --- a/runner/metrics/repository.go +++ b/runner/metrics/repository.go @@ -19,6 +19,7 @@ import ( "strconv" "github.com/cloudbase/garm/metrics" + "github.com/cloudbase/garm/params" "github.com/cloudbase/garm/runner" ) @@ -27,7 +28,7 @@ func CollectRepositoryMetric(ctx context.Context, r *runner.Runner) error { metrics.EnterpriseInfo.Reset() metrics.EnterprisePoolManagerStatus.Reset() - repositories, err := r.ListRepositories(ctx) + repositories, err := r.ListRepositories(ctx, params.RepositoryFilter{}) if err != nil { return err } diff --git a/runner/organizations.go b/runner/organizations.go index 26d4f6e9..0ec4bfa2 100644 --- a/runner/organizations.go +++ b/runner/organizations.go @@ -95,12 +95,12 @@ func (r *Runner) CreateOrganization(ctx context.Context, param params.CreateOrgP return org, nil } -func (r *Runner) ListOrganizations(ctx context.Context) ([]params.Organization, error) { +func (r *Runner) ListOrganizations(ctx context.Context, filter params.OrganizationFilter) ([]params.Organization, error) { if !auth.IsAdmin(ctx) { return nil, runnerErrors.ErrUnauthorized } - orgs, err := r.store.ListOrganizations(ctx) + orgs, err := r.store.ListOrganizations(ctx, filter) if err != nil { return nil, errors.Wrap(err, "listing organizations") } diff --git a/runner/organizations_test.go b/runner/organizations_test.go index 90075c87..9de6d2b4 100644 --- a/runner/organizations_test.go +++ b/runner/organizations_test.go @@ -58,7 +58,9 @@ type OrgTestSuite struct { testCreds params.ForgeCredentials secondaryTestCreds params.ForgeCredentials + giteaTestCreds params.ForgeCredentials githubEndpoint params.ForgeEndpoint + giteaEndpoint params.ForgeEndpoint } func (s *OrgTestSuite) SetupTest() { @@ -72,7 +74,9 @@ func (s *OrgTestSuite) SetupTest() { adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T()) s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) + s.giteaEndpoint = garmTesting.CreateDefaultGiteaEndpoint(adminCtx, db, s.T()) s.testCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), s.githubEndpoint) + s.giteaTestCreds = garmTesting.CreateTestGiteaCredentials(adminCtx, "gitea-creds", db, s.T(), s.giteaEndpoint) s.secondaryTestCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "secondary-creds", db, s.T(), s.githubEndpoint) // create some organization objects in the database, for testing purposes @@ -238,14 +242,74 @@ func (s *OrgTestSuite) TestCreateOrganizationStartPoolMgrFailed() { func (s *OrgTestSuite) TestListOrganizations() { s.Fixtures.PoolMgrCtrlMock.On("GetOrgPoolManager", mock.AnythingOfType("params.Organization")).Return(s.Fixtures.PoolMgrMock, nil) s.Fixtures.PoolMgrMock.On("Status").Return(params.PoolManagerStatus{IsRunning: true}, nil) - orgs, err := s.Runner.ListOrganizations(s.Fixtures.AdminContext) + orgs, err := s.Runner.ListOrganizations(s.Fixtures.AdminContext, params.OrganizationFilter{}) s.Require().Nil(err) garmTesting.EqualDBEntityByName(s.T(), garmTesting.DBEntityMapToSlice(s.Fixtures.StoreOrgs), orgs) } +func (s *OrgTestSuite) TestListOrganizationsWithFilter() { + s.Fixtures.PoolMgrCtrlMock.On("GetOrgPoolManager", mock.AnythingOfType("params.Organization")).Return(s.Fixtures.PoolMgrMock, nil) + s.Fixtures.PoolMgrMock.On("Status").Return(params.PoolManagerStatus{IsRunning: true}, nil) + + org, err := s.Fixtures.Store.CreateOrganization( + s.Fixtures.AdminContext, + "test-org", + s.testCreds, + "super-secret", + params.PoolBalancerTypeRoundRobin) + s.Require().NoError(err) + + org2, err := s.Fixtures.Store.CreateOrganization( + s.Fixtures.AdminContext, + "test-org", + s.giteaTestCreds, + "super-secret", + params.PoolBalancerTypeRoundRobin) + s.Require().NoError(err) + + org3, err := s.Fixtures.Store.CreateOrganization( + s.Fixtures.AdminContext, + "test-org2", + s.giteaTestCreds, + "super-secret", + params.PoolBalancerTypeRoundRobin) + s.Require().NoError(err) + + orgs, err := s.Runner.ListOrganizations( + s.Fixtures.AdminContext, + params.OrganizationFilter{ + Name: "test-org", + }, + ) + + s.Require().Nil(err) + garmTesting.EqualDBEntityByName(s.T(), []params.Organization{org, org2}, orgs) + + orgs, err = s.Runner.ListOrganizations( + s.Fixtures.AdminContext, + params.OrganizationFilter{ + Name: "test-org", + Endpoint: s.giteaEndpoint.Name, + }, + ) + + s.Require().Nil(err) + garmTesting.EqualDBEntityByName(s.T(), []params.Organization{org2}, orgs) + + orgs, err = s.Runner.ListOrganizations( + s.Fixtures.AdminContext, + params.OrganizationFilter{ + Name: "test-org2", + }, + ) + + s.Require().Nil(err) + garmTesting.EqualDBEntityByName(s.T(), []params.Organization{org3}, orgs) +} + func (s *OrgTestSuite) TestListOrganizationsErrUnauthorized() { - _, err := s.Runner.ListOrganizations(context.Background()) + _, err := s.Runner.ListOrganizations(context.Background(), params.OrganizationFilter{}) s.Require().Equal(runnerErrors.ErrUnauthorized, err) } diff --git a/runner/repositories.go b/runner/repositories.go index d5118e96..24beaa07 100644 --- a/runner/repositories.go +++ b/runner/repositories.go @@ -93,12 +93,12 @@ func (r *Runner) CreateRepository(ctx context.Context, param params.CreateRepoPa return repo, nil } -func (r *Runner) ListRepositories(ctx context.Context) ([]params.Repository, error) { +func (r *Runner) ListRepositories(ctx context.Context, filter params.RepositoryFilter) ([]params.Repository, error) { if !auth.IsAdmin(ctx) { return nil, runnerErrors.ErrUnauthorized } - repos, err := r.store.ListRepositories(ctx) + repos, err := r.store.ListRepositories(ctx, filter) if err != nil { return nil, errors.Wrap(err, "listing repositories") } diff --git a/runner/repositories_test.go b/runner/repositories_test.go index 0adf40d7..53fe5869 100644 --- a/runner/repositories_test.go +++ b/runner/repositories_test.go @@ -62,7 +62,9 @@ type RepoTestSuite struct { testCreds params.ForgeCredentials secondaryTestCreds params.ForgeCredentials + giteaTestCreds params.ForgeCredentials githubEndpoint params.ForgeEndpoint + giteaEndpoint params.ForgeEndpoint } func (s *RepoTestSuite) SetupTest() { @@ -75,8 +77,10 @@ func (s *RepoTestSuite) SetupTest() { adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T()) s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) + s.giteaEndpoint = garmTesting.CreateDefaultGiteaEndpoint(adminCtx, db, s.T()) s.testCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), s.githubEndpoint) s.secondaryTestCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "secondary-creds", db, s.T(), s.githubEndpoint) + s.giteaTestCreds = garmTesting.CreateTestGiteaCredentials(adminCtx, "gitea-creds", db, s.T(), s.giteaEndpoint) // create some repository objects in the database, for testing purposes repos := map[string]params.Repository{} @@ -254,14 +258,81 @@ func (s *RepoTestSuite) TestCreateRepositoryStartPoolMgrFailed() { func (s *RepoTestSuite) TestListRepositories() { s.Fixtures.PoolMgrCtrlMock.On("GetRepoPoolManager", mock.AnythingOfType("params.Repository")).Return(s.Fixtures.PoolMgrMock, nil) s.Fixtures.PoolMgrMock.On("Status").Return(params.PoolManagerStatus{IsRunning: true}, nil) - repos, err := s.Runner.ListRepositories(s.Fixtures.AdminContext) + repos, err := s.Runner.ListRepositories(s.Fixtures.AdminContext, params.RepositoryFilter{}) s.Require().Nil(err) garmTesting.EqualDBEntityByName(s.T(), garmTesting.DBEntityMapToSlice(s.Fixtures.StoreRepos), repos) } +func (s *RepoTestSuite) TestListRepositoriesWithFilters() { + s.Fixtures.PoolMgrCtrlMock.On("GetRepoPoolManager", mock.AnythingOfType("params.Repository")).Return(s.Fixtures.PoolMgrMock, nil) + s.Fixtures.PoolMgrMock.On("Status").Return(params.PoolManagerStatus{IsRunning: true}, nil) + + repo, err := s.Fixtures.Store.CreateRepository( + s.Fixtures.AdminContext, + "example-owner", + "example-repo", + s.testCreds, + "test-webhook-secret", + params.PoolBalancerTypeRoundRobin, + ) + if err != nil { + s.FailNow(fmt.Sprintf("failed to create database object (example-repo): %q", err)) + } + + repo2, err := s.Fixtures.Store.CreateRepository( + s.Fixtures.AdminContext, + "another-example-owner", + "example-repo", + s.testCreds, + "test-webhook-secret", + params.PoolBalancerTypeRoundRobin, + ) + if err != nil { + s.FailNow(fmt.Sprintf("failed to create database object (example-repo): %q", err)) + } + + repo3, err := s.Fixtures.Store.CreateRepository( + s.Fixtures.AdminContext, + "example-owner", + "example-repo", + s.giteaTestCreds, + "test-webhook-secret", + params.PoolBalancerTypeRoundRobin, + ) + if err != nil { + s.FailNow(fmt.Sprintf("failed to create database object (example-repo): %q", err)) + } + + repos, err := s.Runner.ListRepositories(s.Fixtures.AdminContext, params.RepositoryFilter{Name: "example-repo"}) + + s.Require().Nil(err) + garmTesting.EqualDBEntityByName(s.T(), []params.Repository{repo, repo2, repo3}, repos) + + repos, err = s.Runner.ListRepositories( + s.Fixtures.AdminContext, + params.RepositoryFilter{ + Name: "example-repo", + Owner: "example-owner", + }, + ) + s.Require().Nil(err) + garmTesting.EqualDBEntityByName(s.T(), []params.Repository{repo, repo3}, repos) + + repos, err = s.Runner.ListRepositories( + s.Fixtures.AdminContext, + params.RepositoryFilter{ + Name: "example-repo", + Owner: "example-owner", + Endpoint: s.giteaEndpoint.Name, + }, + ) + s.Require().Nil(err) + garmTesting.EqualDBEntityByName(s.T(), []params.Repository{repo3}, repos) +} + func (s *RepoTestSuite) TestListRepositoriesErrUnauthorized() { - _, err := s.Runner.ListRepositories(context.Background()) + _, err := s.Runner.ListRepositories(context.Background(), params.RepositoryFilter{}) s.Require().Equal(runnerErrors.ErrUnauthorized, err) } diff --git a/runner/runner.go b/runner/runner.go index aa55ee4f..da3f35ea 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -327,17 +327,17 @@ func (r *Runner) loadReposOrgsAndEnterprises() error { r.mux.Lock() defer r.mux.Unlock() - repos, err := r.store.ListRepositories(r.ctx) + repos, err := r.store.ListRepositories(r.ctx, params.RepositoryFilter{}) if err != nil { return errors.Wrap(err, "fetching repositories") } - orgs, err := r.store.ListOrganizations(r.ctx) + orgs, err := r.store.ListOrganizations(r.ctx, params.OrganizationFilter{}) if err != nil { return errors.Wrap(err, "fetching organizations") } - enterprises, err := r.store.ListEnterprises(r.ctx) + enterprises, err := r.store.ListEnterprises(r.ctx, params.EnterpriseFilter{}) if err != nil { return errors.Wrap(err, "fetching enterprises") } diff --git a/workers/cache/cache.go b/workers/cache/cache.go index a00c7667..3f589edd 100644 --- a/workers/cache/cache.go +++ b/workers/cache/cache.go @@ -96,17 +96,17 @@ func (w *Worker) loadAllEntities() error { return fmt.Errorf("listing scale sets: %w", err) } - repos, err := w.store.ListRepositories(w.ctx) + repos, err := w.store.ListRepositories(w.ctx, params.RepositoryFilter{}) if err != nil { return fmt.Errorf("listing repositories: %w", err) } - orgs, err := w.store.ListOrganizations(w.ctx) + orgs, err := w.store.ListOrganizations(w.ctx, params.OrganizationFilter{}) if err != nil { return fmt.Errorf("listing organizations: %w", err) } - enterprises, err := w.store.ListEnterprises(w.ctx) + enterprises, err := w.store.ListEnterprises(w.ctx, params.EnterpriseFilter{}) if err != nil { return fmt.Errorf("listing enterprises: %w", err) } diff --git a/workers/entity/controller.go b/workers/entity/controller.go index 99618194..3ad52108 100644 --- a/workers/entity/controller.go +++ b/workers/entity/controller.go @@ -24,6 +24,7 @@ import ( "github.com/cloudbase/garm/auth" dbCommon "github.com/cloudbase/garm/database/common" "github.com/cloudbase/garm/database/watcher" + "github.com/cloudbase/garm/params" "github.com/cloudbase/garm/runner/common" garmUtil "github.com/cloudbase/garm/util" ) @@ -63,7 +64,7 @@ type Controller struct { func (c *Controller) loadAllRepositories() error { c.mux.Lock() defer c.mux.Unlock() - repos, err := c.store.ListRepositories(c.ctx) + repos, err := c.store.ListRepositories(c.ctx, params.RepositoryFilter{}) if err != nil { return fmt.Errorf("fetching repositories: %w", err) } @@ -95,7 +96,7 @@ func (c *Controller) loadAllRepositories() error { func (c *Controller) loadAllOrganizations() error { c.mux.Lock() defer c.mux.Unlock() - orgs, err := c.store.ListOrganizations(c.ctx) + orgs, err := c.store.ListOrganizations(c.ctx, params.OrganizationFilter{}) if err != nil { return fmt.Errorf("fetching organizations: %w", err) } @@ -127,7 +128,7 @@ func (c *Controller) loadAllOrganizations() error { func (c *Controller) loadAllEnterprises() error { c.mux.Lock() defer c.mux.Unlock() - enterprises, err := c.store.ListEnterprises(c.ctx) + enterprises, err := c.store.ListEnterprises(c.ctx, params.EnterpriseFilter{}) if err != nil { return fmt.Errorf("fetching enterprises: %w", err) }