diff --git a/auth/instance_middleware.go b/auth/instance_middleware.go index 54fca249..0d3e4f2a 100644 --- a/auth/instance_middleware.go +++ b/auth/instance_middleware.go @@ -39,14 +39,14 @@ type InstanceJWTClaims struct { Name string `json:"name"` PoolID string `json:"provider_id"` // Scope is either repository or organization - Scope params.PoolType `json:"scope"` + Scope params.GithubEntityType `json:"scope"` // Entity is the repo or org name Entity string `json:"entity"` CreateAttempt int `json:"create_attempt"` jwt.RegisteredClaims } -func NewInstanceJWTToken(instance params.Instance, secret, entity string, poolType params.PoolType, ttlMinutes uint) (string, error) { +func NewInstanceJWTToken(instance params.Instance, secret, entity string, poolType params.GithubEntityType, ttlMinutes uint) (string, error) { // Token expiration is equal to the bootstrap timeout set on the pool plus the polling // interval garm uses to check for timed out runners. Runners that have not sent their info // by the end of this interval are most likely failed and will be reaped by garm anyway. diff --git a/database/common/common.go b/database/common/common.go index f41a3559..8ca57ac2 100644 --- a/database/common/common.go +++ b/database/common/common.go @@ -87,7 +87,7 @@ type PoolStore interface { PoolInstanceCount(ctx context.Context, poolID string) (int64, error) GetPoolInstanceByName(ctx context.Context, poolID string, instanceName string) (params.Instance, error) - FindPoolsMatchingAllTags(ctx context.Context, entityType params.PoolType, entityID string, tags []string) ([]params.Pool, error) + FindPoolsMatchingAllTags(ctx context.Context, entityType params.GithubEntityType, entityID string, tags []string) ([]params.Pool, error) } type UserStore interface { @@ -117,7 +117,7 @@ type InstanceStore interface { type JobsStore interface { CreateOrUpdateJob(ctx context.Context, job params.Job) (params.Job, error) - ListEntityJobsByStatus(ctx context.Context, entityType params.PoolType, entityID string, status params.JobStatus) ([]params.Job, error) + ListEntityJobsByStatus(ctx context.Context, entityType params.GithubEntityType, entityID string, status params.JobStatus) ([]params.Job, error) ListJobsByStatus(ctx context.Context, status params.JobStatus) ([]params.Job, error) ListAllJobs(ctx context.Context) ([]params.Job, error) diff --git a/database/common/mocks/Store.go b/database/common/mocks/Store.go index f20b3830..81e47799 100644 --- a/database/common/mocks/Store.go +++ b/database/common/mocks/Store.go @@ -567,7 +567,7 @@ func (_m *Store) FindOrganizationPoolByTags(ctx context.Context, orgID string, t } // FindPoolsMatchingAllTags provides a mock function with given fields: ctx, entityType, entityID, tags -func (_m *Store) FindPoolsMatchingAllTags(ctx context.Context, entityType params.PoolType, entityID string, tags []string) ([]params.Pool, error) { +func (_m *Store) FindPoolsMatchingAllTags(ctx context.Context, entityType params.GithubEntityType, entityID string, tags []string) ([]params.Pool, error) { ret := _m.Called(ctx, entityType, entityID, tags) if len(ret) == 0 { @@ -576,10 +576,10 @@ func (_m *Store) FindPoolsMatchingAllTags(ctx context.Context, entityType params var r0 []params.Pool var r1 error - if rf, ok := ret.Get(0).(func(context.Context, params.PoolType, string, []string) ([]params.Pool, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntityType, string, []string) ([]params.Pool, error)); ok { return rf(ctx, entityType, entityID, tags) } - if rf, ok := ret.Get(0).(func(context.Context, params.PoolType, string, []string) []params.Pool); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntityType, string, []string) []params.Pool); ok { r0 = rf(ctx, entityType, entityID, tags) } else { if ret.Get(0) != nil { @@ -587,7 +587,7 @@ func (_m *Store) FindPoolsMatchingAllTags(ctx context.Context, entityType params } } - if rf, ok := ret.Get(1).(func(context.Context, params.PoolType, string, []string) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, params.GithubEntityType, string, []string) error); ok { r1 = rf(ctx, entityType, entityID, tags) } else { r1 = ret.Error(1) @@ -1271,7 +1271,7 @@ func (_m *Store) ListEnterprises(ctx context.Context) ([]params.Enterprise, erro } // ListEntityJobsByStatus provides a mock function with given fields: ctx, entityType, entityID, status -func (_m *Store) ListEntityJobsByStatus(ctx context.Context, entityType params.PoolType, entityID string, status params.JobStatus) ([]params.Job, error) { +func (_m *Store) ListEntityJobsByStatus(ctx context.Context, entityType params.GithubEntityType, entityID string, status params.JobStatus) ([]params.Job, error) { ret := _m.Called(ctx, entityType, entityID, status) if len(ret) == 0 { @@ -1280,10 +1280,10 @@ func (_m *Store) ListEntityJobsByStatus(ctx context.Context, entityType params.P var r0 []params.Job var r1 error - if rf, ok := ret.Get(0).(func(context.Context, params.PoolType, string, params.JobStatus) ([]params.Job, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntityType, string, params.JobStatus) ([]params.Job, error)); ok { return rf(ctx, entityType, entityID, status) } - if rf, ok := ret.Get(0).(func(context.Context, params.PoolType, string, params.JobStatus) []params.Job); ok { + if rf, ok := ret.Get(0).(func(context.Context, params.GithubEntityType, string, params.JobStatus) []params.Job); ok { r0 = rf(ctx, entityType, entityID, status) } else { if ret.Get(0) != nil { @@ -1291,7 +1291,7 @@ func (_m *Store) ListEntityJobsByStatus(ctx context.Context, entityType params.P } } - if rf, ok := ret.Get(1).(func(context.Context, params.PoolType, string, params.JobStatus) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, params.GithubEntityType, string, params.JobStatus) error); ok { r1 = rf(ctx, entityType, entityID, status) } else { r1 = ret.Error(1) diff --git a/database/sql/enterprise.go b/database/sql/enterprise.go index 274201db..f83dab8c 100644 --- a/database/sql/enterprise.go +++ b/database/sql/enterprise.go @@ -202,7 +202,7 @@ func (s *sqlDatabase) CreateEnterprisePool(ctx context.Context, enterpriseID str } func (s *sqlDatabase) GetEnterprisePool(ctx context.Context, enterpriseID, poolID string) (params.Pool, error) { - pool, err := s.getEntityPool(ctx, params.EnterprisePool, enterpriseID, poolID, "Tags", "Instances") + pool, err := s.getEntityPool(ctx, params.GithubEntityTypeEnterprise, enterpriseID, poolID, "Tags", "Instances") if err != nil { return params.Pool{}, errors.Wrap(err, "fetching pool") } @@ -210,7 +210,7 @@ func (s *sqlDatabase) GetEnterprisePool(ctx context.Context, enterpriseID, poolI } func (s *sqlDatabase) DeleteEnterprisePool(ctx context.Context, enterpriseID, poolID string) error { - pool, err := s.getEntityPool(ctx, params.EnterprisePool, enterpriseID, poolID) + pool, err := s.getEntityPool(ctx, params.GithubEntityTypeEnterprise, enterpriseID, poolID) if err != nil { return errors.Wrap(err, "looking up enterprise pool") } @@ -222,7 +222,7 @@ func (s *sqlDatabase) DeleteEnterprisePool(ctx context.Context, enterpriseID, po } func (s *sqlDatabase) UpdateEnterprisePool(ctx context.Context, enterpriseID, poolID string, param params.UpdatePoolParams) (params.Pool, error) { - pool, err := s.getEntityPool(ctx, params.EnterprisePool, enterpriseID, poolID, "Tags", "Instances", "Enterprise", "Organization", "Repository") + pool, err := s.getEntityPool(ctx, params.GithubEntityTypeEnterprise, enterpriseID, poolID, "Tags", "Instances", "Enterprise", "Organization", "Repository") if err != nil { return params.Pool{}, errors.Wrap(err, "fetching pool") } @@ -231,7 +231,7 @@ func (s *sqlDatabase) UpdateEnterprisePool(ctx context.Context, enterpriseID, po } func (s *sqlDatabase) FindEnterprisePoolByTags(_ context.Context, enterpriseID string, tags []string) (params.Pool, error) { - pool, err := s.findPoolByTags(enterpriseID, params.EnterprisePool, tags) + pool, err := s.findPoolByTags(enterpriseID, params.GithubEntityTypeEnterprise, tags) if err != nil { return params.Pool{}, errors.Wrap(err, "fetching pool") } @@ -239,7 +239,7 @@ func (s *sqlDatabase) FindEnterprisePoolByTags(_ context.Context, enterpriseID s } func (s *sqlDatabase) ListEnterprisePools(ctx context.Context, enterpriseID string) ([]params.Pool, error) { - pools, err := s.listEntityPools(ctx, params.EnterprisePool, enterpriseID, "Tags", "Instances", "Enterprise") + pools, err := s.listEntityPools(ctx, params.GithubEntityTypeEnterprise, enterpriseID, "Tags", "Instances", "Enterprise") if err != nil { return nil, errors.Wrap(err, "fetching pools") } @@ -256,7 +256,7 @@ func (s *sqlDatabase) ListEnterprisePools(ctx context.Context, enterpriseID stri } func (s *sqlDatabase) ListEnterpriseInstances(ctx context.Context, enterpriseID string) ([]params.Instance, error) { - pools, err := s.listEntityPools(ctx, params.EnterprisePool, enterpriseID, "Instances", "Tags", "Instances.Job") + pools, err := s.listEntityPools(ctx, params.GithubEntityTypeEnterprise, enterpriseID, "Instances", "Tags", "Instances.Job") if err != nil { return nil, errors.Wrap(err, "fetching enterprise") } diff --git a/database/sql/jobs.go b/database/sql/jobs.go index 0ab77e90..0201428f 100644 --- a/database/sql/jobs.go +++ b/database/sql/jobs.go @@ -271,7 +271,7 @@ func (s *sqlDatabase) ListJobsByStatus(_ context.Context, status params.JobStatu } // ListEntityJobsByStatus lists all jobs for a given entity type and id. -func (s *sqlDatabase) ListEntityJobsByStatus(_ context.Context, entityType params.PoolType, entityID string, status params.JobStatus) ([]params.Job, error) { +func (s *sqlDatabase) ListEntityJobsByStatus(_ context.Context, entityType params.GithubEntityType, entityID string, status params.JobStatus) ([]params.Job, error) { u, err := uuid.Parse(entityID) if err != nil { return nil, err @@ -281,11 +281,11 @@ func (s *sqlDatabase) ListEntityJobsByStatus(_ context.Context, entityType param query := s.conn.Model(&WorkflowJob{}).Preload("Instance").Where("status = ?", status) switch entityType { - case params.OrganizationPool: + case params.GithubEntityTypeOrganization: query = query.Where("org_id = ?", u) - case params.RepositoryPool: + case params.GithubEntityTypeRepository: query = query.Where("repo_id = ?", u) - case params.EnterprisePool: + case params.GithubEntityTypeEnterprise: query = query.Where("enterprise_id = ?", u) } diff --git a/database/sql/organizations.go b/database/sql/organizations.go index 2dee60b4..4d246065 100644 --- a/database/sql/organizations.go +++ b/database/sql/organizations.go @@ -219,7 +219,7 @@ func (s *sqlDatabase) CreateOrganizationPool(ctx context.Context, orgID string, } func (s *sqlDatabase) ListOrgPools(ctx context.Context, orgID string) ([]params.Pool, error) { - pools, err := s.listEntityPools(ctx, params.OrganizationPool, orgID, "Tags", "Instances", "Organization") + pools, err := s.listEntityPools(ctx, params.GithubEntityTypeOrganization, orgID, "Tags", "Instances", "Organization") if err != nil { return nil, errors.Wrap(err, "fetching pools") } @@ -236,7 +236,7 @@ func (s *sqlDatabase) ListOrgPools(ctx context.Context, orgID string) ([]params. } func (s *sqlDatabase) GetOrganizationPool(ctx context.Context, orgID, poolID string) (params.Pool, error) { - pool, err := s.getEntityPool(ctx, params.OrganizationPool, orgID, poolID, "Tags", "Instances") + pool, err := s.getEntityPool(ctx, params.GithubEntityTypeOrganization, orgID, poolID, "Tags", "Instances") if err != nil { return params.Pool{}, errors.Wrap(err, "fetching pool") } @@ -244,7 +244,7 @@ func (s *sqlDatabase) GetOrganizationPool(ctx context.Context, orgID, poolID str } func (s *sqlDatabase) DeleteOrganizationPool(ctx context.Context, orgID, poolID string) error { - pool, err := s.getEntityPool(ctx, params.OrganizationPool, orgID, poolID) + pool, err := s.getEntityPool(ctx, params.GithubEntityTypeOrganization, orgID, poolID) if err != nil { return errors.Wrap(err, "looking up org pool") } @@ -256,7 +256,7 @@ func (s *sqlDatabase) DeleteOrganizationPool(ctx context.Context, orgID, poolID } func (s *sqlDatabase) FindOrganizationPoolByTags(_ context.Context, orgID string, tags []string) (params.Pool, error) { - pool, err := s.findPoolByTags(orgID, params.OrganizationPool, tags) + pool, err := s.findPoolByTags(orgID, params.GithubEntityTypeOrganization, tags) if err != nil { return params.Pool{}, errors.Wrap(err, "fetching pool") } @@ -264,7 +264,7 @@ func (s *sqlDatabase) FindOrganizationPoolByTags(_ context.Context, orgID string } func (s *sqlDatabase) ListOrgInstances(ctx context.Context, orgID string) ([]params.Instance, error) { - pools, err := s.listEntityPools(ctx, params.OrganizationPool, orgID, "Tags", "Instances", "Instances.Job") + pools, err := s.listEntityPools(ctx, params.GithubEntityTypeOrganization, orgID, "Tags", "Instances", "Instances.Job") if err != nil { return nil, errors.Wrap(err, "fetching org") } @@ -282,7 +282,7 @@ func (s *sqlDatabase) ListOrgInstances(ctx context.Context, orgID string) ([]par } func (s *sqlDatabase) UpdateOrganizationPool(ctx context.Context, orgID, poolID string, param params.UpdatePoolParams) (params.Pool, error) { - pool, err := s.getEntityPool(ctx, params.OrganizationPool, orgID, poolID, "Tags", "Instances", "Enterprise", "Organization", "Repository") + pool, err := s.getEntityPool(ctx, params.GithubEntityTypeOrganization, orgID, poolID, "Tags", "Instances", "Enterprise", "Organization", "Repository") if err != nil { return params.Pool{}, errors.Wrap(err, "fetching pool") } diff --git a/database/sql/pools.go b/database/sql/pools.go index f38eb7d0..65aca8ba 100644 --- a/database/sql/pools.go +++ b/database/sql/pools.go @@ -78,7 +78,7 @@ func (s *sqlDatabase) DeletePoolByID(ctx context.Context, poolID string) error { return nil } -func (s *sqlDatabase) getEntityPool(_ context.Context, entityType params.PoolType, entityID, poolID string, preload ...string) (Pool, error) { +func (s *sqlDatabase) getEntityPool(_ context.Context, entityType params.GithubEntityType, entityID, poolID string, preload ...string) (Pool, error) { if entityID == "" { return Pool{}, errors.Wrap(runnerErrors.ErrBadRequest, "missing entity id") } @@ -97,11 +97,11 @@ func (s *sqlDatabase) getEntityPool(_ context.Context, entityType params.PoolTyp var fieldName string switch entityType { - case params.RepositoryPool: + case params.GithubEntityTypeRepository: fieldName = entityTypeRepoName - case params.OrganizationPool: + case params.GithubEntityTypeOrganization: fieldName = entityTypeOrgName - case params.EnterprisePool: + case params.GithubEntityTypeEnterprise: fieldName = entityTypeEnterpriseName default: return Pool{}, fmt.Errorf("invalid entityType: %v", entityType) @@ -122,7 +122,7 @@ func (s *sqlDatabase) getEntityPool(_ context.Context, entityType params.PoolTyp return pool, nil } -func (s *sqlDatabase) listEntityPools(_ context.Context, entityType params.PoolType, entityID string, preload ...string) ([]Pool, error) { +func (s *sqlDatabase) listEntityPools(_ context.Context, entityType params.GithubEntityType, entityID string, preload ...string) ([]Pool, error) { if _, err := uuid.Parse(entityID); err != nil { return nil, errors.Wrap(runnerErrors.ErrBadRequest, "parsing id") } @@ -136,11 +136,11 @@ func (s *sqlDatabase) listEntityPools(_ context.Context, entityType params.PoolT var fieldName string switch entityType { - case params.RepositoryPool: + case params.GithubEntityTypeRepository: fieldName = entityTypeRepoName - case params.OrganizationPool: + case params.GithubEntityTypeOrganization: fieldName = entityTypeOrgName - case params.EnterprisePool: + case params.GithubEntityTypeEnterprise: fieldName = entityTypeEnterpriseName default: return nil, fmt.Errorf("invalid entityType: %v", entityType) @@ -162,7 +162,7 @@ func (s *sqlDatabase) listEntityPools(_ context.Context, entityType params.PoolT return pools, nil } -func (s *sqlDatabase) findPoolByTags(id string, poolType params.PoolType, tags []string) ([]params.Pool, error) { +func (s *sqlDatabase) findPoolByTags(id string, poolType params.GithubEntityType, tags []string) ([]params.Pool, error) { if len(tags) == 0 { return nil, runnerErrors.NewBadRequestError("missing tags") } @@ -173,11 +173,11 @@ func (s *sqlDatabase) findPoolByTags(id string, poolType params.PoolType, tags [ var fieldName string switch poolType { - case params.RepositoryPool: + case params.GithubEntityTypeRepository: fieldName = entityTypeRepoName - case params.OrganizationPool: + case params.GithubEntityTypeOrganization: fieldName = entityTypeOrgName - case params.EnterprisePool: + case params.GithubEntityTypeEnterprise: fieldName = entityTypeEnterpriseName default: return nil, fmt.Errorf("invalid poolType: %v", poolType) @@ -216,7 +216,7 @@ func (s *sqlDatabase) findPoolByTags(id string, poolType params.PoolType, tags [ return ret, nil } -func (s *sqlDatabase) FindPoolsMatchingAllTags(_ context.Context, entityType params.PoolType, entityID string, tags []string) ([]params.Pool, error) { +func (s *sqlDatabase) FindPoolsMatchingAllTags(_ context.Context, entityType params.GithubEntityType, entityID string, tags []string) ([]params.Pool, error) { if len(tags) == 0 { return nil, runnerErrors.NewBadRequestError("missing tags") } diff --git a/database/sql/repositories.go b/database/sql/repositories.go index 8131f1f3..f7671840 100644 --- a/database/sql/repositories.go +++ b/database/sql/repositories.go @@ -219,7 +219,7 @@ func (s *sqlDatabase) CreateRepositoryPool(ctx context.Context, repoID string, p } func (s *sqlDatabase) ListRepoPools(ctx context.Context, repoID string) ([]params.Pool, error) { - pools, err := s.listEntityPools(ctx, params.RepositoryPool, repoID, "Tags", "Instances", "Repository") + pools, err := s.listEntityPools(ctx, params.GithubEntityTypeRepository, repoID, "Tags", "Instances", "Repository") if err != nil { return nil, errors.Wrap(err, "fetching pools") } @@ -236,7 +236,7 @@ func (s *sqlDatabase) ListRepoPools(ctx context.Context, repoID string) ([]param } func (s *sqlDatabase) GetRepositoryPool(ctx context.Context, repoID, poolID string) (params.Pool, error) { - pool, err := s.getEntityPool(ctx, params.RepositoryPool, repoID, poolID, "Tags", "Instances") + pool, err := s.getEntityPool(ctx, params.GithubEntityTypeRepository, repoID, poolID, "Tags", "Instances") if err != nil { return params.Pool{}, errors.Wrap(err, "fetching pool") } @@ -244,7 +244,7 @@ func (s *sqlDatabase) GetRepositoryPool(ctx context.Context, repoID, poolID stri } func (s *sqlDatabase) DeleteRepositoryPool(ctx context.Context, repoID, poolID string) error { - pool, err := s.getEntityPool(ctx, params.RepositoryPool, repoID, poolID) + pool, err := s.getEntityPool(ctx, params.GithubEntityTypeRepository, repoID, poolID) if err != nil { return errors.Wrap(err, "looking up repo pool") } @@ -256,7 +256,7 @@ func (s *sqlDatabase) DeleteRepositoryPool(ctx context.Context, repoID, poolID s } func (s *sqlDatabase) FindRepositoryPoolByTags(_ context.Context, repoID string, tags []string) (params.Pool, error) { - pool, err := s.findPoolByTags(repoID, params.RepositoryPool, tags) + pool, err := s.findPoolByTags(repoID, params.GithubEntityTypeRepository, tags) if err != nil { return params.Pool{}, errors.Wrap(err, "fetching pool") } @@ -264,7 +264,7 @@ func (s *sqlDatabase) FindRepositoryPoolByTags(_ context.Context, repoID string, } func (s *sqlDatabase) ListRepoInstances(ctx context.Context, repoID string) ([]params.Instance, error) { - pools, err := s.listEntityPools(ctx, params.RepositoryPool, repoID, "Tags", "Instances", "Instances.Job") + pools, err := s.listEntityPools(ctx, params.GithubEntityTypeRepository, repoID, "Tags", "Instances", "Instances.Job") if err != nil { return nil, errors.Wrap(err, "fetching repo") } @@ -283,7 +283,7 @@ func (s *sqlDatabase) ListRepoInstances(ctx context.Context, repoID string) ([]p } func (s *sqlDatabase) UpdateRepositoryPool(ctx context.Context, repoID, poolID string, param params.UpdatePoolParams) (params.Pool, error) { - pool, err := s.getEntityPool(ctx, params.RepositoryPool, repoID, poolID, "Tags", "Instances", "Enterprise", "Organization", "Repository") + pool, err := s.getEntityPool(ctx, params.GithubEntityTypeRepository, repoID, poolID, "Tags", "Instances", "Enterprise", "Organization", "Repository") if err != nil { return params.Pool{}, errors.Wrap(err, "fetching pool") } diff --git a/params/params.go b/params/params.go index b4157206..2b87a4c5 100644 --- a/params/params.go +++ b/params/params.go @@ -31,7 +31,7 @@ import ( ) type ( - PoolType string + GithubEntityType string EventType string EventLevel string ProviderType string @@ -81,9 +81,15 @@ const ( ) const ( - RepositoryPool PoolType = "repository" - OrganizationPool PoolType = "organization" - EnterprisePool PoolType = "enterprise" + GithubEntityTypeRepository GithubEntityType = "repository" + GithubEntityTypeOrganization GithubEntityType = "organization" + GithubEntityTypeEnterprise GithubEntityType = "enterprise" +) + +const ( + MetricsLabelEnterpriseScope = "Enterprise" + MetricsLabelRepositoryScope = "Repository" + MetricsLabelOrganizationScope = "Organization" ) const ( @@ -113,6 +119,10 @@ const ( GithubAuthTypeApp GithubAuthType = "app" ) +func (e GithubEntityType) String() string { + return string(e) +} + type StatusMessage struct { CreatedAt time.Time `json:"created_at"` Message string `json:"message"` @@ -318,14 +328,14 @@ func (p *Pool) RunnerTimeout() uint { return p.RunnerBootstrapTimeout } -func (p *Pool) PoolType() PoolType { +func (p *Pool) PoolType() GithubEntityType { switch { case p.RepoID != "": - return RepositoryPool + return GithubEntityTypeRepository case p.OrgID != "": - return OrganizationPool + return GithubEntityTypeOrganization case p.EnterpriseID != "": - return EnterprisePool + return GithubEntityTypeEnterprise } return "" } @@ -631,3 +641,34 @@ type UpdateSystemInfoParams struct { OSVersion string `json:"os_version,omitempty"` AgentID *int64 `json:"agent_id,omitempty"` } + +type GithubEntity struct { + Owner string `json:"owner"` + Name string `json:"name"` + ID string `json:"id"` + EntityType GithubEntityType `json:"entity_type"` + + WebhookSecret string `json:"-"` +} + +func (g GithubEntity) LabelScope() string { + switch g.EntityType { + case GithubEntityTypeRepository: + return MetricsLabelRepositoryScope + case GithubEntityTypeOrganization: + return MetricsLabelOrganizationScope + case GithubEntityTypeEnterprise: + return MetricsLabelEnterpriseScope + } + return "" +} + +func (g GithubEntity) String() string { + switch g.EntityType { + case GithubEntityTypeRepository: + return fmt.Sprintf("%s/%s", g.Owner, g.Name) + case GithubEntityTypeOrganization, GithubEntityTypeEnterprise: + return g.Owner + } + return "" +} diff --git a/runner/common/mocks/GithubClient.go b/runner/common/mocks/GithubClient.go index 4ba2c2fa..c59c631b 100644 --- a/runner/common/mocks/GithubClient.go +++ b/runner/common/mocks/GithubClient.go @@ -7,6 +7,8 @@ import ( github "github.com/google/go-github/v57/github" mock "github.com/stretchr/testify/mock" + + params "github.com/cloudbase/garm/params" ) // GithubClient is an autogenerated mock type for the GithubClient type @@ -14,155 +16,68 @@ type GithubClient struct { mock.Mock } -// CreateOrgHook provides a mock function with given fields: ctx, org, hook -func (_m *GithubClient) CreateOrgHook(ctx context.Context, org string, hook *github.Hook) (*github.Hook, *github.Response, error) { - ret := _m.Called(ctx, org, hook) +// CreateEntityHook provides a mock function with given fields: ctx, hook +func (_m *GithubClient) CreateEntityHook(ctx context.Context, hook *github.Hook) (*github.Hook, error) { + ret := _m.Called(ctx, hook) if len(ret) == 0 { - panic("no return value specified for CreateOrgHook") + panic("no return value specified for CreateEntityHook") } var r0 *github.Hook - var r1 *github.Response - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, *github.Hook) (*github.Hook, *github.Response, error)); ok { - return rf(ctx, org, hook) + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *github.Hook) (*github.Hook, error)); ok { + return rf(ctx, hook) } - if rf, ok := ret.Get(0).(func(context.Context, string, *github.Hook) *github.Hook); ok { - r0 = rf(ctx, org, hook) + if rf, ok := ret.Get(0).(func(context.Context, *github.Hook) *github.Hook); ok { + r0 = rf(ctx, hook) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*github.Hook) } } - if rf, ok := ret.Get(1).(func(context.Context, string, *github.Hook) *github.Response); ok { - r1 = rf(ctx, org, hook) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*github.Response) - } - } - - if rf, ok := ret.Get(2).(func(context.Context, string, *github.Hook) error); ok { - r2 = rf(ctx, org, hook) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// CreateOrganizationRegistrationToken provides a mock function with given fields: ctx, owner -func (_m *GithubClient) CreateOrganizationRegistrationToken(ctx context.Context, owner string) (*github.RegistrationToken, *github.Response, error) { - ret := _m.Called(ctx, owner) - - if len(ret) == 0 { - panic("no return value specified for CreateOrganizationRegistrationToken") - } - - var r0 *github.RegistrationToken - var r1 *github.Response - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*github.RegistrationToken, *github.Response, error)); ok { - return rf(ctx, owner) - } - if rf, ok := ret.Get(0).(func(context.Context, string) *github.RegistrationToken); ok { - r0 = rf(ctx, owner) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*github.RegistrationToken) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) *github.Response); ok { - r1 = rf(ctx, owner) + if rf, ok := ret.Get(1).(func(context.Context, *github.Hook) error); ok { + r1 = rf(ctx, hook) } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*github.Response) - } - } - - if rf, ok := ret.Get(2).(func(context.Context, string) error); ok { - r2 = rf(ctx, owner) - } else { - r2 = ret.Error(2) + r1 = ret.Error(1) } - return r0, r1, r2 + return r0, r1 } -// CreateRegistrationToken provides a mock function with given fields: ctx, owner, repo -func (_m *GithubClient) CreateRegistrationToken(ctx context.Context, owner string, repo string) (*github.RegistrationToken, *github.Response, error) { - ret := _m.Called(ctx, owner, repo) +// CreateEntityRegistrationToken provides a mock function with given fields: ctx +func (_m *GithubClient) CreateEntityRegistrationToken(ctx context.Context) (*github.RegistrationToken, *github.Response, error) { + ret := _m.Called(ctx) if len(ret) == 0 { - panic("no return value specified for CreateRegistrationToken") + panic("no return value specified for CreateEntityRegistrationToken") } var r0 *github.RegistrationToken var r1 *github.Response var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (*github.RegistrationToken, *github.Response, error)); ok { - return rf(ctx, owner, repo) + if rf, ok := ret.Get(0).(func(context.Context) (*github.RegistrationToken, *github.Response, error)); ok { + return rf(ctx) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) *github.RegistrationToken); ok { - r0 = rf(ctx, owner, repo) + if rf, ok := ret.Get(0).(func(context.Context) *github.RegistrationToken); ok { + r0 = rf(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*github.RegistrationToken) } } - if rf, ok := ret.Get(1).(func(context.Context, string, string) *github.Response); ok { - r1 = rf(ctx, owner, repo) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*github.Response) - } - } - - if rf, ok := ret.Get(2).(func(context.Context, string, string) error); ok { - r2 = rf(ctx, owner, repo) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// CreateRepoHook provides a mock function with given fields: ctx, owner, repo, hook -func (_m *GithubClient) CreateRepoHook(ctx context.Context, owner string, repo string, hook *github.Hook) (*github.Hook, *github.Response, error) { - ret := _m.Called(ctx, owner, repo, hook) - - if len(ret) == 0 { - panic("no return value specified for CreateRepoHook") - } - - var r0 *github.Hook - var r1 *github.Response - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.Hook) (*github.Hook, *github.Response, error)); ok { - return rf(ctx, owner, repo, hook) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.Hook) *github.Hook); ok { - r0 = rf(ctx, owner, repo, hook) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*github.Hook) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, *github.Hook) *github.Response); ok { - r1 = rf(ctx, owner, repo, hook) + if rf, ok := ret.Get(1).(func(context.Context) *github.Response); ok { + r1 = rf(ctx) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(*github.Response) } } - if rf, ok := ret.Get(2).(func(context.Context, string, string, *github.Hook) error); ok { - r2 = rf(ctx, owner, repo, hook) + if rf, ok := ret.Get(2).(func(context.Context) error); ok { + r2 = rf(ctx) } else { r2 = ret.Error(2) } @@ -170,29 +85,29 @@ func (_m *GithubClient) CreateRepoHook(ctx context.Context, owner string, repo s return r0, r1, r2 } -// DeleteOrgHook provides a mock function with given fields: ctx, org, id -func (_m *GithubClient) DeleteOrgHook(ctx context.Context, org string, id int64) (*github.Response, error) { - ret := _m.Called(ctx, org, id) +// DeleteEntityHook provides a mock function with given fields: ctx, id +func (_m *GithubClient) DeleteEntityHook(ctx context.Context, id int64) (*github.Response, error) { + ret := _m.Called(ctx, id) if len(ret) == 0 { - panic("no return value specified for DeleteOrgHook") + panic("no return value specified for DeleteEntityHook") } var r0 *github.Response var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, int64) (*github.Response, error)); ok { - return rf(ctx, org, id) + if rf, ok := ret.Get(0).(func(context.Context, int64) (*github.Response, error)); ok { + return rf(ctx, id) } - if rf, ok := ret.Get(0).(func(context.Context, string, int64) *github.Response); ok { - r0 = rf(ctx, org, id) + if rf, ok := ret.Get(0).(func(context.Context, int64) *github.Response); ok { + r0 = rf(ctx, id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*github.Response) } } - if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { - r1 = rf(ctx, org, id) + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) } else { r1 = ret.Error(1) } @@ -200,29 +115,29 @@ func (_m *GithubClient) DeleteOrgHook(ctx context.Context, org string, id int64) return r0, r1 } -// DeleteRepoHook provides a mock function with given fields: ctx, owner, repo, id -func (_m *GithubClient) DeleteRepoHook(ctx context.Context, owner string, repo string, id int64) (*github.Response, error) { - ret := _m.Called(ctx, owner, repo, id) +// GetEntityHook provides a mock function with given fields: ctx, id +func (_m *GithubClient) GetEntityHook(ctx context.Context, id int64) (*github.Hook, error) { + ret := _m.Called(ctx, id) if len(ret) == 0 { - panic("no return value specified for DeleteRepoHook") + panic("no return value specified for GetEntityHook") } - var r0 *github.Response + var r0 *github.Hook var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) (*github.Response, error)); ok { - return rf(ctx, owner, repo, id) + if rf, ok := ret.Get(0).(func(context.Context, int64) (*github.Hook, error)); ok { + return rf(ctx, id) } - if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) *github.Response); ok { - r0 = rf(ctx, owner, repo, id) + if rf, ok := ret.Get(0).(func(context.Context, int64) *github.Hook); ok { + r0 = rf(ctx, id) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*github.Response) + r0 = ret.Get(0).(*github.Hook) } } - if rf, ok := ret.Get(1).(func(context.Context, string, string, int64) error); ok { - r1 = rf(ctx, owner, repo, id) + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) } else { r1 = ret.Error(1) } @@ -230,155 +145,38 @@ func (_m *GithubClient) DeleteRepoHook(ctx context.Context, owner string, repo s return r0, r1 } -// GenerateOrgJITConfig provides a mock function with given fields: ctx, owner, request -func (_m *GithubClient) GenerateOrgJITConfig(ctx context.Context, owner string, request *github.GenerateJITConfigRequest) (*github.JITRunnerConfig, *github.Response, error) { - ret := _m.Called(ctx, owner, request) +// GetEntityJITConfig provides a mock function with given fields: ctx, instance, pool, labels +func (_m *GithubClient) GetEntityJITConfig(ctx context.Context, instance string, pool params.Pool, labels []string) (map[string]string, *github.Runner, error) { + ret := _m.Called(ctx, instance, pool, labels) if len(ret) == 0 { - panic("no return value specified for GenerateOrgJITConfig") + panic("no return value specified for GetEntityJITConfig") } - var r0 *github.JITRunnerConfig - var r1 *github.Response + var r0 map[string]string + var r1 *github.Runner var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, *github.GenerateJITConfigRequest) (*github.JITRunnerConfig, *github.Response, error)); ok { - return rf(ctx, owner, request) + if rf, ok := ret.Get(0).(func(context.Context, string, params.Pool, []string) (map[string]string, *github.Runner, error)); ok { + return rf(ctx, instance, pool, labels) } - if rf, ok := ret.Get(0).(func(context.Context, string, *github.GenerateJITConfigRequest) *github.JITRunnerConfig); ok { - r0 = rf(ctx, owner, request) + if rf, ok := ret.Get(0).(func(context.Context, string, params.Pool, []string) map[string]string); ok { + r0 = rf(ctx, instance, pool, labels) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*github.JITRunnerConfig) + r0 = ret.Get(0).(map[string]string) } } - if rf, ok := ret.Get(1).(func(context.Context, string, *github.GenerateJITConfigRequest) *github.Response); ok { - r1 = rf(ctx, owner, request) + if rf, ok := ret.Get(1).(func(context.Context, string, params.Pool, []string) *github.Runner); ok { + r1 = rf(ctx, instance, pool, labels) } else { if ret.Get(1) != nil { - r1 = ret.Get(1).(*github.Response) + r1 = ret.Get(1).(*github.Runner) } } - if rf, ok := ret.Get(2).(func(context.Context, string, *github.GenerateJITConfigRequest) error); ok { - r2 = rf(ctx, owner, request) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// GenerateRepoJITConfig provides a mock function with given fields: ctx, owner, repo, request -func (_m *GithubClient) GenerateRepoJITConfig(ctx context.Context, owner string, repo string, request *github.GenerateJITConfigRequest) (*github.JITRunnerConfig, *github.Response, error) { - ret := _m.Called(ctx, owner, repo, request) - - if len(ret) == 0 { - panic("no return value specified for GenerateRepoJITConfig") - } - - var r0 *github.JITRunnerConfig - var r1 *github.Response - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.GenerateJITConfigRequest) (*github.JITRunnerConfig, *github.Response, error)); ok { - return rf(ctx, owner, repo, request) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.GenerateJITConfigRequest) *github.JITRunnerConfig); ok { - r0 = rf(ctx, owner, repo, request) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*github.JITRunnerConfig) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, *github.GenerateJITConfigRequest) *github.Response); ok { - r1 = rf(ctx, owner, repo, request) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*github.Response) - } - } - - if rf, ok := ret.Get(2).(func(context.Context, string, string, *github.GenerateJITConfigRequest) error); ok { - r2 = rf(ctx, owner, repo, request) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// GetOrgHook provides a mock function with given fields: ctx, org, id -func (_m *GithubClient) GetOrgHook(ctx context.Context, org string, id int64) (*github.Hook, *github.Response, error) { - ret := _m.Called(ctx, org, id) - - if len(ret) == 0 { - panic("no return value specified for GetOrgHook") - } - - var r0 *github.Hook - var r1 *github.Response - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, int64) (*github.Hook, *github.Response, error)); ok { - return rf(ctx, org, id) - } - if rf, ok := ret.Get(0).(func(context.Context, string, int64) *github.Hook); ok { - r0 = rf(ctx, org, id) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*github.Hook) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, int64) *github.Response); ok { - r1 = rf(ctx, org, id) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*github.Response) - } - } - - if rf, ok := ret.Get(2).(func(context.Context, string, int64) error); ok { - r2 = rf(ctx, org, id) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// GetRepoHook provides a mock function with given fields: ctx, owner, repo, id -func (_m *GithubClient) GetRepoHook(ctx context.Context, owner string, repo string, id int64) (*github.Hook, *github.Response, error) { - ret := _m.Called(ctx, owner, repo, id) - - if len(ret) == 0 { - panic("no return value specified for GetRepoHook") - } - - var r0 *github.Hook - var r1 *github.Response - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) (*github.Hook, *github.Response, error)); ok { - return rf(ctx, owner, repo, id) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) *github.Hook); ok { - r0 = rf(ctx, owner, repo, id) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*github.Hook) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, int64) *github.Response); ok { - r1 = rf(ctx, owner, repo, id) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*github.Response) - } - } - - if rf, ok := ret.Get(2).(func(context.Context, string, string, int64) error); ok { - r2 = rf(ctx, owner, repo, id) + if rf, ok := ret.Get(2).(func(context.Context, string, params.Pool, []string) error); ok { + r2 = rf(ctx, instance, pool, labels) } else { r2 = ret.Error(2) } @@ -425,194 +223,38 @@ func (_m *GithubClient) GetWorkflowJobByID(ctx context.Context, owner string, re return r0, r1, r2 } -// ListOrgHooks provides a mock function with given fields: ctx, org, opts -func (_m *GithubClient) ListOrgHooks(ctx context.Context, org string, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) { - ret := _m.Called(ctx, org, opts) - - if len(ret) == 0 { - panic("no return value specified for ListOrgHooks") - } - - var r0 []*github.Hook - var r1 *github.Response - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, *github.ListOptions) ([]*github.Hook, *github.Response, error)); ok { - return rf(ctx, org, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, string, *github.ListOptions) []*github.Hook); ok { - r0 = rf(ctx, org, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*github.Hook) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, *github.ListOptions) *github.Response); ok { - r1 = rf(ctx, org, opts) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*github.Response) - } - } - - if rf, ok := ret.Get(2).(func(context.Context, string, *github.ListOptions) error); ok { - r2 = rf(ctx, org, opts) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// ListOrganizationRunnerApplicationDownloads provides a mock function with given fields: ctx, owner -func (_m *GithubClient) ListOrganizationRunnerApplicationDownloads(ctx context.Context, owner string) ([]*github.RunnerApplicationDownload, *github.Response, error) { - ret := _m.Called(ctx, owner) - - if len(ret) == 0 { - panic("no return value specified for ListOrganizationRunnerApplicationDownloads") - } - - var r0 []*github.RunnerApplicationDownload - var r1 *github.Response - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string) ([]*github.RunnerApplicationDownload, *github.Response, error)); ok { - return rf(ctx, owner) - } - if rf, ok := ret.Get(0).(func(context.Context, string) []*github.RunnerApplicationDownload); ok { - r0 = rf(ctx, owner) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*github.RunnerApplicationDownload) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) *github.Response); ok { - r1 = rf(ctx, owner) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*github.Response) - } - } - - if rf, ok := ret.Get(2).(func(context.Context, string) error); ok { - r2 = rf(ctx, owner) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// ListOrganizationRunnerGroups provides a mock function with given fields: ctx, org, opts -func (_m *GithubClient) ListOrganizationRunnerGroups(ctx context.Context, org string, opts *github.ListOrgRunnerGroupOptions) (*github.RunnerGroups, *github.Response, error) { - ret := _m.Called(ctx, org, opts) - - if len(ret) == 0 { - panic("no return value specified for ListOrganizationRunnerGroups") - } - - var r0 *github.RunnerGroups - var r1 *github.Response - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, *github.ListOrgRunnerGroupOptions) (*github.RunnerGroups, *github.Response, error)); ok { - return rf(ctx, org, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, string, *github.ListOrgRunnerGroupOptions) *github.RunnerGroups); ok { - r0 = rf(ctx, org, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*github.RunnerGroups) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, *github.ListOrgRunnerGroupOptions) *github.Response); ok { - r1 = rf(ctx, org, opts) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*github.Response) - } - } - - if rf, ok := ret.Get(2).(func(context.Context, string, *github.ListOrgRunnerGroupOptions) error); ok { - r2 = rf(ctx, org, opts) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// ListOrganizationRunners provides a mock function with given fields: ctx, owner, opts -func (_m *GithubClient) ListOrganizationRunners(ctx context.Context, owner string, opts *github.ListOptions) (*github.Runners, *github.Response, error) { - ret := _m.Called(ctx, owner, opts) - - if len(ret) == 0 { - panic("no return value specified for ListOrganizationRunners") - } - - var r0 *github.Runners - var r1 *github.Response - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, *github.ListOptions) (*github.Runners, *github.Response, error)); ok { - return rf(ctx, owner, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, string, *github.ListOptions) *github.Runners); ok { - r0 = rf(ctx, owner, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*github.Runners) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, *github.ListOptions) *github.Response); ok { - r1 = rf(ctx, owner, opts) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*github.Response) - } - } - - if rf, ok := ret.Get(2).(func(context.Context, string, *github.ListOptions) error); ok { - r2 = rf(ctx, owner, opts) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// ListRepoHooks provides a mock function with given fields: ctx, owner, repo, opts -func (_m *GithubClient) ListRepoHooks(ctx context.Context, owner string, repo string, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) { - ret := _m.Called(ctx, owner, repo, opts) +// ListEntityHooks provides a mock function with given fields: ctx, opts +func (_m *GithubClient) ListEntityHooks(ctx context.Context, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) { + ret := _m.Called(ctx, opts) if len(ret) == 0 { - panic("no return value specified for ListRepoHooks") + panic("no return value specified for ListEntityHooks") } var r0 []*github.Hook var r1 *github.Response var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.ListOptions) ([]*github.Hook, *github.Response, error)); ok { - return rf(ctx, owner, repo, opts) + if rf, ok := ret.Get(0).(func(context.Context, *github.ListOptions) ([]*github.Hook, *github.Response, error)); ok { + return rf(ctx, opts) } - if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.ListOptions) []*github.Hook); ok { - r0 = rf(ctx, owner, repo, opts) + if rf, ok := ret.Get(0).(func(context.Context, *github.ListOptions) []*github.Hook); ok { + r0 = rf(ctx, opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*github.Hook) } } - if rf, ok := ret.Get(1).(func(context.Context, string, string, *github.ListOptions) *github.Response); ok { - r1 = rf(ctx, owner, repo, opts) + if rf, ok := ret.Get(1).(func(context.Context, *github.ListOptions) *github.Response); ok { + r1 = rf(ctx, opts) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(*github.Response) } } - if rf, ok := ret.Get(2).(func(context.Context, string, string, *github.ListOptions) error); ok { - r2 = rf(ctx, owner, repo, opts) + if rf, ok := ret.Get(2).(func(context.Context, *github.ListOptions) error); ok { + r2 = rf(ctx, opts) } else { r2 = ret.Error(2) } @@ -620,38 +262,38 @@ func (_m *GithubClient) ListRepoHooks(ctx context.Context, owner string, repo st return r0, r1, r2 } -// ListRunnerApplicationDownloads provides a mock function with given fields: ctx, owner, repo -func (_m *GithubClient) ListRunnerApplicationDownloads(ctx context.Context, owner string, repo string) ([]*github.RunnerApplicationDownload, *github.Response, error) { - ret := _m.Called(ctx, owner, repo) +// ListEntityRunnerApplicationDownloads provides a mock function with given fields: ctx +func (_m *GithubClient) ListEntityRunnerApplicationDownloads(ctx context.Context) ([]*github.RunnerApplicationDownload, *github.Response, error) { + ret := _m.Called(ctx) if len(ret) == 0 { - panic("no return value specified for ListRunnerApplicationDownloads") + panic("no return value specified for ListEntityRunnerApplicationDownloads") } var r0 []*github.RunnerApplicationDownload var r1 *github.Response var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]*github.RunnerApplicationDownload, *github.Response, error)); ok { - return rf(ctx, owner, repo) + if rf, ok := ret.Get(0).(func(context.Context) ([]*github.RunnerApplicationDownload, *github.Response, error)); ok { + return rf(ctx) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) []*github.RunnerApplicationDownload); ok { - r0 = rf(ctx, owner, repo) + if rf, ok := ret.Get(0).(func(context.Context) []*github.RunnerApplicationDownload); ok { + r0 = rf(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*github.RunnerApplicationDownload) } } - if rf, ok := ret.Get(1).(func(context.Context, string, string) *github.Response); ok { - r1 = rf(ctx, owner, repo) + if rf, ok := ret.Get(1).(func(context.Context) *github.Response); ok { + r1 = rf(ctx) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(*github.Response) } } - if rf, ok := ret.Get(2).(func(context.Context, string, string) error); ok { - r2 = rf(ctx, owner, repo) + if rf, ok := ret.Get(2).(func(context.Context) error); ok { + r2 = rf(ctx) } else { r2 = ret.Error(2) } @@ -659,38 +301,38 @@ func (_m *GithubClient) ListRunnerApplicationDownloads(ctx context.Context, owne return r0, r1, r2 } -// ListRunners provides a mock function with given fields: ctx, owner, repo, opts -func (_m *GithubClient) ListRunners(ctx context.Context, owner string, repo string, opts *github.ListOptions) (*github.Runners, *github.Response, error) { - ret := _m.Called(ctx, owner, repo, opts) +// ListEntityRunners provides a mock function with given fields: ctx, opts +func (_m *GithubClient) ListEntityRunners(ctx context.Context, opts *github.ListOptions) (*github.Runners, *github.Response, error) { + ret := _m.Called(ctx, opts) if len(ret) == 0 { - panic("no return value specified for ListRunners") + panic("no return value specified for ListEntityRunners") } var r0 *github.Runners var r1 *github.Response var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.ListOptions) (*github.Runners, *github.Response, error)); ok { - return rf(ctx, owner, repo, opts) + if rf, ok := ret.Get(0).(func(context.Context, *github.ListOptions) (*github.Runners, *github.Response, error)); ok { + return rf(ctx, opts) } - if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.ListOptions) *github.Runners); ok { - r0 = rf(ctx, owner, repo, opts) + if rf, ok := ret.Get(0).(func(context.Context, *github.ListOptions) *github.Runners); ok { + r0 = rf(ctx, opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*github.Runners) } } - if rf, ok := ret.Get(1).(func(context.Context, string, string, *github.ListOptions) *github.Response); ok { - r1 = rf(ctx, owner, repo, opts) + if rf, ok := ret.Get(1).(func(context.Context, *github.ListOptions) *github.Response); ok { + r1 = rf(ctx, opts) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(*github.Response) } } - if rf, ok := ret.Get(2).(func(context.Context, string, string, *github.ListOptions) error); ok { - r2 = rf(ctx, owner, repo, opts) + if rf, ok := ret.Get(2).(func(context.Context, *github.ListOptions) error); ok { + r2 = rf(ctx, opts) } else { r2 = ret.Error(2) } @@ -698,89 +340,29 @@ func (_m *GithubClient) ListRunners(ctx context.Context, owner string, repo stri return r0, r1, r2 } -// PingOrgHook provides a mock function with given fields: ctx, org, id -func (_m *GithubClient) PingOrgHook(ctx context.Context, org string, id int64) (*github.Response, error) { - ret := _m.Called(ctx, org, id) - - if len(ret) == 0 { - panic("no return value specified for PingOrgHook") - } - - var r0 *github.Response - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, int64) (*github.Response, error)); ok { - return rf(ctx, org, id) - } - if rf, ok := ret.Get(0).(func(context.Context, string, int64) *github.Response); ok { - r0 = rf(ctx, org, id) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*github.Response) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { - r1 = rf(ctx, org, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// PingRepoHook provides a mock function with given fields: ctx, owner, repo, id -func (_m *GithubClient) PingRepoHook(ctx context.Context, owner string, repo string, id int64) (*github.Response, error) { - ret := _m.Called(ctx, owner, repo, id) - - if len(ret) == 0 { - panic("no return value specified for PingRepoHook") - } - - var r0 *github.Response - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) (*github.Response, error)); ok { - return rf(ctx, owner, repo, id) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) *github.Response); ok { - r0 = rf(ctx, owner, repo, id) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*github.Response) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, int64) error); ok { - r1 = rf(ctx, owner, repo, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// RemoveOrganizationRunner provides a mock function with given fields: ctx, owner, runnerID -func (_m *GithubClient) RemoveOrganizationRunner(ctx context.Context, owner string, runnerID int64) (*github.Response, error) { - ret := _m.Called(ctx, owner, runnerID) +// PingEntityHook provides a mock function with given fields: ctx, id +func (_m *GithubClient) PingEntityHook(ctx context.Context, id int64) (*github.Response, error) { + ret := _m.Called(ctx, id) if len(ret) == 0 { - panic("no return value specified for RemoveOrganizationRunner") + panic("no return value specified for PingEntityHook") } var r0 *github.Response var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, int64) (*github.Response, error)); ok { - return rf(ctx, owner, runnerID) + if rf, ok := ret.Get(0).(func(context.Context, int64) (*github.Response, error)); ok { + return rf(ctx, id) } - if rf, ok := ret.Get(0).(func(context.Context, string, int64) *github.Response); ok { - r0 = rf(ctx, owner, runnerID) + if rf, ok := ret.Get(0).(func(context.Context, int64) *github.Response); ok { + r0 = rf(ctx, id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*github.Response) } } - if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { - r1 = rf(ctx, owner, runnerID) + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) } else { r1 = ret.Error(1) } @@ -788,29 +370,29 @@ func (_m *GithubClient) RemoveOrganizationRunner(ctx context.Context, owner stri return r0, r1 } -// RemoveRunner provides a mock function with given fields: ctx, owner, repo, runnerID -func (_m *GithubClient) RemoveRunner(ctx context.Context, owner string, repo string, runnerID int64) (*github.Response, error) { - ret := _m.Called(ctx, owner, repo, runnerID) +// RemoveEntityRunner provides a mock function with given fields: ctx, runnerID +func (_m *GithubClient) RemoveEntityRunner(ctx context.Context, runnerID int64) (*github.Response, error) { + ret := _m.Called(ctx, runnerID) if len(ret) == 0 { - panic("no return value specified for RemoveRunner") + panic("no return value specified for RemoveEntityRunner") } var r0 *github.Response var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) (*github.Response, error)); ok { - return rf(ctx, owner, repo, runnerID) + if rf, ok := ret.Get(0).(func(context.Context, int64) (*github.Response, error)); ok { + return rf(ctx, runnerID) } - if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) *github.Response); ok { - r0 = rf(ctx, owner, repo, runnerID) + if rf, ok := ret.Get(0).(func(context.Context, int64) *github.Response); ok { + r0 = rf(ctx, runnerID) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*github.Response) } } - if rf, ok := ret.Get(1).(func(context.Context, string, string, int64) error); ok { - r1 = rf(ctx, owner, repo, runnerID) + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, runnerID) } else { r1 = ret.Error(1) } diff --git a/runner/common/mocks/GithubEntityOperations.go b/runner/common/mocks/GithubEntityOperations.go new file mode 100644 index 00000000..488387f6 --- /dev/null +++ b/runner/common/mocks/GithubEntityOperations.go @@ -0,0 +1,376 @@ +// Code generated by mockery v2.42.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + github "github.com/google/go-github/v57/github" + mock "github.com/stretchr/testify/mock" + + params "github.com/cloudbase/garm/params" +) + +// GithubEntityOperations is an autogenerated mock type for the GithubEntityOperations type +type GithubEntityOperations struct { + mock.Mock +} + +// CreateEntityHook provides a mock function with given fields: ctx, hook +func (_m *GithubEntityOperations) CreateEntityHook(ctx context.Context, hook *github.Hook) (*github.Hook, error) { + ret := _m.Called(ctx, hook) + + if len(ret) == 0 { + panic("no return value specified for CreateEntityHook") + } + + var r0 *github.Hook + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *github.Hook) (*github.Hook, error)); ok { + return rf(ctx, hook) + } + if rf, ok := ret.Get(0).(func(context.Context, *github.Hook) *github.Hook); ok { + r0 = rf(ctx, hook) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.Hook) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *github.Hook) error); ok { + r1 = rf(ctx, hook) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateEntityRegistrationToken provides a mock function with given fields: ctx +func (_m *GithubEntityOperations) CreateEntityRegistrationToken(ctx context.Context) (*github.RegistrationToken, *github.Response, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for CreateEntityRegistrationToken") + } + + var r0 *github.RegistrationToken + var r1 *github.Response + var r2 error + if rf, ok := ret.Get(0).(func(context.Context) (*github.RegistrationToken, *github.Response, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *github.RegistrationToken); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.RegistrationToken) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) *github.Response); ok { + r1 = rf(ctx) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*github.Response) + } + } + + if rf, ok := ret.Get(2).(func(context.Context) error); ok { + r2 = rf(ctx) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// DeleteEntityHook provides a mock function with given fields: ctx, id +func (_m *GithubEntityOperations) DeleteEntityHook(ctx context.Context, id int64) (*github.Response, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for DeleteEntityHook") + } + + var r0 *github.Response + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*github.Response, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *github.Response); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.Response) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetEntityHook provides a mock function with given fields: ctx, id +func (_m *GithubEntityOperations) GetEntityHook(ctx context.Context, id int64) (*github.Hook, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for GetEntityHook") + } + + var r0 *github.Hook + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*github.Hook, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *github.Hook); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.Hook) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetEntityJITConfig provides a mock function with given fields: ctx, instance, pool, labels +func (_m *GithubEntityOperations) GetEntityJITConfig(ctx context.Context, instance string, pool params.Pool, labels []string) (map[string]string, *github.Runner, error) { + ret := _m.Called(ctx, instance, pool, labels) + + if len(ret) == 0 { + panic("no return value specified for GetEntityJITConfig") + } + + var r0 map[string]string + var r1 *github.Runner + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, params.Pool, []string) (map[string]string, *github.Runner, error)); ok { + return rf(ctx, instance, pool, labels) + } + if rf, ok := ret.Get(0).(func(context.Context, string, params.Pool, []string) map[string]string); ok { + r0 = rf(ctx, instance, pool, labels) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, params.Pool, []string) *github.Runner); ok { + r1 = rf(ctx, instance, pool, labels) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*github.Runner) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, string, params.Pool, []string) error); ok { + r2 = rf(ctx, instance, pool, labels) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// ListEntityHooks provides a mock function with given fields: ctx, opts +func (_m *GithubEntityOperations) ListEntityHooks(ctx context.Context, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for ListEntityHooks") + } + + var r0 []*github.Hook + var r1 *github.Response + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, *github.ListOptions) ([]*github.Hook, *github.Response, error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *github.ListOptions) []*github.Hook); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*github.Hook) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *github.ListOptions) *github.Response); ok { + r1 = rf(ctx, opts) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*github.Response) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, *github.ListOptions) error); ok { + r2 = rf(ctx, opts) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// ListEntityRunnerApplicationDownloads provides a mock function with given fields: ctx +func (_m *GithubEntityOperations) ListEntityRunnerApplicationDownloads(ctx context.Context) ([]*github.RunnerApplicationDownload, *github.Response, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListEntityRunnerApplicationDownloads") + } + + var r0 []*github.RunnerApplicationDownload + var r1 *github.Response + var r2 error + if rf, ok := ret.Get(0).(func(context.Context) ([]*github.RunnerApplicationDownload, *github.Response, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []*github.RunnerApplicationDownload); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*github.RunnerApplicationDownload) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) *github.Response); ok { + r1 = rf(ctx) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*github.Response) + } + } + + if rf, ok := ret.Get(2).(func(context.Context) error); ok { + r2 = rf(ctx) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// ListEntityRunners provides a mock function with given fields: ctx, opts +func (_m *GithubEntityOperations) ListEntityRunners(ctx context.Context, opts *github.ListOptions) (*github.Runners, *github.Response, error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for ListEntityRunners") + } + + var r0 *github.Runners + var r1 *github.Response + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, *github.ListOptions) (*github.Runners, *github.Response, error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *github.ListOptions) *github.Runners); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.Runners) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *github.ListOptions) *github.Response); ok { + r1 = rf(ctx, opts) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*github.Response) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, *github.ListOptions) error); ok { + r2 = rf(ctx, opts) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// PingEntityHook provides a mock function with given fields: ctx, id +func (_m *GithubEntityOperations) PingEntityHook(ctx context.Context, id int64) (*github.Response, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for PingEntityHook") + } + + var r0 *github.Response + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*github.Response, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *github.Response); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.Response) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemoveEntityRunner provides a mock function with given fields: ctx, runnerID +func (_m *GithubEntityOperations) RemoveEntityRunner(ctx context.Context, runnerID int64) (*github.Response, error) { + ret := _m.Called(ctx, runnerID) + + if len(ret) == 0 { + panic("no return value specified for RemoveEntityRunner") + } + + var r0 *github.Response + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*github.Response, error)); ok { + return rf(ctx, runnerID) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *github.Response); ok { + r0 = rf(ctx, runnerID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.Response) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, runnerID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewGithubEntityOperations creates a new instance of GithubEntityOperations. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewGithubEntityOperations(t interface { + mock.TestingT + Cleanup(func()) +}) *GithubEntityOperations { + mock := &GithubEntityOperations{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/runner/common/util.go b/runner/common/util.go index f9ebf633..d2e6c16b 100644 --- a/runner/common/util.go +++ b/runner/common/util.go @@ -4,22 +4,21 @@ import ( "context" "github.com/google/go-github/v57/github" -) -type OrganizationHooks interface { - ListOrgHooks(ctx context.Context, org string, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) - GetOrgHook(ctx context.Context, org string, id int64) (*github.Hook, *github.Response, error) - CreateOrgHook(ctx context.Context, org string, hook *github.Hook) (*github.Hook, *github.Response, error) - DeleteOrgHook(ctx context.Context, org string, id int64) (*github.Response, error) - PingOrgHook(ctx context.Context, org string, id int64) (*github.Response, error) -} + "github.com/cloudbase/garm/params" +) -type RepositoryHooks interface { - ListRepoHooks(ctx context.Context, owner, repo string, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) - GetRepoHook(ctx context.Context, owner, repo string, id int64) (*github.Hook, *github.Response, error) - CreateRepoHook(ctx context.Context, owner, repo string, hook *github.Hook) (*github.Hook, *github.Response, error) - DeleteRepoHook(ctx context.Context, owner, repo string, id int64) (*github.Response, error) - PingRepoHook(ctx context.Context, owner, repo string, id int64) (*github.Response, error) +type GithubEntityOperations interface { + ListEntityHooks(ctx context.Context, opts *github.ListOptions) (ret []*github.Hook, response *github.Response, err error) + GetEntityHook(ctx context.Context, id int64) (ret *github.Hook, err error) + CreateEntityHook(ctx context.Context, hook *github.Hook) (ret *github.Hook, err error) + DeleteEntityHook(ctx context.Context, id int64) (ret *github.Response, err error) + PingEntityHook(ctx context.Context, id int64) (ret *github.Response, err error) + ListEntityRunners(ctx context.Context, opts *github.ListOptions) (*github.Runners, *github.Response, error) + ListEntityRunnerApplicationDownloads(ctx context.Context) ([]*github.RunnerApplicationDownload, *github.Response, error) + RemoveEntityRunner(ctx context.Context, runnerID int64) (*github.Response, error) + CreateEntityRegistrationToken(ctx context.Context) (*github.RegistrationToken, *github.Response, error) + GetEntityJITConfig(ctx context.Context, instance string, pool params.Pool, labels []string) (jitConfigMap map[string]string, runner *github.Runner, err error) } // GithubClient that describes the minimum list of functions we need to interact with github. @@ -27,50 +26,8 @@ type RepositoryHooks interface { // //go:generate mockery --all type GithubClient interface { - OrganizationHooks - RepositoryHooks + GithubEntityOperations // GetWorkflowJobByID gets details about a single workflow job. GetWorkflowJobByID(ctx context.Context, owner, repo string, jobID int64) (*github.WorkflowJob, *github.Response, error) - // ListRunners lists all runners within a repository. - ListRunners(ctx context.Context, owner, repo string, opts *github.ListOptions) (*github.Runners, *github.Response, error) - // ListRunnerApplicationDownloads returns a list of github runner application downloads for the - // various supported operating systems and architectures. - ListRunnerApplicationDownloads(ctx context.Context, owner, repo string) ([]*github.RunnerApplicationDownload, *github.Response, error) - // RemoveRunner removes one runner from a repository. - RemoveRunner(ctx context.Context, owner, repo string, runnerID int64) (*github.Response, error) - // CreateRegistrationToken creates a runner registration token for one repository. - CreateRegistrationToken(ctx context.Context, owner, repo string) (*github.RegistrationToken, *github.Response, error) - // GenerateRepoJITConfig generates a just-in-time configuration for a repository. - GenerateRepoJITConfig(ctx context.Context, owner, repo string, request *github.GenerateJITConfigRequest) (*github.JITRunnerConfig, *github.Response, error) - - // ListOrganizationRunners lists all runners within an organization. - ListOrganizationRunners(ctx context.Context, owner string, opts *github.ListOptions) (*github.Runners, *github.Response, error) - // ListOrganizationRunnerApplicationDownloads returns a list of github runner application downloads for the - // various supported operating systems and architectures. - ListOrganizationRunnerApplicationDownloads(ctx context.Context, owner string) ([]*github.RunnerApplicationDownload, *github.Response, error) - // RemoveOrganizationRunner removes one github runner from an organization. - RemoveOrganizationRunner(ctx context.Context, owner string, runnerID int64) (*github.Response, error) - // CreateOrganizationRegistrationToken creates a runner registration token for an organization. - CreateOrganizationRegistrationToken(ctx context.Context, owner string) (*github.RegistrationToken, *github.Response, error) - // GenerateOrgJITConfig generate a just-in-time configuration for an organization. - GenerateOrgJITConfig(ctx context.Context, owner string, request *github.GenerateJITConfigRequest) (*github.JITRunnerConfig, *github.Response, error) - // ListOrganizationRunnerGroups lists all runner groups within an organization. - ListOrganizationRunnerGroups(ctx context.Context, org string, opts *github.ListOrgRunnerGroupOptions) (*github.RunnerGroups, *github.Response, error) -} - -type GithubEnterpriseClient interface { - // ListRunners lists all runners within a repository. - ListRunners(ctx context.Context, enterprise string, opts *github.ListOptions) (*github.Runners, *github.Response, error) - // RemoveRunner removes one runner from an enterprise. - RemoveRunner(ctx context.Context, enterprise string, runnerID int64) (*github.Response, error) - // CreateRegistrationToken creates a runner registration token for an enterprise. - CreateRegistrationToken(ctx context.Context, enterprise string) (*github.RegistrationToken, *github.Response, error) - // ListRunnerApplicationDownloads returns a list of github runner application downloads for the - // various supported operating systems and architectures. - ListRunnerApplicationDownloads(ctx context.Context, enterprise string) ([]*github.RunnerApplicationDownload, *github.Response, error) - // GenerateEnterpriseJITConfig generate a just-in-time configuration for an enterprise. - GenerateEnterpriseJITConfig(ctx context.Context, enterprise string, request *github.GenerateJITConfigRequest) (*github.JITRunnerConfig, *github.Response, error) - // ListRunnerGroups lists all self-hosted runner groups configured in an enterprise. - ListRunnerGroups(ctx context.Context, enterprise string, opts *github.ListEnterpriseRunnerGroupOptions) (*github.EnterpriseRunnerGroups, *github.Response, error) } diff --git a/runner/metadata.go b/runner/metadata.go index 84151fcd..5dfaefa3 100644 --- a/runner/metadata.go +++ b/runner/metadata.go @@ -69,11 +69,11 @@ func (r *Runner) GetRunnerServiceName(ctx context.Context) (string, error) { tpl := "actions.runner.%s.%s" var serviceName string switch pool.PoolType() { - case params.EnterprisePool: + case params.GithubEntityTypeEnterprise: serviceName = fmt.Sprintf(tpl, pool.EnterpriseName, instance.Name) - case params.OrganizationPool: + case params.GithubEntityTypeOrganization: serviceName = fmt.Sprintf(tpl, pool.OrgName, instance.Name) - case params.RepositoryPool: + case params.GithubEntityTypeRepository: serviceName = fmt.Sprintf(tpl, strings.ReplaceAll(pool.RepoName, "/", "-"), instance.Name) } return serviceName, nil diff --git a/runner/pool/common.go b/runner/pool/common.go index 7479ef2e..fcf4f73f 100644 --- a/runner/pool/common.go +++ b/runner/pool/common.go @@ -1,6 +1,8 @@ package pool import ( + "context" + "net/http" "net/url" "strings" @@ -62,3 +64,25 @@ func hookToParamsHookInfo(hook *github.Hook) params.HookInfo { InsecureSSL: insecureSSL, } } + +func (r *basePoolManager) listHooks(ctx context.Context) ([]*github.Hook, error) { + opts := github.ListOptions{ + PerPage: 100, + } + var allHooks []*github.Hook + for { + hooks, ghResp, err := r.ghcli.ListEntityHooks(ctx, &opts) + if err != nil { + if ghResp != nil && ghResp.StatusCode == http.StatusNotFound { + return nil, runnerErrors.NewBadRequestError("repository not found or your PAT does not have access to manage webhooks") + } + return nil, errors.Wrap(err, "fetching hooks") + } + allHooks = append(allHooks, hooks...) + if ghResp.NextPage == 0 { + break + } + opts.Page = ghResp.NextPage + } + return allHooks, nil +} diff --git a/runner/pool/enterprise.go b/runner/pool/enterprise.go deleted file mode 100644 index 24685fcb..00000000 --- a/runner/pool/enterprise.go +++ /dev/null @@ -1,398 +0,0 @@ -package pool - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "log/slog" - "net/http" - "strings" - "sync" - - "github.com/google/go-github/v57/github" - "github.com/pkg/errors" - - runnerErrors "github.com/cloudbase/garm-provider-common/errors" - commonParams "github.com/cloudbase/garm-provider-common/params" - dbCommon "github.com/cloudbase/garm/database/common" - "github.com/cloudbase/garm/metrics" - "github.com/cloudbase/garm/params" - "github.com/cloudbase/garm/runner/common" - "github.com/cloudbase/garm/util" -) - -// test that we implement PoolManager -var _ poolHelper = &enterprise{} - -func NewEnterprisePoolManager(ctx context.Context, cfg params.Enterprise, cfgInternal params.Internal, providers map[string]common.Provider, store dbCommon.Store) (common.PoolManager, error) { - ctx = util.WithContext(ctx, slog.Any("pool_mgr", cfg.Name), slog.Any("pool_type", params.EnterprisePool)) - ghc, ghEnterpriseClient, err := util.GithubClient(ctx, cfgInternal.GithubCredentialsDetails) - if err != nil { - return nil, errors.Wrap(err, "getting github client") - } - - wg := &sync.WaitGroup{} - keyMuxes := &keyMutex{} - - helper := &enterprise{ - cfg: cfg, - cfgInternal: cfgInternal, - ctx: ctx, - ghcli: ghc, - ghcEnterpriseCli: ghEnterpriseClient, - id: cfg.ID, - store: store, - } - - repo := &basePoolManager{ - ctx: ctx, - store: store, - providers: providers, - controllerID: cfgInternal.ControllerID, - urls: urls{ - webhookURL: cfgInternal.BaseWebhookURL, - callbackURL: cfgInternal.InstanceCallbackURL, - metadataURL: cfgInternal.InstanceMetadataURL, - controllerWebhookURL: cfgInternal.ControllerWebhookURL, - }, - quit: make(chan struct{}), - helper: helper, - credsDetails: cfgInternal.GithubCredentialsDetails, - wg: wg, - keyMux: keyMuxes, - } - return repo, nil -} - -type enterprise struct { - cfg params.Enterprise - cfgInternal params.Internal - ctx context.Context - ghcli common.GithubClient - ghcEnterpriseCli common.GithubEnterpriseClient - id string - store dbCommon.Store - - mux sync.Mutex -} - -func (e *enterprise) PoolBalancerType() params.PoolBalancerType { - if e.cfgInternal.PoolBalancerType == "" { - return params.PoolBalancerTypeRoundRobin - } - return e.cfgInternal.PoolBalancerType -} - -func (e *enterprise) findRunnerGroupByName(name string) (*github.EnterpriseRunnerGroup, error) { - // nolint:golangci-lint,godox - // TODO(gabriel-samfira): implement caching - opts := github.ListEnterpriseRunnerGroupOptions{ - ListOptions: github.ListOptions{ - PerPage: 100, - }, - } - - for { - metrics.GithubOperationCount.WithLabelValues( - "ListOrganizationRunnerGroups", // label: operation - metricsLabelEnterpriseScope, // label: scope - ).Inc() - runnerGroups, ghResp, err := e.ghcEnterpriseCli.ListRunnerGroups(e.ctx, e.cfg.Name, &opts) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "ListOrganizationRunnerGroups", // label: operation - metricsLabelEnterpriseScope, // label: scope - ).Inc() - if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { - return nil, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching runners") - } - return nil, errors.Wrap(err, "fetching runners") - } - for _, runnerGroup := range runnerGroups.RunnerGroups { - if runnerGroup.Name != nil && *runnerGroup.Name == name { - return runnerGroup, nil - } - } - if ghResp.NextPage == 0 { - break - } - opts.Page = ghResp.NextPage - } - - return nil, errors.Wrap(runnerErrors.ErrNotFound, "runner group not found") -} - -func (e *enterprise) GetJITConfig(ctx context.Context, instance string, pool params.Pool, labels []string) (jitConfigMap map[string]string, runner *github.Runner, err error) { - var rg int64 = 1 - if pool.GitHubRunnerGroup != "" { - runnerGroup, err := e.findRunnerGroupByName(pool.GitHubRunnerGroup) - if err != nil { - return nil, nil, fmt.Errorf("failed to find runner group: %w", err) - } - rg = *runnerGroup.ID - } - - req := github.GenerateJITConfigRequest{ - Name: instance, - RunnerGroupID: rg, - Labels: labels, - // nolint:golangci-lint,godox - // TODO(gabriel-samfira): Should we make this configurable? - WorkFolder: github.String("_work"), - } - metrics.GithubOperationCount.WithLabelValues( - "GenerateEnterpriseJITConfig", // label: operation - metricsLabelEnterpriseScope, // label: scope - ).Inc() - jitConfig, resp, err := e.ghcEnterpriseCli.GenerateEnterpriseJITConfig(ctx, e.cfg.Name, &req) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "GenerateEnterpriseJITConfig", // label: operation - metricsLabelEnterpriseScope, // label: scope - ).Inc() - if resp != nil && resp.StatusCode == http.StatusUnauthorized { - return nil, nil, fmt.Errorf("failed to get JIT config: %w", err) - } - return nil, nil, fmt.Errorf("failed to get JIT config: %w", err) - } - - runner = jitConfig.Runner - defer func() { - if err != nil && runner != nil { - metrics.GithubOperationCount.WithLabelValues( - "RemoveRunner", // label: operation - metricsLabelEnterpriseScope, // label: scope - ).Inc() - _, innerErr := e.ghcEnterpriseCli.RemoveRunner(e.ctx, e.cfg.Name, runner.GetID()) - if innerErr != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "RemoveRunner", // label: operation - metricsLabelEnterpriseScope, // label: scope - ).Inc() - } - slog.With(slog.Any("error", innerErr)).ErrorContext( - ctx, "failed to remove runner", - "runner_id", runner.GetID(), "organization", e.cfg.Name) - } - }() - - decoded, err := base64.StdEncoding.DecodeString(*jitConfig.EncodedJITConfig) - if err != nil { - return nil, nil, fmt.Errorf("failed to decode JIT config: %w", err) - } - - var ret map[string]string - if err := json.Unmarshal(decoded, &ret); err != nil { - return nil, nil, fmt.Errorf("failed to unmarshal JIT config: %w", err) - } - - return ret, jitConfig.Runner, nil -} - -func (e *enterprise) PoolType() params.PoolType { - return params.EnterprisePool -} - -func (e *enterprise) GetRunnerInfoFromWorkflow(job params.WorkflowJob) (params.RunnerInfo, error) { - if err := e.ValidateOwner(job); err != nil { - return params.RunnerInfo{}, errors.Wrap(err, "validating owner") - } - metrics.GithubOperationCount.WithLabelValues( - "GetWorkflowJobByID", // label: operation - metricsLabelEnterpriseScope, // label: scope - ).Inc() - workflow, ghResp, err := e.ghcli.GetWorkflowJobByID(e.ctx, job.Repository.Owner.Login, job.Repository.Name, job.WorkflowJob.ID) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "GetWorkflowJobByID", // label: operation - metricsLabelEnterpriseScope, // label: scope - ).Inc() - if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { - return params.RunnerInfo{}, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching workflow info") - } - return params.RunnerInfo{}, errors.Wrap(err, "fetching workflow info") - } - - if workflow.RunnerName != nil { - return params.RunnerInfo{ - Name: *workflow.RunnerName, - Labels: workflow.Labels, - }, nil - } - return params.RunnerInfo{}, fmt.Errorf("failed to find runner name from workflow") -} - -func (e *enterprise) UpdateState(param params.UpdatePoolStateParams) error { - e.mux.Lock() - defer e.mux.Unlock() - - e.cfg.WebhookSecret = param.WebhookSecret - if param.InternalConfig != nil { - e.cfgInternal = *param.InternalConfig - } - - ghc, ghcEnterprise, err := util.GithubClient(e.ctx, e.cfgInternal.GithubCredentialsDetails) - if err != nil { - return errors.Wrap(err, "getting github client") - } - e.ghcli = ghc - e.ghcEnterpriseCli = ghcEnterprise - return nil -} - -func (e *enterprise) GetGithubRunners() ([]*github.Runner, error) { - opts := github.ListOptions{ - PerPage: 100, - } - - var allRunners []*github.Runner - for { - metrics.GithubOperationCount.WithLabelValues( - "ListRunners", // label: operation - metricsLabelEnterpriseScope, // label: scope - ).Inc() - runners, ghResp, err := e.ghcEnterpriseCli.ListRunners(e.ctx, e.cfg.Name, &opts) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "ListRunners", // label: operation - metricsLabelEnterpriseScope, // label: scope - ).Inc() - if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { - return nil, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching runners") - } - return nil, errors.Wrap(err, "fetching runners") - } - allRunners = append(allRunners, runners.Runners...) - if ghResp.NextPage == 0 { - break - } - opts.Page = ghResp.NextPage - } - return allRunners, nil -} - -func (e *enterprise) FetchTools() ([]commonParams.RunnerApplicationDownload, error) { - e.mux.Lock() - defer e.mux.Unlock() - metrics.GithubOperationCount.WithLabelValues( - "ListRunnerApplicationDownloads", // label: operation - metricsLabelEnterpriseScope, // label: scope - ).Inc() - tools, ghResp, err := e.ghcEnterpriseCli.ListRunnerApplicationDownloads(e.ctx, e.cfg.Name) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "ListRunnerApplicationDownloads", // label: operation - metricsLabelEnterpriseScope, // label: scope - ).Inc() - if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { - return nil, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching runners") - } - return nil, errors.Wrap(err, "fetching runner tools") - } - - ret := []commonParams.RunnerApplicationDownload{} - for _, tool := range tools { - if tool == nil { - continue - } - ret = append(ret, commonParams.RunnerApplicationDownload(*tool)) - } - - return ret, nil -} - -func (e *enterprise) FetchDbInstances() ([]params.Instance, error) { - return e.store.ListEnterpriseInstances(e.ctx, e.id) -} - -func (e *enterprise) RemoveGithubRunner(runnerID int64) (*github.Response, error) { - metrics.GithubOperationCount.WithLabelValues( - "RemoveRunner", // label: operation - metricsLabelEnterpriseScope, // label: scope - ).Inc() - ghResp, err := e.ghcEnterpriseCli.RemoveRunner(e.ctx, e.cfg.Name, runnerID) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "RemoveRunner", // label: operation - metricsLabelEnterpriseScope, // label: scope - ).Inc() - return nil, err - } - return ghResp, nil -} - -func (e *enterprise) ListPools() ([]params.Pool, error) { - pools, err := e.store.ListEnterprisePools(e.ctx, e.id) - if err != nil { - return nil, errors.Wrap(err, "fetching pools") - } - return pools, nil -} - -func (e *enterprise) GithubURL() string { - return fmt.Sprintf("%s/enterprises/%s", e.cfgInternal.GithubCredentialsDetails.BaseURL, e.cfg.Name) -} - -func (e *enterprise) JwtToken() string { - return e.cfgInternal.JWTSecret -} - -func (e *enterprise) GetGithubRegistrationToken() (string, error) { - metrics.GithubOperationCount.WithLabelValues( - "CreateRegistrationToken", // label: operation - metricsLabelEnterpriseScope, // label: scope - ).Inc() - - tk, ghResp, err := e.ghcEnterpriseCli.CreateRegistrationToken(e.ctx, e.cfg.Name) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "CreateRegistrationToken", // label: operation - metricsLabelEnterpriseScope, // label: scope - ).Inc() - if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { - return "", errors.Wrap(runnerErrors.ErrUnauthorized, "fetching registration token") - } - return "", errors.Wrap(err, "creating runner token") - } - return *tk.Token, nil -} - -func (e *enterprise) String() string { - return e.cfg.Name -} - -func (e *enterprise) WebhookSecret() string { - return e.cfg.WebhookSecret -} - -func (e *enterprise) GetPoolByID(poolID string) (params.Pool, error) { - pool, err := e.store.GetEnterprisePool(e.ctx, e.id, poolID) - if err != nil { - return params.Pool{}, errors.Wrap(err, "fetching pool") - } - return pool, nil -} - -func (e *enterprise) ValidateOwner(job params.WorkflowJob) error { - if !strings.EqualFold(job.Enterprise.Slug, e.cfg.Name) { - return runnerErrors.NewBadRequestError("job not meant for this pool manager") - } - return nil -} - -func (e *enterprise) ID() string { - return e.id -} - -func (e *enterprise) InstallHook(_ context.Context, _ *github.Hook) (params.HookInfo, error) { - return params.HookInfo{}, fmt.Errorf("not implemented") -} - -func (e *enterprise) UninstallHook(_ context.Context, _ string) error { - return fmt.Errorf("not implemented") -} - -func (e *enterprise) GetHookInfo(_ context.Context) (params.HookInfo, error) { - return params.HookInfo{}, fmt.Errorf("not implemented") -} diff --git a/runner/pool/interfaces.go b/runner/pool/interfaces.go deleted file mode 100644 index 3815a3ac..00000000 --- a/runner/pool/interfaces.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2022 Cloudbase Solutions SRL -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package pool - -import ( - "context" - - "github.com/google/go-github/v57/github" - - commonParams "github.com/cloudbase/garm-provider-common/params" - "github.com/cloudbase/garm/params" -) - -type poolHelper interface { - GetGithubRunners() ([]*github.Runner, error) - GetGithubRegistrationToken() (string, error) - GetRunnerInfoFromWorkflow(job params.WorkflowJob) (params.RunnerInfo, error) - RemoveGithubRunner(runnerID int64) (*github.Response, error) - FetchTools() ([]commonParams.RunnerApplicationDownload, error) - - InstallHook(ctx context.Context, req *github.Hook) (params.HookInfo, error) - UninstallHook(ctx context.Context, url string) error - GetHookInfo(ctx context.Context) (params.HookInfo, error) - - GetJITConfig(ctx context.Context, instanceName string, pool params.Pool, labels []string) (map[string]string, *github.Runner, error) - - FetchDbInstances() ([]params.Instance, error) - ListPools() ([]params.Pool, error) - GithubURL() string - JwtToken() string - String() string - GetPoolByID(poolID string) (params.Pool, error) - ValidateOwner(job params.WorkflowJob) error - UpdateState(param params.UpdatePoolStateParams) error - WebhookSecret() string - ID() string - PoolType() params.PoolType - PoolBalancerType() params.PoolBalancerType -} diff --git a/runner/pool/organization.go b/runner/pool/organization.go deleted file mode 100644 index 65448953..00000000 --- a/runner/pool/organization.go +++ /dev/null @@ -1,512 +0,0 @@ -// Copyright 2022 Cloudbase Solutions SRL -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package pool - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "log/slog" - "net/http" - "strings" - "sync" - - "github.com/google/go-github/v57/github" - "github.com/pkg/errors" - - runnerErrors "github.com/cloudbase/garm-provider-common/errors" - commonParams "github.com/cloudbase/garm-provider-common/params" - dbCommon "github.com/cloudbase/garm/database/common" - "github.com/cloudbase/garm/metrics" - "github.com/cloudbase/garm/params" - "github.com/cloudbase/garm/runner/common" - "github.com/cloudbase/garm/util" -) - -// test that we implement PoolManager -var _ poolHelper = &organization{} - -func NewOrganizationPoolManager(ctx context.Context, cfg params.Organization, cfgInternal params.Internal, providers map[string]common.Provider, store dbCommon.Store) (common.PoolManager, error) { - ctx = util.WithContext(ctx, slog.Any("pool_mgr", cfg.Name), slog.Any("pool_type", params.OrganizationPool)) - ghc, _, err := util.GithubClient(ctx, cfgInternal.GithubCredentialsDetails) - if err != nil { - return nil, errors.Wrap(err, "getting github client") - } - - wg := &sync.WaitGroup{} - keyMuxes := &keyMutex{} - - helper := &organization{ - cfg: cfg, - cfgInternal: cfgInternal, - ctx: ctx, - ghcli: ghc, - id: cfg.ID, - store: store, - } - - repo := &basePoolManager{ - ctx: ctx, - store: store, - providers: providers, - controllerID: cfgInternal.ControllerID, - urls: urls{ - webhookURL: cfgInternal.BaseWebhookURL, - callbackURL: cfgInternal.InstanceCallbackURL, - metadataURL: cfgInternal.InstanceMetadataURL, - controllerWebhookURL: cfgInternal.ControllerWebhookURL, - }, - quit: make(chan struct{}), - helper: helper, - credsDetails: cfgInternal.GithubCredentialsDetails, - wg: wg, - keyMux: keyMuxes, - } - return repo, nil -} - -type organization struct { - cfg params.Organization - cfgInternal params.Internal - ctx context.Context - ghcli common.GithubClient - id string - store dbCommon.Store - - mux sync.Mutex -} - -func (o *organization) PoolBalancerType() params.PoolBalancerType { - if o.cfgInternal.PoolBalancerType == "" { - return params.PoolBalancerTypeRoundRobin - } - return o.cfgInternal.PoolBalancerType -} - -func (o *organization) findRunnerGroupByName(name string) (*github.RunnerGroup, error) { - // nolint:golangci-lint,godox - // TODO(gabriel-samfira): implement caching - opts := github.ListOrgRunnerGroupOptions{ - ListOptions: github.ListOptions{ - PerPage: 100, - }, - } - - for { - metrics.GithubOperationCount.WithLabelValues( - "ListOrganizationRunnerGroups", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - runnerGroups, ghResp, err := o.ghcli.ListOrganizationRunnerGroups(o.ctx, o.cfg.Name, &opts) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "ListOrganizationRunnerGroups", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { - return nil, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching runners") - } - return nil, errors.Wrap(err, "fetching runners") - } - for _, runnerGroup := range runnerGroups.RunnerGroups { - if runnerGroup.GetName() == name { - return runnerGroup, nil - } - } - if ghResp.NextPage == 0 { - break - } - opts.Page = ghResp.NextPage - } - - return nil, errors.Wrap(runnerErrors.ErrNotFound, "runner group not found") -} - -func (o *organization) GetJITConfig(ctx context.Context, instance string, pool params.Pool, labels []string) (jitConfigMap map[string]string, runner *github.Runner, err error) { - var rg int64 = 1 - if pool.GitHubRunnerGroup != "" { - runnerGroup, err := o.findRunnerGroupByName(pool.GitHubRunnerGroup) - if err != nil { - return nil, nil, fmt.Errorf("failed to find runner group: %w", err) - } - rg = runnerGroup.GetID() - } - - req := github.GenerateJITConfigRequest{ - Name: instance, - RunnerGroupID: rg, - Labels: labels, - // nolint:golangci-lint,godox - // TODO(gabriel-samfira): Should we make this configurable? - WorkFolder: github.String("_work"), - } - metrics.GithubOperationCount.WithLabelValues( - "GenerateOrgJITConfig", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - jitConfig, resp, err := o.ghcli.GenerateOrgJITConfig(ctx, o.cfg.Name, &req) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "GenerateOrgJITConfig", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - if resp != nil && resp.StatusCode == http.StatusUnauthorized { - return nil, nil, fmt.Errorf("failed to get JIT config: %w", err) - } - return nil, nil, fmt.Errorf("failed to get JIT config: %w", err) - } - - runner = jitConfig.GetRunner() - defer func() { - if err != nil && runner != nil { - metrics.GithubOperationCount.WithLabelValues( - "RemoveOrganizationRunner", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - _, innerErr := o.ghcli.RemoveOrganizationRunner(o.ctx, o.cfg.Name, runner.GetID()) - if innerErr != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "RemoveOrganizationRunner", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - } - slog.With(slog.Any("error", innerErr)).ErrorContext( - ctx, "failed to remove runner", - "runner_id", runner.GetID(), "organization", o.cfg.Name) - } - }() - - decoded, err := base64.StdEncoding.DecodeString(jitConfig.GetEncodedJITConfig()) - if err != nil { - return nil, nil, fmt.Errorf("failed to decode JIT config: %w", err) - } - - var ret map[string]string - if err := json.Unmarshal(decoded, &ret); err != nil { - return nil, nil, fmt.Errorf("failed to unmarshal JIT config: %w", err) - } - - return ret, runner, nil -} - -func (o *organization) PoolType() params.PoolType { - return params.OrganizationPool -} - -func (o *organization) GetRunnerInfoFromWorkflow(job params.WorkflowJob) (params.RunnerInfo, error) { - if err := o.ValidateOwner(job); err != nil { - return params.RunnerInfo{}, errors.Wrap(err, "validating owner") - } - metrics.GithubOperationCount.WithLabelValues( - "GetWorkflowJobByID", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - workflow, ghResp, err := o.ghcli.GetWorkflowJobByID(o.ctx, job.Organization.Login, job.Repository.Name, job.WorkflowJob.ID) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "GetWorkflowJobByID", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { - return params.RunnerInfo{}, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching workflow info") - } - return params.RunnerInfo{}, errors.Wrap(err, "fetching workflow info") - } - - if workflow.RunnerName != nil { - return params.RunnerInfo{ - Name: *workflow.RunnerName, - Labels: workflow.Labels, - }, nil - } - return params.RunnerInfo{}, fmt.Errorf("failed to find runner name from workflow") -} - -func (o *organization) UpdateState(param params.UpdatePoolStateParams) error { - o.mux.Lock() - defer o.mux.Unlock() - - o.cfg.WebhookSecret = param.WebhookSecret - if param.InternalConfig != nil { - o.cfgInternal = *param.InternalConfig - } - - ghc, _, err := util.GithubClient(o.ctx, o.cfgInternal.GithubCredentialsDetails) - if err != nil { - return errors.Wrap(err, "getting github client") - } - o.ghcli = ghc - return nil -} - -func (o *organization) GetGithubRunners() ([]*github.Runner, error) { - opts := github.ListOptions{ - PerPage: 100, - } - - var allRunners []*github.Runner - for { - metrics.GithubOperationCount.WithLabelValues( - "ListOrganizationRunners", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - runners, ghResp, err := o.ghcli.ListOrganizationRunners(o.ctx, o.cfg.Name, &opts) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "ListOrganizationRunners", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { - return nil, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching runners") - } - return nil, errors.Wrap(err, "fetching runners") - } - allRunners = append(allRunners, runners.Runners...) - if ghResp.NextPage == 0 { - break - } - opts.Page = ghResp.NextPage - } - - return allRunners, nil -} - -func (o *organization) FetchTools() ([]commonParams.RunnerApplicationDownload, error) { - o.mux.Lock() - defer o.mux.Unlock() - metrics.GithubOperationCount.WithLabelValues( - "ListOrganizationRunnerApplicationDownloads", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - tools, ghResp, err := o.ghcli.ListOrganizationRunnerApplicationDownloads(o.ctx, o.cfg.Name) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "ListOrganizationRunnerApplicationDownloads", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { - return nil, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching tools") - } - return nil, errors.Wrap(err, "fetching runner tools") - } - - ret := []commonParams.RunnerApplicationDownload{} - for _, tool := range tools { - if tool == nil { - continue - } - ret = append(ret, commonParams.RunnerApplicationDownload(*tool)) - } - - return ret, nil -} - -func (o *organization) FetchDbInstances() ([]params.Instance, error) { - return o.store.ListOrgInstances(o.ctx, o.id) -} - -func (o *organization) RemoveGithubRunner(runnerID int64) (*github.Response, error) { - metrics.GithubOperationCount.WithLabelValues( - "RemoveRunner", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - - ghResp, err := o.ghcli.RemoveOrganizationRunner(o.ctx, o.cfg.Name, runnerID) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "RemoveRunner", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - return nil, err - } - - return ghResp, nil -} - -func (o *organization) ListPools() ([]params.Pool, error) { - pools, err := o.store.ListOrgPools(o.ctx, o.id) - if err != nil { - return nil, errors.Wrap(err, "fetching pools") - } - return pools, nil -} - -func (o *organization) GithubURL() string { - return fmt.Sprintf("%s/%s", o.cfgInternal.GithubCredentialsDetails.BaseURL, o.cfg.Name) -} - -func (o *organization) JwtToken() string { - return o.cfgInternal.JWTSecret -} - -func (o *organization) GetGithubRegistrationToken() (string, error) { - metrics.GithubOperationCount.WithLabelValues( - "CreateOrganizationRegistrationToken", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - tk, ghResp, err := o.ghcli.CreateOrganizationRegistrationToken(o.ctx, o.cfg.Name) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "CreateOrganizationRegistrationToken", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { - return "", errors.Wrap(runnerErrors.ErrUnauthorized, "fetching token") - } - - return "", errors.Wrap(err, "creating runner token") - } - return *tk.Token, nil -} - -func (o *organization) String() string { - return o.cfg.Name -} - -func (o *organization) WebhookSecret() string { - return o.cfg.WebhookSecret -} - -func (o *organization) GetPoolByID(poolID string) (params.Pool, error) { - pool, err := o.store.GetOrganizationPool(o.ctx, o.id, poolID) - if err != nil { - return params.Pool{}, errors.Wrap(err, "fetching pool") - } - return pool, nil -} - -func (o *organization) ValidateOwner(job params.WorkflowJob) error { - if !strings.EqualFold(job.Organization.Login, o.cfg.Name) { - return runnerErrors.NewBadRequestError("job not meant for this pool manager") - } - return nil -} - -func (o *organization) ID() string { - return o.id -} - -func (o *organization) listHooks(ctx context.Context) ([]*github.Hook, error) { - opts := github.ListOptions{ - PerPage: 100, - } - var allHooks []*github.Hook - for { - metrics.GithubOperationCount.WithLabelValues( - "ListOrgHooks", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - hooks, ghResp, err := o.ghcli.ListOrgHooks(ctx, o.cfg.Name, &opts) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "ListOrgHooks", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - if ghResp != nil && ghResp.StatusCode == http.StatusNotFound { - return nil, runnerErrors.NewBadRequestError("organization not found or your PAT does not have access to manage webhooks") - } - return nil, errors.Wrap(err, "fetching hooks") - } - allHooks = append(allHooks, hooks...) - if ghResp.NextPage == 0 { - break - } - opts.Page = ghResp.NextPage - } - return allHooks, nil -} - -func (o *organization) InstallHook(ctx context.Context, req *github.Hook) (params.HookInfo, error) { - allHooks, err := o.listHooks(ctx) - if err != nil { - return params.HookInfo{}, errors.Wrap(err, "listing hooks") - } - - if err := validateHookRequest(o.cfgInternal.ControllerID, o.cfgInternal.BaseWebhookURL, allHooks, req); err != nil { - return params.HookInfo{}, errors.Wrap(err, "validating hook request") - } - - metrics.GithubOperationCount.WithLabelValues( - "CreateOrgHook", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - - hook, _, err := o.ghcli.CreateOrgHook(ctx, o.cfg.Name, req) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "CreateOrgHook", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - return params.HookInfo{}, errors.Wrap(err, "creating organization hook") - } - - metrics.GithubOperationCount.WithLabelValues( - "PingOrgHook", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - - if _, err := o.ghcli.PingOrgHook(ctx, o.cfg.Name, hook.GetID()); err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "PingOrgHook", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to ping hook", "hook_id", hook.GetID()) - } - - return hookToParamsHookInfo(hook), nil -} - -func (o *organization) UninstallHook(ctx context.Context, url string) error { - allHooks, err := o.listHooks(ctx) - if err != nil { - return errors.Wrap(err, "listing hooks") - } - - for _, hook := range allHooks { - if hook.Config["url"] == url { - metrics.GithubOperationCount.WithLabelValues( - "DeleteOrgHook", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - _, err = o.ghcli.DeleteOrgHook(ctx, o.cfg.Name, hook.GetID()) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "DeleteOrgHook", // label: operation - metricsLabelOrganizationScope, // label: scope - ).Inc() - return errors.Wrap(err, "deleting hook") - } - return nil - } - } - return nil -} - -func (o *organization) GetHookInfo(ctx context.Context) (params.HookInfo, error) { - allHooks, err := o.listHooks(ctx) - if err != nil { - return params.HookInfo{}, errors.Wrap(err, "listing hooks") - } - - for _, hook := range allHooks { - hookInfo := hookToParamsHookInfo(hook) - if strings.EqualFold(hookInfo.URL, o.cfgInternal.ControllerWebhookURL) { - return hookInfo, nil - } - } - - return params.HookInfo{}, runnerErrors.NewNotFoundError("hook not found") -} diff --git a/runner/pool/pool.go b/runner/pool/pool.go index f3b21852..a66bacf8 100644 --- a/runner/pool/pool.go +++ b/runner/pool/pool.go @@ -35,8 +35,10 @@ import ( "github.com/cloudbase/garm-provider-common/util" "github.com/cloudbase/garm/auth" dbCommon "github.com/cloudbase/garm/database/common" + "github.com/cloudbase/garm/metrics" "github.com/cloudbase/garm/params" "github.com/cloudbase/garm/runner/common" + garmUtil "github.com/cloudbase/garm/util" ) var ( @@ -58,10 +60,6 @@ const ( // nolint:golangci-lint,godox // TODO: make this configurable(?) maxCreateAttempts = 5 - - metricsLabelEnterpriseScope = "Enterprise" - metricsLabelRepositoryScope = "Repository" - metricsLabelOrganizationScope = "Organization" ) type keyMutex struct { @@ -96,9 +94,50 @@ type urls struct { webhookURL string controllerWebhookURL string } + +func NewEntityPoolManager(ctx context.Context, entity params.GithubEntity, cfgInternal params.Internal, providers map[string]common.Provider, store dbCommon.Store) (common.PoolManager, error) { + ctx = garmUtil.WithContext(ctx, slog.Any("pool_mgr", entity), slog.Any("pool_type", params.GithubEntityTypeRepository)) + ghc, err := garmUtil.GithubClient(ctx, entity, cfgInternal.GithubCredentialsDetails) + if err != nil { + return nil, errors.Wrap(err, "getting github client") + } + + if entity.WebhookSecret == "" { + return nil, errors.New("webhook secret is empty") + } + + wg := &sync.WaitGroup{} + keyMuxes := &keyMutex{} + + repo := &basePoolManager{ + ctx: ctx, + cfgInternal: cfgInternal, + entity: entity, + ghcli: ghc, + + store: store, + providers: providers, + controllerID: cfgInternal.ControllerID, + urls: urls{ + webhookURL: cfgInternal.BaseWebhookURL, + callbackURL: cfgInternal.InstanceCallbackURL, + metadataURL: cfgInternal.InstanceMetadataURL, + controllerWebhookURL: cfgInternal.ControllerWebhookURL, + }, + quit: make(chan struct{}), + credsDetails: cfgInternal.GithubCredentialsDetails, + wg: wg, + keyMux: keyMuxes, + } + return repo, nil +} + type basePoolManager struct { ctx context.Context controllerID string + entity params.GithubEntity + ghcli common.GithubClient + cfgInternal params.Internal store dbCommon.Store @@ -106,7 +145,6 @@ type basePoolManager struct { tools []commonParams.RunnerApplicationDownload quit chan struct{} - helper poolHelper credsDetails params.GithubCredentials managerIsRunning bool @@ -120,7 +158,7 @@ type basePoolManager struct { } func (r *basePoolManager) HandleWorkflowJob(job params.WorkflowJob) error { - if err := r.helper.ValidateOwner(job); err != nil { + if err := r.ValidateOwner(job); err != nil { return errors.Wrap(err, "validating owner") } @@ -145,7 +183,7 @@ func (r *basePoolManager) HandleWorkflowJob(job params.WorkflowJob) error { return } // This job is new to us. Check if we have a pool that can handle it. - potentialPools, err := r.store.FindPoolsMatchingAllTags(r.ctx, r.helper.PoolType(), r.helper.ID(), jobParams.Labels) + potentialPools, err := r.store.FindPoolsMatchingAllTags(r.ctx, r.entity.EntityType, r.entity.ID, jobParams.Labels) if err != nil { slog.With(slog.Any("error", err)).ErrorContext( r.ctx, "failed to find pools matching tags; not recording job", @@ -250,7 +288,7 @@ func (r *basePoolManager) HandleWorkflowJob(job params.WorkflowJob) error { // A runner has picked up the job, and is now running it. It may need to be replaced if the pool has // a minimum number of idle runners configured. - pool, err := r.store.GetPoolByID(r.ctx, instance.PoolID) + pool, err := r.GetPoolByID(instance.PoolID) if err != nil { return errors.Wrap(err, "getting pool") } @@ -332,12 +370,12 @@ func (r *basePoolManager) startLoopForFunction(f func() error, interval time.Dur func (r *basePoolManager) updateTools() error { // Update tools cache. - tools, err := r.helper.FetchTools() + tools, err := r.FetchTools() if err != nil { slog.With(slog.Any("error", err)).ErrorContext( r.ctx, "failed to update tools for repo") r.setPoolRunningState(false, err.Error()) - return fmt.Errorf("failed to update tools for repo %s: %w", r.helper.String(), err) + return fmt.Errorf("failed to update tools for repo %s: %w", r.entity.String(), err) } r.mux.Lock() r.tools = tools @@ -387,7 +425,7 @@ func (r *basePoolManager) isManagedRunner(labels []string) bool { // If we were offline and did not process the webhook, the instance will linger. // We need to remove it from the provider and database. func (r *basePoolManager) cleanupOrphanedProviderRunners(runners []*github.Runner) error { - dbInstances, err := r.helper.FetchDbInstances() + dbInstances, err := r.FetchDbInstances() if err != nil { return errors.Wrap(err, "fetching instances from db") } @@ -422,7 +460,7 @@ func (r *basePoolManager) cleanupOrphanedProviderRunners(runners []*github.Runne continue } - pool, err := r.store.GetPoolByID(r.ctx, instance.PoolID) + pool, err := r.GetPoolByID(instance.PoolID) if err != nil { return errors.Wrap(err, "fetching instance pool info") } @@ -463,7 +501,7 @@ func (r *basePoolManager) cleanupOrphanedProviderRunners(runners []*github.Runne // of "running" in the provider, but that has not registered with Github, and has // received no new updates in the configured timeout interval. func (r *basePoolManager) reapTimedOutRunners(runners []*github.Runner) error { - dbInstances, err := r.helper.FetchDbInstances() + dbInstances, err := r.FetchDbInstances() if err != nil { return errors.Wrap(err, "fetching instances from db") } @@ -492,7 +530,7 @@ func (r *basePoolManager) reapTimedOutRunners(runners []*github.Runner) error { } defer r.keyMux.Unlock(instance.Name, false) - pool, err := r.store.GetPoolByID(r.ctx, instance.PoolID) + pool, err := r.GetPoolByID(instance.PoolID) if err != nil { return errors.Wrap(err, "fetching instance pool info") } @@ -560,7 +598,7 @@ func (r *basePoolManager) cleanupOrphanedGithubRunners(runners []*github.Runner) slog.InfoContext( r.ctx, "Runner has no database entry in garm, removing from github", "runner_name", runner.GetName()) - resp, err := r.helper.RemoveGithubRunner(*runner.ID) + resp, err := r.RemoveGithubRunner(*runner.ID) if err != nil { // Removed in the meantime? if resp != nil && resp.StatusCode == http.StatusNotFound { @@ -594,7 +632,7 @@ func (r *basePoolManager) cleanupOrphanedGithubRunners(runners []*github.Runner) } } - pool, err := r.helper.GetPoolByID(dbInstance.PoolID) + pool, err := r.GetPoolByID(dbInstance.PoolID) if err != nil { return errors.Wrap(err, "fetching pool") } @@ -640,7 +678,7 @@ func (r *basePoolManager) cleanupOrphanedGithubRunners(runners []*github.Runner) slog.InfoContext( r.ctx, "Runner instance is no longer on the provider, removing from github", "runner_name", dbInstance.Name) - resp, err := r.helper.RemoveGithubRunner(*runner.ID) + resp, err := r.RemoveGithubRunner(*runner.ID) if err != nil { // Removed in the meantime? if resp != nil && resp.StatusCode == http.StatusNotFound { @@ -713,7 +751,7 @@ func (r *basePoolManager) fetchInstance(runnerName string) (params.Instance, err return params.Instance{}, errors.Wrap(err, "fetching instance") } - _, err = r.helper.GetPoolByID(runner.PoolID) + _, err = r.GetPoolByID(runner.PoolID) if err != nil { return params.Instance{}, errors.Wrap(err, "fetching pool") } @@ -760,7 +798,7 @@ func (r *basePoolManager) setInstanceStatus(runnerName string, status commonPara } func (r *basePoolManager) AddRunner(ctx context.Context, poolID string, aditionalLabels []string) (err error) { - pool, err := r.helper.GetPoolByID(poolID) + pool, err := r.GetPoolByID(poolID) if err != nil { return errors.Wrap(err, "fetching pool") } @@ -778,7 +816,7 @@ func (r *basePoolManager) AddRunner(ctx context.Context, poolID string, aditiona if !provider.DisableJITConfig() { // Attempt to create JIT config - jitConfig, runner, err = r.helper.GetJITConfig(ctx, name, pool, labels) + jitConfig, runner, err = r.ghcli.GetEntityJITConfig(ctx, name, pool, labels) if err != nil { slog.With(slog.Any("error", err)).ErrorContext( ctx, "failed to get JIT config, falling back to registration token") @@ -819,7 +857,7 @@ func (r *basePoolManager) AddRunner(ctx context.Context, poolID string, aditiona } if runner != nil { - _, runnerCleanupErr := r.helper.RemoveGithubRunner(runner.GetID()) + _, runnerCleanupErr := r.RemoveGithubRunner(runner.GetID()) if err != nil { slog.With(slog.Any("error", runnerCleanupErr)).ErrorContext( ctx, "failed to remove runner", @@ -869,7 +907,7 @@ func (r *basePoolManager) getLabelsForInstance(pool params.Pool) []string { } func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error { - pool, err := r.helper.GetPoolByID(instance.PoolID) + pool, err := r.GetPoolByID(instance.PoolID) if err != nil { return errors.Wrap(err, "fetching pool") } @@ -881,8 +919,8 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error jwtValidity := pool.RunnerTimeout() - entity := r.helper.String() - jwtToken, err := auth.NewInstanceJWTToken(instance, r.helper.JwtToken(), entity, pool.PoolType(), jwtValidity) + entity := r.entity.String() + jwtToken, err := auth.NewInstanceJWTToken(instance, r.cfgInternal.JWTSecret, entity, pool.PoolType(), jwtValidity) if err != nil { return errors.Wrap(err, "fetching instance jwt token") } @@ -892,7 +930,7 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error bootstrapArgs := commonParams.BootstrapInstance{ Name: instance.Name, Tools: r.tools, - RepoURL: r.helper.GithubURL(), + RepoURL: r.GithubURL(), MetadataURL: instance.MetadataURL, CallbackURL: instance.CallbackURL, InstanceToken: jwtToken, @@ -965,7 +1003,7 @@ func (r *basePoolManager) getRunnerDetailsFromJob(job params.WorkflowJob) (param slog.InfoContext( r.ctx, "runner name not found in workflow job, attempting to fetch from API", "job_id", job.WorkflowJob.ID) - runnerInfo, err = r.helper.GetRunnerInfoFromWorkflow(job) + runnerInfo, err = r.GetRunnerInfoFromWorkflow(job) if err != nil { return params.RunnerInfo{}, errors.Wrap(err, "fetching runner name from API") } @@ -1035,15 +1073,15 @@ func (r *basePoolManager) paramsWorkflowJobToParamsJob(job params.WorkflowJob) ( jobParams.RunnerName = runnerName - switch r.helper.PoolType() { - case params.EnterprisePool: + switch r.entity.EntityType { + case params.GithubEntityTypeEnterprise: jobParams.EnterpriseID = &asUUID - case params.RepositoryPool: + case params.GithubEntityTypeRepository: jobParams.RepoID = &asUUID - case params.OrganizationPool: + case params.GithubEntityTypeOrganization: jobParams.OrgID = &asUUID default: - return jobParams, errors.Errorf("unknown pool type: %s", r.helper.PoolType()) + return jobParams, errors.Errorf("unknown pool type: %s", r.entity.EntityType) } return jobParams, nil @@ -1147,7 +1185,7 @@ func (r *basePoolManager) scaleDownOnePool(ctx context.Context, pool params.Pool // nolint:golangci-lint,godox // TODO: should probably allow aditional filters to list functions. Would help to filter by date // instead of returning a bunch of results and filtering manually. - queued, err := r.store.ListEntityJobsByStatus(r.ctx, r.helper.PoolType(), r.helper.ID(), params.JobStatusQueued) + queued, err := r.store.ListEntityJobsByStatus(r.ctx, r.entity.EntityType, r.entity.ID, params.JobStatusQueued) if err != nil && !errors.Is(err, runnerErrors.ErrNotFound) { return errors.Wrap(err, "listing queued jobs") } @@ -1328,7 +1366,7 @@ func (r *basePoolManager) retryFailedInstancesForOnePool(ctx context.Context, po } func (r *basePoolManager) retryFailedInstances() error { - pools, err := r.helper.ListPools() + pools, err := r.ListPools() if err != nil { return fmt.Errorf("error listing pools: %w", err) } @@ -1351,7 +1389,7 @@ func (r *basePoolManager) retryFailedInstances() error { } func (r *basePoolManager) scaleDown() error { - pools, err := r.helper.ListPools() + pools, err := r.ListPools() if err != nil { return fmt.Errorf("error listing pools: %w", err) } @@ -1372,7 +1410,7 @@ func (r *basePoolManager) scaleDown() error { } func (r *basePoolManager) ensureMinIdleRunners() error { - pools, err := r.helper.ListPools() + pools, err := r.ListPools() if err != nil { return fmt.Errorf("error listing pools: %w", err) } @@ -1392,7 +1430,7 @@ func (r *basePoolManager) ensureMinIdleRunners() error { } func (r *basePoolManager) deleteInstanceFromProvider(ctx context.Context, instance params.Instance) error { - pool, err := r.helper.GetPoolByID(instance.PoolID) + pool, err := r.GetPoolByID(instance.PoolID) if err != nil { return errors.Wrap(err, "fetching pool") } @@ -1422,7 +1460,7 @@ func (r *basePoolManager) deleteInstanceFromProvider(ctx context.Context, instan } func (r *basePoolManager) deletePendingInstances() error { - instances, err := r.helper.FetchDbInstances() + instances, err := r.FetchDbInstances() if err != nil { return fmt.Errorf("failed to fetch instances from store: %w", err) } @@ -1510,7 +1548,7 @@ func (r *basePoolManager) deletePendingInstances() error { func (r *basePoolManager) addPendingInstances() error { // nolint:golangci-lint,godox // TODO: filter instances by status. - instances, err := r.helper.FetchDbInstances() + instances, err := r.FetchDbInstances() if err != nil { return fmt.Errorf("failed to fetch instances from store: %w", err) } @@ -1587,7 +1625,7 @@ func (r *basePoolManager) Wait() error { func (r *basePoolManager) runnerCleanup() (err error) { slog.DebugContext( r.ctx, "running runner cleanup") - runners, err := r.helper.GetGithubRunners() + runners, err := r.GetGithubRunners() if err != nil { return fmt.Errorf("failed to fetch github runners: %w", err) } @@ -1653,9 +1691,28 @@ func (r *basePoolManager) Stop() error { } func (r *basePoolManager) RefreshState(param params.UpdatePoolStateParams) error { - if err := r.helper.UpdateState(param); err != nil { - return fmt.Errorf("failed to update pool state: %w", err) + r.mux.Lock() + + if param.WebhookSecret != "" { + r.entity.WebhookSecret = param.WebhookSecret + } + if param.InternalConfig != nil { + r.cfgInternal = *param.InternalConfig + r.urls = urls{ + webhookURL: r.cfgInternal.BaseWebhookURL, + callbackURL: r.cfgInternal.InstanceCallbackURL, + metadataURL: r.cfgInternal.InstanceMetadataURL, + controllerWebhookURL: r.cfgInternal.ControllerWebhookURL, + } } + + ghc, err := garmUtil.GithubClient(r.ctx, r.entity, r.cfgInternal.GithubCredentialsDetails) + if err != nil { + return errors.Wrap(err, "getting github client") + } + r.ghcli = ghc + r.mux.Unlock() + // Update the tools as soon as state is updated. This should revive a stopped pool manager // or stop one if the supplied credentials are not okay. if err := r.updateTools(); err != nil { @@ -1665,25 +1722,25 @@ func (r *basePoolManager) RefreshState(param params.UpdatePoolStateParams) error } func (r *basePoolManager) WebhookSecret() string { - return r.helper.WebhookSecret() + return r.entity.WebhookSecret } func (r *basePoolManager) GithubRunnerRegistrationToken() (string, error) { - return r.helper.GetGithubRegistrationToken() + return r.GetGithubRegistrationToken() } func (r *basePoolManager) ID() string { - return r.helper.ID() + return r.entity.ID } // Delete runner will delete a runner from a pool. If forceRemove is set to true, any error received from // the IaaS provider will be ignored and deletion will continue. func (r *basePoolManager) DeleteRunner(runner params.Instance, forceRemove, bypassGHUnauthorizedError bool) error { if !r.managerIsRunning && !bypassGHUnauthorizedError { - return runnerErrors.NewConflictError("pool manager is not running for %s", r.helper.String()) + return runnerErrors.NewConflictError("pool manager is not running for %s", r.entity.String()) } if runner.AgentID != 0 { - resp, err := r.helper.RemoveGithubRunner(runner.AgentID) + resp, err := r.RemoveGithubRunner(runner.AgentID) if err != nil { if resp != nil { switch resp.StatusCode { @@ -1767,13 +1824,13 @@ func (r *basePoolManager) DeleteRunner(runner params.Instance, forceRemove, bypa // so those will trigger the creation of a runner. The jobs we don't know about will be dealt with by the idle runners. // Once jobs are consumed, you can set min-idle-runners to 0 again. func (r *basePoolManager) consumeQueuedJobs() error { - queued, err := r.store.ListEntityJobsByStatus(r.ctx, r.helper.PoolType(), r.helper.ID(), params.JobStatusQueued) + queued, err := r.store.ListEntityJobsByStatus(r.ctx, r.entity.EntityType, r.entity.ID, params.JobStatusQueued) if err != nil { return errors.Wrap(err, "listing queued jobs") } poolsCache := poolsForTags{ - poolCacheType: r.helper.PoolBalancerType(), + poolCacheType: r.PoolBalancerType(), } slog.DebugContext( @@ -1825,7 +1882,7 @@ func (r *basePoolManager) consumeQueuedJobs() error { poolRR, ok := poolsCache.Get(job.Labels) if !ok { - potentialPools, err := r.store.FindPoolsMatchingAllTags(r.ctx, r.helper.PoolType(), r.helper.ID(), job.Labels) + potentialPools, err := r.store.FindPoolsMatchingAllTags(r.ctx, r.entity.EntityType, r.entity.ID, job.Labels) if err != nil { slog.With(slog.Any("error", err)).ErrorContext( r.ctx, "error finding pools matching labels") @@ -1896,6 +1953,73 @@ func (r *basePoolManager) consumeQueuedJobs() error { return nil } +func (r *basePoolManager) UninstallWebhook(ctx context.Context) error { + if r.urls.controllerWebhookURL == "" { + return errors.Wrap(runnerErrors.ErrBadRequest, "controller webhook url is empty") + } + + allHooks, err := r.listHooks(ctx) + if err != nil { + return errors.Wrap(err, "listing hooks") + } + + var controllerHookID int64 + var baseHook string + trimmedBase := strings.TrimRight(r.urls.webhookURL, "/") + trimmedController := strings.TrimRight(r.urls.controllerWebhookURL, "/") + + for _, hook := range allHooks { + hookInfo := hookToParamsHookInfo(hook) + info := strings.TrimRight(hookInfo.URL, "/") + if strings.EqualFold(info, trimmedController) { + controllerHookID = hook.GetID() + } + + if strings.EqualFold(info, trimmedBase) { + baseHook = hookInfo.URL + } + } + + if controllerHookID != 0 { + _, err = r.ghcli.DeleteEntityHook(ctx, controllerHookID) + if err != nil { + return fmt.Errorf("deleting hook: %w", err) + } + return nil + } + + if baseHook != "" { + return runnerErrors.NewBadRequestError("base hook found (%s) and must be deleted manually", baseHook) + } + + return nil +} + +func (r *basePoolManager) InstallHook(ctx context.Context, req *github.Hook) (params.HookInfo, error) { + allHooks, err := r.listHooks(ctx) + if err != nil { + return params.HookInfo{}, errors.Wrap(err, "listing hooks") + } + + if err := validateHookRequest(r.cfgInternal.ControllerID, r.cfgInternal.BaseWebhookURL, allHooks, req); err != nil { + return params.HookInfo{}, errors.Wrap(err, "validating hook request") + } + + hook, err := r.ghcli.CreateEntityHook(ctx, req) + if err != nil { + return params.HookInfo{}, errors.Wrap(err, "creating entity hook") + } + + if _, err := r.ghcli.PingEntityHook(ctx, hook.GetID()); err != nil { + slog.With(slog.Any("error", err)).ErrorContext( + ctx, "failed to ping hook", + "hook_id", hook.GetID(), + "entity", r.entity) + } + + return hookToParamsHookInfo(hook), nil +} + func (r *basePoolManager) InstallWebhook(ctx context.Context, param params.InstallWebhookParams) (params.HookInfo, error) { if r.urls.controllerWebhookURL == "" { return params.HookInfo{}, errors.Wrap(runnerErrors.ErrBadRequest, "controller webhook url is empty") @@ -1918,19 +2042,212 @@ func (r *basePoolManager) InstallWebhook(ctx context.Context, param params.Insta }, } - return r.helper.InstallHook(ctx, req) + return r.InstallHook(ctx, req) } -func (r *basePoolManager) UninstallWebhook(ctx context.Context) error { - if r.urls.controllerWebhookURL == "" { - return errors.Wrap(runnerErrors.ErrBadRequest, "controller webhook url is empty") +func (r *basePoolManager) ValidateOwner(job params.WorkflowJob) error { + switch r.entity.EntityType { + case params.GithubEntityTypeRepository: + if !strings.EqualFold(job.Repository.Name, r.entity.Name) || !strings.EqualFold(job.Repository.Owner.Login, r.entity.Owner) { + return runnerErrors.NewBadRequestError("job not meant for this pool manager") + } + case params.GithubEntityTypeOrganization: + if !strings.EqualFold(job.Organization.Login, r.entity.Owner) { + return runnerErrors.NewBadRequestError("job not meant for this pool manager") + } + case params.GithubEntityTypeEnterprise: + if !strings.EqualFold(job.Enterprise.Slug, r.entity.Owner) { + return runnerErrors.NewBadRequestError("job not meant for this pool manager") + } + default: + return runnerErrors.NewBadRequestError("unknown entity type") + } + + return nil +} + +func (r *basePoolManager) GetRunnerInfoFromWorkflow(job params.WorkflowJob) (params.RunnerInfo, error) { + if err := r.ValidateOwner(job); err != nil { + return params.RunnerInfo{}, errors.Wrap(err, "validating owner") + } + metrics.GithubOperationCount.WithLabelValues( + "GetWorkflowJobByID", // label: operation + r.entity.LabelScope(), // label: scope + ).Inc() + workflow, ghResp, err := r.ghcli.GetWorkflowJobByID(r.ctx, job.Repository.Owner.Login, job.Repository.Name, job.WorkflowJob.ID) + if err != nil { + metrics.GithubOperationFailedCount.WithLabelValues( + "GetWorkflowJobByID", // label: operation + r.entity.LabelScope(), // label: scope + ).Inc() + if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { + return params.RunnerInfo{}, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching workflow info") + } + return params.RunnerInfo{}, errors.Wrap(err, "fetching workflow info") + } + + if workflow.RunnerName != nil { + return params.RunnerInfo{ + Name: *workflow.RunnerName, + Labels: workflow.Labels, + }, nil + } + return params.RunnerInfo{}, fmt.Errorf("failed to find runner name from workflow") +} + +func (r *basePoolManager) GetGithubRegistrationToken() (string, error) { + tk, ghResp, err := r.ghcli.CreateEntityRegistrationToken(r.ctx) + if err != nil { + if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { + return "", errors.Wrap(runnerErrors.ErrUnauthorized, "fetching token") + } + return "", errors.Wrap(err, "creating runner token") + } + return *tk.Token, nil +} + +func (r *basePoolManager) RemoveGithubRunner(runnerID int64) (*github.Response, error) { + ghResp, err := r.ghcli.RemoveEntityRunner(r.ctx, runnerID) + if err != nil { + return nil, fmt.Errorf("removing runner: %w", err) + } + return ghResp, nil +} + +func (r *basePoolManager) FetchTools() ([]commonParams.RunnerApplicationDownload, error) { + tools, ghResp, err := r.ghcli.ListEntityRunnerApplicationDownloads(r.ctx) + if err != nil { + if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { + return nil, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching tools") + } + return nil, errors.Wrap(err, "fetching runner tools") + } + + ret := []commonParams.RunnerApplicationDownload{} + for _, tool := range tools { + if tool == nil { + continue + } + ret = append(ret, commonParams.RunnerApplicationDownload(*tool)) + } + return ret, nil +} + +func (r *basePoolManager) GetGithubRunners() ([]*github.Runner, error) { + opts := github.ListOptions{ + PerPage: 100, + } + var allRunners []*github.Runner + + for { + runners, ghResp, err := r.ghcli.ListEntityRunners(r.ctx, &opts) + if err != nil { + if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { + return nil, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching runners") + } + return nil, errors.Wrap(err, "fetching runners") + } + allRunners = append(allRunners, runners.Runners...) + if ghResp.NextPage == 0 { + break + } + opts.Page = ghResp.NextPage + } + + return allRunners, nil +} + +func (r *basePoolManager) PoolBalancerType() params.PoolBalancerType { + if r.cfgInternal.PoolBalancerType == "" { + return params.PoolBalancerTypeRoundRobin + } + return r.cfgInternal.PoolBalancerType +} + +func (r *basePoolManager) GithubURL() string { + switch r.entity.EntityType { + case params.GithubEntityTypeRepository: + return fmt.Sprintf("%s/%s/%s", r.cfgInternal.GithubCredentialsDetails.BaseURL, r.entity.Owner, r.entity.Name) + case params.GithubEntityTypeOrganization: + return fmt.Sprintf("%s/%s", r.cfgInternal.GithubCredentialsDetails.BaseURL, r.entity.Owner) + case params.GithubEntityTypeEnterprise: + return fmt.Sprintf("%s/enterprises/%s", r.cfgInternal.GithubCredentialsDetails.BaseURL, r.entity.Owner) + } + return "" +} + +func (r *basePoolManager) FetchDbInstances() ([]params.Instance, error) { + switch r.entity.EntityType { + case params.GithubEntityTypeRepository: + return r.store.ListRepoInstances(r.ctx, r.entity.ID) + case params.GithubEntityTypeOrganization: + return r.store.ListOrgInstances(r.ctx, r.entity.ID) + case params.GithubEntityTypeEnterprise: + return r.store.ListEnterpriseInstances(r.ctx, r.entity.ID) + } + return nil, fmt.Errorf("unknown entity type: %s", r.entity.EntityType) +} + +func (r *basePoolManager) ListPools() ([]params.Pool, error) { + switch r.entity.EntityType { + case params.GithubEntityTypeRepository: + return r.store.ListRepoPools(r.ctx, r.entity.ID) + case params.GithubEntityTypeOrganization: + return r.store.ListOrgPools(r.ctx, r.entity.ID) + case params.GithubEntityTypeEnterprise: + return r.store.ListEnterprisePools(r.ctx, r.entity.ID) + default: + return nil, fmt.Errorf("unknown entity type: %s", r.entity.EntityType) } +} - return r.helper.UninstallHook(ctx, r.urls.controllerWebhookURL) +func (r *basePoolManager) GetPoolByID(poolID string) (params.Pool, error) { + switch r.entity.EntityType { + case params.GithubEntityTypeRepository: + return r.store.GetRepositoryPool(r.ctx, r.entity.ID, poolID) + case params.GithubEntityTypeOrganization: + return r.store.GetOrganizationPool(r.ctx, r.entity.ID, poolID) + case params.GithubEntityTypeEnterprise: + return r.store.GetEnterprisePool(r.ctx, r.entity.ID, poolID) + default: + return params.Pool{}, fmt.Errorf("unknown entity type: %s", r.entity.EntityType) + } } func (r *basePoolManager) GetWebhookInfo(ctx context.Context) (params.HookInfo, error) { - return r.helper.GetHookInfo(ctx) + allHooks, err := r.listHooks(ctx) + if err != nil { + return params.HookInfo{}, errors.Wrap(err, "listing hooks") + } + trimmedBase := strings.TrimRight(r.urls.webhookURL, "/") + trimmedController := strings.TrimRight(r.urls.controllerWebhookURL, "/") + + var controllerHookInfo *params.HookInfo + var baseHookInfo *params.HookInfo + + for _, hook := range allHooks { + hookInfo := hookToParamsHookInfo(hook) + info := strings.TrimRight(hookInfo.URL, "/") + if strings.EqualFold(info, trimmedController) { + controllerHookInfo = &hookInfo + break + } + if strings.EqualFold(info, trimmedBase) { + baseHookInfo = &hookInfo + } + } + + // Return the controller hook info if available. + if controllerHookInfo != nil { + return *controllerHookInfo, nil + } + + // Fall back to base hook info if defined. + if baseHookInfo != nil { + return *baseHookInfo, nil + } + + return params.HookInfo{}, runnerErrors.NewNotFoundError("hook not found") } func (r *basePoolManager) RootCABundle() (params.CertificateBundle, error) { diff --git a/runner/pool/repository.go b/runner/pool/repository.go deleted file mode 100644 index 3383aacf..00000000 --- a/runner/pool/repository.go +++ /dev/null @@ -1,472 +0,0 @@ -// Copyright 2022 Cloudbase Solutions SRL -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package pool - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "log/slog" - "net/http" - "strings" - "sync" - - "github.com/google/go-github/v57/github" - "github.com/pkg/errors" - - runnerErrors "github.com/cloudbase/garm-provider-common/errors" - commonParams "github.com/cloudbase/garm-provider-common/params" - dbCommon "github.com/cloudbase/garm/database/common" - "github.com/cloudbase/garm/metrics" - "github.com/cloudbase/garm/params" - "github.com/cloudbase/garm/runner/common" - "github.com/cloudbase/garm/util" -) - -// test that we implement PoolManager -var _ poolHelper = &repository{} - -func NewRepositoryPoolManager(ctx context.Context, cfg params.Repository, cfgInternal params.Internal, providers map[string]common.Provider, store dbCommon.Store) (common.PoolManager, error) { - ctx = util.WithContext(ctx, slog.Any("pool_mgr", fmt.Sprintf("%s/%s", cfg.Owner, cfg.Name)), slog.Any("pool_type", params.RepositoryPool)) - ghc, _, err := util.GithubClient(ctx, cfgInternal.GithubCredentialsDetails) - if err != nil { - return nil, errors.Wrap(err, "getting github client") - } - - wg := &sync.WaitGroup{} - keyMuxes := &keyMutex{} - - helper := &repository{ - cfg: cfg, - cfgInternal: cfgInternal, - ctx: ctx, - ghcli: ghc, - id: cfg.ID, - store: store, - } - - repo := &basePoolManager{ - ctx: ctx, - store: store, - providers: providers, - controllerID: cfgInternal.ControllerID, - urls: urls{ - webhookURL: cfgInternal.BaseWebhookURL, - callbackURL: cfgInternal.InstanceCallbackURL, - metadataURL: cfgInternal.InstanceMetadataURL, - controllerWebhookURL: cfgInternal.ControllerWebhookURL, - }, - quit: make(chan struct{}), - helper: helper, - credsDetails: cfgInternal.GithubCredentialsDetails, - wg: wg, - keyMux: keyMuxes, - } - return repo, nil -} - -var _ poolHelper = &repository{} - -type repository struct { - cfg params.Repository - cfgInternal params.Internal - ctx context.Context - ghcli common.GithubClient - id string - store dbCommon.Store - - mux sync.Mutex -} - -func (r *repository) PoolBalancerType() params.PoolBalancerType { - if r.cfgInternal.PoolBalancerType == "" { - return params.PoolBalancerTypeRoundRobin - } - return r.cfgInternal.PoolBalancerType -} - -// nolint:golint,revive -// pool is used in enterprise and organzation -func (r *repository) GetJITConfig(ctx context.Context, instance string, pool params.Pool, labels []string) (jitConfigMap map[string]string, runner *github.Runner, err error) { - req := github.GenerateJITConfigRequest{ - Name: instance, - // At the repository level we only have the default runner group. - RunnerGroupID: 1, - Labels: labels, - // nolint:golangci-lint,godox - // TODO(gabriel-samfira): Should we make this configurable? - WorkFolder: github.String("_work"), - } - metrics.GithubOperationCount.WithLabelValues( - "GenerateRepoJITConfig", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - jitConfig, resp, err := r.ghcli.GenerateRepoJITConfig(ctx, r.cfg.Owner, r.cfg.Name, &req) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "GenerateRepoJITConfig", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - if resp != nil && resp.StatusCode == http.StatusUnauthorized { - return nil, nil, fmt.Errorf("failed to get JIT config: %w", err) - } - return nil, nil, fmt.Errorf("failed to get JIT config: %w", err) - } - runner = jitConfig.Runner - - defer func() { - if err != nil && runner != nil { - metrics.GithubOperationCount.WithLabelValues( - "RemoveRunner", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - _, innerErr := r.ghcli.RemoveRunner(r.ctx, r.cfg.Owner, r.cfg.Name, runner.GetID()) - if innerErr != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "RemoveRunner", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - } - slog.With(slog.Any("error", innerErr)).ErrorContext( - ctx, "failed to remove runner", - "runner_id", runner.GetID(), - "repo", r.cfg.Name, - "owner", r.cfg.Owner) - } - }() - - decoded, err := base64.StdEncoding.DecodeString(jitConfig.GetEncodedJITConfig()) - if err != nil { - return nil, nil, fmt.Errorf("failed to decode JIT config: %w", err) - } - - var ret map[string]string - if err := json.Unmarshal(decoded, &ret); err != nil { - return nil, nil, fmt.Errorf("failed to unmarshal JIT config: %w", err) - } - - return ret, runner, nil -} - -func (r *repository) PoolType() params.PoolType { - return params.RepositoryPool -} - -func (r *repository) GetRunnerInfoFromWorkflow(job params.WorkflowJob) (params.RunnerInfo, error) { - if err := r.ValidateOwner(job); err != nil { - return params.RunnerInfo{}, errors.Wrap(err, "validating owner") - } - metrics.GithubOperationCount.WithLabelValues( - "GetWorkflowJobByID", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - workflow, ghResp, err := r.ghcli.GetWorkflowJobByID(r.ctx, job.Repository.Owner.Login, job.Repository.Name, job.WorkflowJob.ID) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "GetWorkflowJobByID", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { - return params.RunnerInfo{}, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching workflow info") - } - return params.RunnerInfo{}, errors.Wrap(err, "fetching workflow info") - } - - if workflow.RunnerName != nil { - return params.RunnerInfo{ - Name: *workflow.RunnerName, - Labels: workflow.Labels, - }, nil - } - return params.RunnerInfo{}, fmt.Errorf("failed to find runner name from workflow") -} - -func (r *repository) UpdateState(param params.UpdatePoolStateParams) error { - r.mux.Lock() - defer r.mux.Unlock() - - r.cfg.WebhookSecret = param.WebhookSecret - if param.InternalConfig != nil { - r.cfgInternal = *param.InternalConfig - } - - ghc, _, err := util.GithubClient(r.ctx, r.cfgInternal.GithubCredentialsDetails) - if err != nil { - return errors.Wrap(err, "getting github client") - } - r.ghcli = ghc - return nil -} - -func (r *repository) GetGithubRunners() ([]*github.Runner, error) { - opts := github.ListOptions{ - PerPage: 100, - } - - var allRunners []*github.Runner - for { - metrics.GithubOperationCount.WithLabelValues( - "ListRunners", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - runners, ghResp, err := r.ghcli.ListRunners(r.ctx, r.cfg.Owner, r.cfg.Name, &opts) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "ListRunners", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { - return nil, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching runners") - } - return nil, errors.Wrap(err, "fetching runners") - } - allRunners = append(allRunners, runners.Runners...) - if ghResp.NextPage == 0 { - break - } - opts.Page = ghResp.NextPage - } - - return allRunners, nil -} - -func (r *repository) FetchTools() ([]commonParams.RunnerApplicationDownload, error) { - r.mux.Lock() - defer r.mux.Unlock() - metrics.GithubOperationCount.WithLabelValues( - "ListRunnerApplicationDownloads", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - tools, ghResp, err := r.ghcli.ListRunnerApplicationDownloads(r.ctx, r.cfg.Owner, r.cfg.Name) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "ListRunnerApplicationDownloads", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { - return nil, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching tools") - } - return nil, errors.Wrap(err, "fetching runner tools") - } - - ret := []commonParams.RunnerApplicationDownload{} - for _, tool := range tools { - if tool == nil { - continue - } - ret = append(ret, commonParams.RunnerApplicationDownload(*tool)) - } - - return ret, nil -} - -func (r *repository) FetchDbInstances() ([]params.Instance, error) { - return r.store.ListRepoInstances(r.ctx, r.id) -} - -func (r *repository) RemoveGithubRunner(runnerID int64) (*github.Response, error) { - metrics.GithubOperationCount.WithLabelValues( - "RemoveRunner", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - ghResp, err := r.ghcli.RemoveRunner(r.ctx, r.cfg.Owner, r.cfg.Name, runnerID) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "RemoveRunner", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - return nil, err - } - - return ghResp, nil -} - -func (r *repository) ListPools() ([]params.Pool, error) { - pools, err := r.store.ListRepoPools(r.ctx, r.id) - if err != nil { - return nil, errors.Wrap(err, "fetching pools") - } - return pools, nil -} - -func (r *repository) GithubURL() string { - return fmt.Sprintf("%s/%s/%s", r.cfgInternal.GithubCredentialsDetails.BaseURL, r.cfg.Owner, r.cfg.Name) -} - -func (r *repository) JwtToken() string { - return r.cfgInternal.JWTSecret -} - -func (r *repository) GetGithubRegistrationToken() (string, error) { - metrics.GithubOperationCount.WithLabelValues( - "CreateRegistrationToken", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - tk, ghResp, err := r.ghcli.CreateRegistrationToken(r.ctx, r.cfg.Owner, r.cfg.Name) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "CreateRegistrationToken", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { - return "", errors.Wrap(runnerErrors.ErrUnauthorized, "fetching token") - } - return "", errors.Wrap(err, "creating runner token") - } - return *tk.Token, nil -} - -func (r *repository) String() string { - return fmt.Sprintf("%s/%s", r.cfg.Owner, r.cfg.Name) -} - -func (r *repository) WebhookSecret() string { - return r.cfg.WebhookSecret -} - -func (r *repository) GetPoolByID(poolID string) (params.Pool, error) { - pool, err := r.store.GetRepositoryPool(r.ctx, r.id, poolID) - if err != nil { - return params.Pool{}, errors.Wrap(err, "fetching pool") - } - return pool, nil -} - -func (r *repository) ValidateOwner(job params.WorkflowJob) error { - if !strings.EqualFold(job.Repository.Name, r.cfg.Name) || !strings.EqualFold(job.Repository.Owner.Login, r.cfg.Owner) { - return runnerErrors.NewBadRequestError("job not meant for this pool manager") - } - return nil -} - -func (r *repository) ID() string { - return r.id -} - -func (r *repository) listHooks(ctx context.Context) ([]*github.Hook, error) { - opts := github.ListOptions{ - PerPage: 100, - } - var allHooks []*github.Hook - for { - metrics.GithubOperationCount.WithLabelValues( - "ListRepoHooks", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - hooks, ghResp, err := r.ghcli.ListRepoHooks(ctx, r.cfg.Owner, r.cfg.Name, &opts) - if err != nil { - metrics.GithubOperationCount.WithLabelValues( - "ListRepoHooks", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - if ghResp != nil && ghResp.StatusCode == http.StatusNotFound { - return nil, runnerErrors.NewBadRequestError("repository not found or your PAT does not have access to manage webhooks") - } - return nil, errors.Wrap(err, "fetching hooks") - } - allHooks = append(allHooks, hooks...) - if ghResp.NextPage == 0 { - break - } - opts.Page = ghResp.NextPage - } - return allHooks, nil -} - -func (r *repository) InstallHook(ctx context.Context, req *github.Hook) (params.HookInfo, error) { - allHooks, err := r.listHooks(ctx) - if err != nil { - return params.HookInfo{}, errors.Wrap(err, "listing hooks") - } - - if err := validateHookRequest(r.cfgInternal.ControllerID, r.cfgInternal.BaseWebhookURL, allHooks, req); err != nil { - return params.HookInfo{}, errors.Wrap(err, "validating hook request") - } - - metrics.GithubOperationCount.WithLabelValues( - "CreateRepoHook", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - - hook, _, err := r.ghcli.CreateRepoHook(ctx, r.cfg.Owner, r.cfg.Name, req) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "CreateRepoHook", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - return params.HookInfo{}, errors.Wrap(err, "creating repository hook") - } - - metrics.GithubOperationCount.WithLabelValues( - "PingRepoHook", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - - if _, err := r.ghcli.PingRepoHook(ctx, r.cfg.Owner, r.cfg.Name, hook.GetID()); err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "PingRepoHook", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - slog.With(slog.Any("error", err)).ErrorContext( - ctx, "failed to ping hook", - "hook_id", hook.GetID(), - "repo", r.cfg.Name, - "owner", r.cfg.Owner) - } - - return hookToParamsHookInfo(hook), nil -} - -func (r *repository) UninstallHook(ctx context.Context, url string) error { - allHooks, err := r.listHooks(ctx) - if err != nil { - return errors.Wrap(err, "listing hooks") - } - - for _, hook := range allHooks { - if hook.Config["url"] == url { - metrics.GithubOperationCount.WithLabelValues( - "DeleteRepoHook", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - _, err = r.ghcli.DeleteRepoHook(ctx, r.cfg.Owner, r.cfg.Name, hook.GetID()) - if err != nil { - metrics.GithubOperationFailedCount.WithLabelValues( - "DeleteRepoHook", // label: operation - metricsLabelRepositoryScope, // label: scope - ).Inc() - return errors.Wrap(err, "deleting hook") - } - return nil - } - } - return nil -} - -func (r *repository) GetHookInfo(ctx context.Context) (params.HookInfo, error) { - allHooks, err := r.listHooks(ctx) - if err != nil { - return params.HookInfo{}, errors.Wrap(err, "listing hooks") - } - - for _, hook := range allHooks { - hookInfo := hookToParamsHookInfo(hook) - if strings.EqualFold(hookInfo.URL, r.cfgInternal.ControllerWebhookURL) { - return hookInfo, nil - } - } - return params.HookInfo{}, runnerErrors.NewNotFoundError("hook not found") -} diff --git a/runner/runner.go b/runner/runner.go index df31ffa7..7eab27f9 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -109,7 +109,14 @@ func (p *poolManagerCtrl) CreateRepoPoolManager(ctx context.Context, repo params if err != nil { return nil, errors.Wrap(err, "fetching internal config") } - poolManager, err := pool.NewRepositoryPoolManager(ctx, repo, cfgInternal, providers, store) + entity := params.GithubEntity{ + Owner: repo.Owner, + Name: repo.Name, + ID: repo.ID, + WebhookSecret: repo.WebhookSecret, + EntityType: params.GithubEntityTypeRepository, + } + poolManager, err := pool.NewEntityPoolManager(ctx, entity, cfgInternal, providers, store) if err != nil { return nil, errors.Wrap(err, "creating repo pool manager") } @@ -175,7 +182,13 @@ func (p *poolManagerCtrl) CreateOrgPoolManager(ctx context.Context, org params.O if err != nil { return nil, errors.Wrap(err, "fetching internal config") } - poolManager, err := pool.NewOrganizationPoolManager(ctx, org, cfgInternal, providers, store) + entity := params.GithubEntity{ + Owner: org.Name, + ID: org.ID, + WebhookSecret: org.WebhookSecret, + EntityType: params.GithubEntityTypeOrganization, + } + poolManager, err := pool.NewEntityPoolManager(ctx, entity, cfgInternal, providers, store) if err != nil { return nil, errors.Wrap(err, "creating org pool manager") } @@ -241,7 +254,14 @@ func (p *poolManagerCtrl) CreateEnterprisePoolManager(ctx context.Context, enter if err != nil { return nil, errors.Wrap(err, "fetching internal config") } - poolManager, err := pool.NewEnterprisePoolManager(ctx, enterprise, cfgInternal, providers, store) + + entity := params.GithubEntity{ + Owner: enterprise.Name, + ID: enterprise.ID, + WebhookSecret: enterprise.WebhookSecret, + EntityType: params.GithubEntityTypeEnterprise, + } + poolManager, err := pool.NewEntityPoolManager(ctx, entity, cfgInternal, providers, store) if err != nil { return nil, errors.Wrap(err, "creating enterprise pool manager") } diff --git a/util/util.go b/util/util.go index 639f22fb..27d674ef 100644 --- a/util/util.go +++ b/util/util.go @@ -16,73 +16,438 @@ package util import ( "context" + "encoding/base64" + "encoding/json" + "fmt" + "log/slog" + "net/http" "github.com/google/go-github/v57/github" "github.com/pkg/errors" + runnerErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/metrics" "github.com/cloudbase/garm/params" "github.com/cloudbase/garm/runner/common" ) type githubClient struct { *github.ActionsService - org *github.OrganizationsService - repo *github.RepositoriesService + org *github.OrganizationsService + repo *github.RepositoriesService + enterprise *github.EnterpriseService + + entity params.GithubEntity } -func (g *githubClient) ListOrgHooks(ctx context.Context, org string, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) { - return g.org.ListHooks(ctx, org, opts) +func (g *githubClient) ListEntityHooks(ctx context.Context, opts *github.ListOptions) (ret []*github.Hook, response *github.Response, err error) { + metrics.GithubOperationCount.WithLabelValues( + "ListHooks", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + defer func() { + if err != nil { + metrics.GithubOperationFailedCount.WithLabelValues( + "ListHooks", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + } + }() + switch g.entity.EntityType { + case params.GithubEntityTypeRepository: + ret, response, err = g.repo.ListHooks(ctx, g.entity.Owner, g.entity.Name, opts) + case params.GithubEntityTypeOrganization: + ret, response, err = g.org.ListHooks(ctx, g.entity.Owner, opts) + default: + return nil, nil, errors.New("invalid entity type") + } + return ret, response, err } -func (g *githubClient) GetOrgHook(ctx context.Context, org string, id int64) (*github.Hook, *github.Response, error) { - return g.org.GetHook(ctx, org, id) +func (g *githubClient) GetEntityHook(ctx context.Context, id int64) (ret *github.Hook, err error) { + metrics.GithubOperationCount.WithLabelValues( + "GetHook", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + defer func() { + if err != nil { + metrics.GithubOperationFailedCount.WithLabelValues( + "GetHook", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + } + }() + switch g.entity.EntityType { + case params.GithubEntityTypeRepository: + ret, _, err = g.repo.GetHook(ctx, g.entity.Owner, g.entity.Name, id) + case params.GithubEntityTypeOrganization: + ret, _, err = g.org.GetHook(ctx, g.entity.Owner, id) + default: + return nil, errors.New("invalid entity type") + } + return ret, err } -func (g *githubClient) CreateOrgHook(ctx context.Context, org string, hook *github.Hook) (*github.Hook, *github.Response, error) { - return g.org.CreateHook(ctx, org, hook) +func (g *githubClient) CreateEntityHook(ctx context.Context, hook *github.Hook) (ret *github.Hook, err error) { + metrics.GithubOperationCount.WithLabelValues( + "CreateHook", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + defer func() { + if err != nil { + metrics.GithubOperationFailedCount.WithLabelValues( + "CreateHook", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + } + }() + switch g.entity.EntityType { + case params.GithubEntityTypeRepository: + ret, _, err = g.repo.CreateHook(ctx, g.entity.Owner, g.entity.Name, hook) + case params.GithubEntityTypeOrganization: + ret, _, err = g.org.CreateHook(ctx, g.entity.Owner, hook) + default: + return nil, errors.New("invalid entity type") + } + return ret, err +} + +func (g *githubClient) DeleteEntityHook(ctx context.Context, id int64) (ret *github.Response, err error) { + metrics.GithubOperationCount.WithLabelValues( + "DeleteHook", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + defer func() { + if err != nil { + metrics.GithubOperationFailedCount.WithLabelValues( + "DeleteHook", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + } + }() + switch g.entity.EntityType { + case params.GithubEntityTypeRepository: + ret, err = g.repo.DeleteHook(ctx, g.entity.Owner, g.entity.Name, id) + case params.GithubEntityTypeOrganization: + ret, err = g.org.DeleteHook(ctx, g.entity.Owner, id) + default: + return nil, errors.New("invalid entity type") + } + return ret, err } -func (g *githubClient) DeleteOrgHook(ctx context.Context, org string, id int64) (*github.Response, error) { - return g.org.DeleteHook(ctx, org, id) +func (g *githubClient) PingEntityHook(ctx context.Context, id int64) (ret *github.Response, err error) { + metrics.GithubOperationCount.WithLabelValues( + "PingHook", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + defer func() { + if err != nil { + metrics.GithubOperationFailedCount.WithLabelValues( + "PingHook", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + } + }() + switch g.entity.EntityType { + case params.GithubEntityTypeRepository: + ret, err = g.repo.PingHook(ctx, g.entity.Owner, g.entity.Name, id) + case params.GithubEntityTypeOrganization: + ret, err = g.org.PingHook(ctx, g.entity.Owner, id) + default: + return nil, errors.New("invalid entity type") + } + return ret, err +} + +func (g *githubClient) ListEntityRunners(ctx context.Context, opts *github.ListOptions) (*github.Runners, *github.Response, error) { + var ret *github.Runners + var response *github.Response + var err error + + metrics.GithubOperationCount.WithLabelValues( + "ListEntityRunners", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + defer func() { + if err != nil { + metrics.GithubOperationFailedCount.WithLabelValues( + "ListEntityRunners", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + } + }() + + switch g.entity.EntityType { + case params.GithubEntityTypeRepository: + ret, response, err = g.ListRunners(ctx, g.entity.Owner, g.entity.Name, opts) + case params.GithubEntityTypeOrganization: + ret, response, err = g.ListOrganizationRunners(ctx, g.entity.Owner, opts) + case params.GithubEntityTypeEnterprise: + ret, response, err = g.enterprise.ListRunners(ctx, g.entity.Owner, opts) + default: + return nil, nil, errors.New("invalid entity type") + } + + return ret, response, err } -func (g *githubClient) PingOrgHook(ctx context.Context, org string, id int64) (*github.Response, error) { - return g.org.PingHook(ctx, org, id) +func (g *githubClient) ListEntityRunnerApplicationDownloads(ctx context.Context) ([]*github.RunnerApplicationDownload, *github.Response, error) { + var ret []*github.RunnerApplicationDownload + var response *github.Response + var err error + + metrics.GithubOperationCount.WithLabelValues( + "ListEntityRunnerApplicationDownloads", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + defer func() { + if err != nil { + metrics.GithubOperationFailedCount.WithLabelValues( + "ListEntityRunnerApplicationDownloads", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + } + }() + + switch g.entity.EntityType { + case params.GithubEntityTypeRepository: + ret, response, err = g.ListRunnerApplicationDownloads(ctx, g.entity.Owner, g.entity.Name) + case params.GithubEntityTypeOrganization: + ret, response, err = g.ListOrganizationRunnerApplicationDownloads(ctx, g.entity.Owner) + case params.GithubEntityTypeEnterprise: + ret, response, err = g.enterprise.ListRunnerApplicationDownloads(ctx, g.entity.Owner) + default: + return nil, nil, errors.New("invalid entity type") + } + + return ret, response, err } -func (g *githubClient) ListRepoHooks(ctx context.Context, owner, repo string, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) { - return g.repo.ListHooks(ctx, owner, repo, opts) +func (g *githubClient) RemoveEntityRunner(ctx context.Context, runnerID int64) (*github.Response, error) { + var response *github.Response + var err error + + metrics.GithubOperationCount.WithLabelValues( + "RemoveEntityRunner", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + defer func() { + if err != nil { + metrics.GithubOperationFailedCount.WithLabelValues( + "RemoveEntityRunner", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + } + }() + + switch g.entity.EntityType { + case params.GithubEntityTypeRepository: + response, err = g.RemoveRunner(ctx, g.entity.Owner, g.entity.Name, runnerID) + case params.GithubEntityTypeOrganization: + response, err = g.RemoveOrganizationRunner(ctx, g.entity.Owner, runnerID) + case params.GithubEntityTypeEnterprise: + response, err = g.enterprise.RemoveRunner(ctx, g.entity.Owner, runnerID) + default: + return nil, errors.New("invalid entity type") + } + + return response, err } -func (g *githubClient) GetRepoHook(ctx context.Context, owner, repo string, id int64) (*github.Hook, *github.Response, error) { - return g.repo.GetHook(ctx, owner, repo, id) +func (g *githubClient) CreateEntityRegistrationToken(ctx context.Context) (*github.RegistrationToken, *github.Response, error) { + var ret *github.RegistrationToken + var response *github.Response + var err error + + metrics.GithubOperationCount.WithLabelValues( + "CreateEntityRegistrationToken", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + defer func() { + if err != nil { + metrics.GithubOperationFailedCount.WithLabelValues( + "CreateEntityRegistrationToken", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + } + }() + + switch g.entity.EntityType { + case params.GithubEntityTypeRepository: + ret, response, err = g.CreateRegistrationToken(ctx, g.entity.Owner, g.entity.Name) + case params.GithubEntityTypeOrganization: + ret, response, err = g.CreateOrganizationRegistrationToken(ctx, g.entity.Owner) + case params.GithubEntityTypeEnterprise: + ret, response, err = g.enterprise.CreateRegistrationToken(ctx, g.entity.Owner) + default: + return nil, nil, errors.New("invalid entity type") + } + + return ret, response, err } -func (g *githubClient) CreateRepoHook(ctx context.Context, owner, repo string, hook *github.Hook) (*github.Hook, *github.Response, error) { - return g.repo.CreateHook(ctx, owner, repo, hook) +func (g *githubClient) getOrganizationRunnerGroupIDByName(ctx context.Context, entity params.GithubEntity, rgName string) (int64, error) { + opts := github.ListOrgRunnerGroupOptions{ + ListOptions: github.ListOptions{ + PerPage: 100, + }, + } + + for { + metrics.GithubOperationCount.WithLabelValues( + "ListOrganizationRunnerGroups", // label: operation + entity.LabelScope(), // label: scope + ).Inc() + runnerGroups, ghResp, err := g.ListOrganizationRunnerGroups(ctx, entity.Owner, &opts) + if err != nil { + metrics.GithubOperationFailedCount.WithLabelValues( + "ListOrganizationRunnerGroups", // label: operation + entity.LabelScope(), // label: scope + ).Inc() + if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { + return 0, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching runners") + } + return 0, errors.Wrap(err, "fetching runners") + } + for _, runnerGroup := range runnerGroups.RunnerGroups { + if runnerGroup.Name != nil && *runnerGroup.Name == rgName { + return *runnerGroup.ID, nil + } + } + if ghResp.NextPage == 0 { + break + } + opts.Page = ghResp.NextPage + } + return 0, runnerErrors.NewNotFoundError("runner group not found") } -func (g *githubClient) DeleteRepoHook(ctx context.Context, owner, repo string, id int64) (*github.Response, error) { - return g.repo.DeleteHook(ctx, owner, repo, id) +func (g *githubClient) getEnterpriseRunnerGroupIDByName(ctx context.Context, entity params.GithubEntity, rgName string) (int64, error) { + opts := github.ListEnterpriseRunnerGroupOptions{ + ListOptions: github.ListOptions{ + PerPage: 100, + }, + } + + for { + metrics.GithubOperationCount.WithLabelValues( + "ListRunnerGroups", // label: operation + entity.LabelScope(), // label: scope + ).Inc() + runnerGroups, ghResp, err := g.enterprise.ListRunnerGroups(ctx, entity.Owner, &opts) + if err != nil { + metrics.GithubOperationFailedCount.WithLabelValues( + "ListRunnerGroups", // label: operation + entity.LabelScope(), // label: scope + ).Inc() + if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { + return 0, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching runners") + } + return 0, errors.Wrap(err, "fetching runners") + } + for _, runnerGroup := range runnerGroups.RunnerGroups { + if runnerGroup.Name != nil && *runnerGroup.Name == rgName { + return *runnerGroup.ID, nil + } + } + if ghResp.NextPage == 0 { + break + } + opts.Page = ghResp.NextPage + } + return 0, runnerErrors.NewNotFoundError("runner group not found") } -func (g *githubClient) PingRepoHook(ctx context.Context, owner, repo string, id int64) (*github.Response, error) { - return g.repo.PingHook(ctx, owner, repo, id) +func (g *githubClient) GetEntityJITConfig(ctx context.Context, instance string, pool params.Pool, labels []string) (jitConfigMap map[string]string, runner *github.Runner, err error) { + var rgID int64 + + switch g.entity.EntityType { + case params.GithubEntityTypeRepository: + rgID = 1 + case params.GithubEntityTypeOrganization: + rgID, err = g.getOrganizationRunnerGroupIDByName(ctx, g.entity, pool.GitHubRunnerGroup) + case params.GithubEntityTypeEnterprise: + rgID, err = g.getEnterpriseRunnerGroupIDByName(ctx, g.entity, pool.GitHubRunnerGroup) + } + + if err != nil { + return nil, nil, fmt.Errorf("getting runner group ID: %w", err) + } + + req := github.GenerateJITConfigRequest{ + Name: instance, + RunnerGroupID: rgID, + Labels: labels, + // nolint:golangci-lint,godox + // TODO(gabriel-samfira): Should we make this configurable? + WorkFolder: github.String("_work"), + } + + metrics.GithubOperationCount.WithLabelValues( + "GetEntityJITConfig", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + + var ret *github.JITRunnerConfig + var response *github.Response + + switch g.entity.EntityType { + case params.GithubEntityTypeRepository: + ret, response, err = g.GenerateRepoJITConfig(ctx, g.entity.Owner, g.entity.Name, &req) + case params.GithubEntityTypeOrganization: + ret, response, err = g.GenerateOrgJITConfig(ctx, g.entity.Owner, &req) + case params.GithubEntityTypeEnterprise: + ret, response, err = g.enterprise.GenerateEnterpriseJITConfig(ctx, g.entity.Owner, &req) + } + if err != nil { + metrics.GithubOperationFailedCount.WithLabelValues( + "GetEntityJITConfig", // label: operation + g.entity.LabelScope(), // label: scope + ).Inc() + if response != nil && response.StatusCode == http.StatusUnauthorized { + return nil, nil, fmt.Errorf("failed to get JIT config: %w", err) + } + return nil, nil, fmt.Errorf("failed to get JIT config: %w", err) + } + + defer func(run *github.Runner) { + if err != nil && run != nil { + _, innerErr := g.RemoveEntityRunner(ctx, run.GetID()) + slog.With(slog.Any("error", innerErr)).ErrorContext( + ctx, "failed to remove runner", + "runner_id", run.GetID(), string(g.entity.EntityType), g.entity.String()) + } + }(ret.Runner) + + decoded, err := base64.StdEncoding.DecodeString(*ret.EncodedJITConfig) + if err != nil { + return nil, nil, fmt.Errorf("failed to decode JIT config: %w", err) + } + + var jitConfig map[string]string + if err := json.Unmarshal(decoded, &jitConfig); err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal JIT config: %w", err) + } + + return jitConfig, ret.Runner, nil } -func GithubClient(_ context.Context, credsDetails params.GithubCredentials) (common.GithubClient, common.GithubEnterpriseClient, error) { +func GithubClient(_ context.Context, entity params.GithubEntity, credsDetails params.GithubCredentials) (common.GithubClient, error) { if credsDetails.HTTPClient == nil { - return nil, nil, errors.New("http client is nil") + return nil, errors.New("http client is nil") } ghClient, err := github.NewClient(credsDetails.HTTPClient).WithEnterpriseURLs(credsDetails.APIBaseURL, credsDetails.UploadBaseURL) if err != nil { - return nil, nil, errors.Wrap(err, "fetching github client") + return nil, errors.Wrap(err, "fetching github client") } cli := &githubClient{ ActionsService: ghClient.Actions, org: ghClient.Organizations, repo: ghClient.Repositories, + enterprise: ghClient.Enterprise, + entity: entity, } - return cli, ghClient.Enterprise, nil + return cli, nil }