From 90870c11bebcc34c8bc8900e2b2e7b3fd30a080d Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Mon, 15 Apr 2024 08:32:19 +0000 Subject: [PATCH 01/27] Use database for github creds Add database models that deal with github credentials. This change adds models for github endpoints (github.com, GHES, etc). This change also adds code to migrate config credntials to the DB. Tests need to be fixed and new tests need to be written. This will come in a later commit. Signed-off-by: Gabriel Adrian Samfira --- cmd/garm-cli/cmd/enterprise.go | 4 +- cmd/garm/main.go | 3 + config/config.go | 13 + database/common/common.go | 20 ++ database/common/mocks/Store.go | 320 +++++++++++++++++++ database/sql/enterprise.go | 12 +- database/sql/enterprise_test.go | 4 +- database/sql/github.go | 473 +++++++++++++++++++++++++++++ database/sql/models.go | 59 +++- database/sql/organizations.go | 12 +- database/sql/organizations_test.go | 4 +- database/sql/repositories.go | 42 ++- database/sql/repositories_test.go | 4 +- database/sql/sql.go | 168 +++++++++- database/sql/users.go | 17 ++ database/sql/util.go | 22 +- params/params.go | 91 +++++- params/requests.go | 67 ++++ runner/enterprises_test.go | 4 +- runner/organizations_test.go | 4 +- runner/repositories_test.go | 10 +- runner/runner.go | 36 +-- 22 files changed, 1312 insertions(+), 77 deletions(-) create mode 100644 database/sql/github.go diff --git a/cmd/garm-cli/cmd/enterprise.go b/cmd/garm-cli/cmd/enterprise.go index 27ca662c..98457aef 100644 --- a/cmd/garm-cli/cmd/enterprise.go +++ b/cmd/garm-cli/cmd/enterprise.go @@ -204,7 +204,7 @@ func formatEnterprises(enterprises []params.Enterprise) { header := table.Row{"ID", "Name", "Credentials name", "Pool Balancer Type", "Pool mgr running"} t.AppendHeader(header) for _, val := range enterprises { - t.AppendRow(table.Row{val.ID, val.Name, val.CredentialsName, val.GetBalancerType(), val.PoolManagerStatus.IsRunning}) + t.AppendRow(table.Row{val.ID, val.Name, val.Credentials.Name, val.GetBalancerType(), val.PoolManagerStatus.IsRunning}) t.AppendSeparator() } fmt.Println(t.Render()) @@ -218,7 +218,7 @@ func formatOneEnterprise(enterprise params.Enterprise) { t.AppendRow(table.Row{"ID", enterprise.ID}) t.AppendRow(table.Row{"Name", enterprise.Name}) t.AppendRow(table.Row{"Pool balancer type", enterprise.GetBalancerType()}) - t.AppendRow(table.Row{"Credentials", enterprise.CredentialsName}) + t.AppendRow(table.Row{"Credentials", enterprise.Credentials.Name}) t.AppendRow(table.Row{"Pool manager running", enterprise.PoolManagerStatus.IsRunning}) if !enterprise.PoolManagerStatus.IsRunning { t.AppendRow(table.Row{"Failure reason", enterprise.PoolManagerStatus.FailureReason}) diff --git a/cmd/garm/main.go b/cmd/garm/main.go index ad80c521..454d766f 100644 --- a/cmd/garm/main.go +++ b/cmd/garm/main.go @@ -167,6 +167,9 @@ func main() { } setupLogging(ctx, logCfg, hub) + // Migrate credentials to the new format. This field will be read + // by the DB migration logic. + cfg.Database.MigrateCredentials = cfg.Github db, err := database.NewDatabase(ctx, cfg.Database) if err != nil { log.Fatal(err) diff --git a/config/config.go b/config/config.go index d777de7e..baafcb8e 100644 --- a/config/config.go +++ b/config/config.go @@ -241,6 +241,14 @@ type GithubApp struct { InstallationID int64 `toml:"installation_id" json:"installation-id"` } +func (a *GithubApp) PrivateKeyBytes() ([]byte, error) { + keyBytes, err := os.ReadFile(a.PrivateKeyPath) + if err != nil { + return nil, fmt.Errorf("reading private_key_path: %w", err) + } + return keyBytes, nil +} + func (a *GithubApp) Validate() error { if a.AppID == 0 { return fmt.Errorf("missing app_id") @@ -472,6 +480,11 @@ type Database struct { // Don't lose or change this. It will invalidate all encrypted data // in the DB. This field must be set and must be exactly 32 characters. Passphrase string `toml:"passphrase"` + + // MigrateCredentials is a list of github credentials that need to be migrated + // from the config file to the database. This field will be removed once GARM + // reaches version 0.2.x. It's only meant to be used for the migration process. + MigrateCredentials []Github `toml:"-"` } // GormParams returns the database type and connection URI diff --git a/database/common/common.go b/database/common/common.go index c270051f..8f901ab7 100644 --- a/database/common/common.go +++ b/database/common/common.go @@ -20,6 +20,23 @@ import ( "github.com/cloudbase/garm/params" ) +type GithubEndpointStore interface { + CreateGithubEndpoint(ctx context.Context, param params.CreateGithubEndpointParams) (params.GithubEndpoint, error) + GetGithubEndpoint(ctx context.Context, name string) (params.GithubEndpoint, error) + ListGithubEndpoints(ctx context.Context) ([]params.GithubEndpoint, error) + UpdateGithubEndpoint(ctx context.Context, name string, param params.UpdateGithubEndpointParams) (params.GithubEndpoint, error) + DeleteGithubEndpoint(ctx context.Context, name string) error +} + +type GithubCredentialsStore interface { + CreateGithubCredentials(ctx context.Context, endpointName string, param params.CreateGithubCredentialsParams) (params.GithubCredentials, error) + GetGithubCredentials(ctx context.Context, id uint, detailed bool) (params.GithubCredentials, error) + GetGithubCredentialsByName(ctx context.Context, name string, detailed bool) (params.GithubCredentials, error) + ListGithubCredentials(ctx context.Context) ([]params.GithubCredentials, error) + UpdateGithubCredentials(ctx context.Context, id uint, param params.UpdateGithubCredentialsParams) (params.GithubCredentials, error) + DeleteGithubCredentials(ctx context.Context, id uint) error +} + type RepoStore interface { CreateRepository(ctx context.Context, owner, name, credentialsName, webhookSecret string, poolBalancerType params.PoolBalancerType) (params.Repository, error) GetRepository(ctx context.Context, owner, name string) (params.Repository, error) @@ -65,6 +82,7 @@ type PoolStore interface { type UserStore interface { GetUser(ctx context.Context, user string) (params.User, error) GetUserByID(ctx context.Context, userID string) (params.User, error) + GetAdminUser(ctx context.Context) (params.User, error) CreateUser(ctx context.Context, user params.NewUserParams) (params.User, error) UpdateUser(ctx context.Context, user string, param params.UpdateUserParams) (params.User, error) @@ -121,6 +139,8 @@ type Store interface { InstanceStore JobsStore EntityPools + GithubEndpointStore + GithubCredentialsStore ControllerInfo() (params.ControllerInfo, error) InitController() (params.ControllerInfo, error) diff --git a/database/common/mocks/Store.go b/database/common/mocks/Store.go index 5b24fdc8..f8877ef7 100644 --- a/database/common/mocks/Store.go +++ b/database/common/mocks/Store.go @@ -134,6 +134,62 @@ func (_m *Store) CreateEntityPool(ctx context.Context, entity params.GithubEntit return r0, r1 } +// CreateGithubCredentials provides a mock function with given fields: ctx, endpointName, param +func (_m *Store) CreateGithubCredentials(ctx context.Context, endpointName string, param params.CreateGithubCredentialsParams) (params.GithubCredentials, error) { + ret := _m.Called(ctx, endpointName, param) + + if len(ret) == 0 { + panic("no return value specified for CreateGithubCredentials") + } + + var r0 params.GithubCredentials + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, params.CreateGithubCredentialsParams) (params.GithubCredentials, error)); ok { + return rf(ctx, endpointName, param) + } + if rf, ok := ret.Get(0).(func(context.Context, string, params.CreateGithubCredentialsParams) params.GithubCredentials); ok { + r0 = rf(ctx, endpointName, param) + } else { + r0 = ret.Get(0).(params.GithubCredentials) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, params.CreateGithubCredentialsParams) error); ok { + r1 = rf(ctx, endpointName, param) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateGithubEndpoint provides a mock function with given fields: ctx, param +func (_m *Store) CreateGithubEndpoint(ctx context.Context, param params.CreateGithubEndpointParams) (params.GithubEndpoint, error) { + ret := _m.Called(ctx, param) + + if len(ret) == 0 { + panic("no return value specified for CreateGithubEndpoint") + } + + var r0 params.GithubEndpoint + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, params.CreateGithubEndpointParams) (params.GithubEndpoint, error)); ok { + return rf(ctx, param) + } + if rf, ok := ret.Get(0).(func(context.Context, params.CreateGithubEndpointParams) params.GithubEndpoint); ok { + r0 = rf(ctx, param) + } else { + r0 = ret.Get(0).(params.GithubEndpoint) + } + + if rf, ok := ret.Get(1).(func(context.Context, params.CreateGithubEndpointParams) error); ok { + r1 = rf(ctx, param) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CreateInstance provides a mock function with given fields: ctx, poolID, param func (_m *Store) CreateInstance(ctx context.Context, poolID string, param params.CreateInstanceParams) (params.Instance, error) { ret := _m.Called(ctx, poolID, param) @@ -328,6 +384,42 @@ func (_m *Store) DeleteEntityPool(ctx context.Context, entity params.GithubEntit return r0 } +// DeleteGithubCredentials provides a mock function with given fields: ctx, id +func (_m *Store) DeleteGithubCredentials(ctx context.Context, id uint) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for DeleteGithubCredentials") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, uint) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteGithubEndpoint provides a mock function with given fields: ctx, name +func (_m *Store) DeleteGithubEndpoint(ctx context.Context, name string) error { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for DeleteGithubEndpoint") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // DeleteInstance provides a mock function with given fields: ctx, poolID, instanceName func (_m *Store) DeleteInstance(ctx context.Context, poolID string, instanceName string) error { ret := _m.Called(ctx, poolID, instanceName) @@ -448,6 +540,34 @@ func (_m *Store) FindPoolsMatchingAllTags(ctx context.Context, entityType params return r0, r1 } +// GetAdminUser provides a mock function with given fields: ctx +func (_m *Store) GetAdminUser(ctx context.Context) (params.User, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetAdminUser") + } + + var r0 params.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (params.User, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) params.User); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(params.User) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetEnterprise provides a mock function with given fields: ctx, name func (_m *Store) GetEnterprise(ctx context.Context, name string) (params.Enterprise, error) { ret := _m.Called(ctx, name) @@ -532,6 +652,90 @@ func (_m *Store) GetEntityPool(ctx context.Context, entity params.GithubEntity, return r0, r1 } +// GetGithubCredentials provides a mock function with given fields: ctx, id, detailed +func (_m *Store) GetGithubCredentials(ctx context.Context, id uint, detailed bool) (params.GithubCredentials, error) { + ret := _m.Called(ctx, id, detailed) + + if len(ret) == 0 { + panic("no return value specified for GetGithubCredentials") + } + + var r0 params.GithubCredentials + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint, bool) (params.GithubCredentials, error)); ok { + return rf(ctx, id, detailed) + } + if rf, ok := ret.Get(0).(func(context.Context, uint, bool) params.GithubCredentials); ok { + r0 = rf(ctx, id, detailed) + } else { + r0 = ret.Get(0).(params.GithubCredentials) + } + + if rf, ok := ret.Get(1).(func(context.Context, uint, bool) error); ok { + r1 = rf(ctx, id, detailed) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetGithubCredentialsByName provides a mock function with given fields: ctx, name, detailed +func (_m *Store) GetGithubCredentialsByName(ctx context.Context, name string, detailed bool) (params.GithubCredentials, error) { + ret := _m.Called(ctx, name, detailed) + + if len(ret) == 0 { + panic("no return value specified for GetGithubCredentialsByName") + } + + var r0 params.GithubCredentials + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, bool) (params.GithubCredentials, error)); ok { + return rf(ctx, name, detailed) + } + if rf, ok := ret.Get(0).(func(context.Context, string, bool) params.GithubCredentials); ok { + r0 = rf(ctx, name, detailed) + } else { + r0 = ret.Get(0).(params.GithubCredentials) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok { + r1 = rf(ctx, name, detailed) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetGithubEndpoint provides a mock function with given fields: ctx, name +func (_m *Store) GetGithubEndpoint(ctx context.Context, name string) (params.GithubEndpoint, error) { + ret := _m.Called(ctx, name) + + if len(ret) == 0 { + panic("no return value specified for GetGithubEndpoint") + } + + var r0 params.GithubEndpoint + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (params.GithubEndpoint, error)); ok { + return rf(ctx, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string) params.GithubEndpoint); ok { + r0 = rf(ctx, name) + } else { + r0 = ret.Get(0).(params.GithubEndpoint) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetInstanceByName provides a mock function with given fields: ctx, instanceName func (_m *Store) GetInstanceByName(ctx context.Context, instanceName string) (params.Instance, error) { ret := _m.Called(ctx, instanceName) @@ -1068,6 +1272,66 @@ func (_m *Store) ListEntityPools(ctx context.Context, entity params.GithubEntity return r0, r1 } +// ListGithubCredentials provides a mock function with given fields: ctx +func (_m *Store) ListGithubCredentials(ctx context.Context) ([]params.GithubCredentials, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListGithubCredentials") + } + + var r0 []params.GithubCredentials + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]params.GithubCredentials, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []params.GithubCredentials); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]params.GithubCredentials) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListGithubEndpoints provides a mock function with given fields: ctx +func (_m *Store) ListGithubEndpoints(ctx context.Context) ([]params.GithubEndpoint, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListGithubEndpoints") + } + + var r0 []params.GithubEndpoint + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]params.GithubEndpoint, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []params.GithubEndpoint); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]params.GithubEndpoint) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // ListJobsByStatus provides a mock function with given fields: ctx, status func (_m *Store) ListJobsByStatus(ctx context.Context, status params.JobStatus) ([]params.Job, error) { ret := _m.Called(ctx, status) @@ -1308,6 +1572,62 @@ func (_m *Store) UpdateEntityPool(ctx context.Context, entity params.GithubEntit return r0, r1 } +// UpdateGithubCredentials provides a mock function with given fields: ctx, id, param +func (_m *Store) UpdateGithubCredentials(ctx context.Context, id uint, param params.UpdateGithubCredentialsParams) (params.GithubCredentials, error) { + ret := _m.Called(ctx, id, param) + + if len(ret) == 0 { + panic("no return value specified for UpdateGithubCredentials") + } + + var r0 params.GithubCredentials + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint, params.UpdateGithubCredentialsParams) (params.GithubCredentials, error)); ok { + return rf(ctx, id, param) + } + if rf, ok := ret.Get(0).(func(context.Context, uint, params.UpdateGithubCredentialsParams) params.GithubCredentials); ok { + r0 = rf(ctx, id, param) + } else { + r0 = ret.Get(0).(params.GithubCredentials) + } + + if rf, ok := ret.Get(1).(func(context.Context, uint, params.UpdateGithubCredentialsParams) error); ok { + r1 = rf(ctx, id, param) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateGithubEndpoint provides a mock function with given fields: ctx, name, param +func (_m *Store) UpdateGithubEndpoint(ctx context.Context, name string, param params.UpdateGithubEndpointParams) (params.GithubEndpoint, error) { + ret := _m.Called(ctx, name, param) + + if len(ret) == 0 { + panic("no return value specified for UpdateGithubEndpoint") + } + + var r0 params.GithubEndpoint + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, params.UpdateGithubEndpointParams) (params.GithubEndpoint, error)); ok { + return rf(ctx, name, param) + } + if rf, ok := ret.Get(0).(func(context.Context, string, params.UpdateGithubEndpointParams) params.GithubEndpoint); ok { + r0 = rf(ctx, name, param) + } else { + r0 = ret.Get(0).(params.GithubEndpoint) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, params.UpdateGithubEndpointParams) error); ok { + r1 = rf(ctx, name, param) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // UpdateInstance provides a mock function with given fields: ctx, instanceName, param func (_m *Store) UpdateInstance(ctx context.Context, instanceName string, param params.UpdateInstanceParams) (params.Instance, error) { ret := _m.Called(ctx, instanceName, param) diff --git a/database/sql/enterprise.go b/database/sql/enterprise.go index 3eb53b9e..e7270faf 100644 --- a/database/sql/enterprise.go +++ b/database/sql/enterprise.go @@ -54,7 +54,7 @@ func (s *sqlDatabase) GetEnterprise(ctx context.Context, name string) (params.En } func (s *sqlDatabase) GetEnterpriseByID(ctx context.Context, enterpriseID string) (params.Enterprise, error) { - enterprise, err := s.getEnterpriseByID(ctx, enterpriseID, "Pools") + enterprise, err := s.getEnterpriseByID(ctx, enterpriseID, "Pools", "Credentials", "Endpoint") if err != nil { return params.Enterprise{}, errors.Wrap(err, "fetching enterprise") } @@ -68,7 +68,7 @@ func (s *sqlDatabase) GetEnterpriseByID(ctx context.Context, enterpriseID string func (s *sqlDatabase) ListEnterprises(_ context.Context) ([]params.Enterprise, error) { var enterprises []Enterprise - q := s.conn.Find(&enterprises) + q := s.conn.Preload("Credentials").Find(&enterprises) if q.Error != nil { return []params.Enterprise{}, errors.Wrap(q.Error, "fetching enterprises") } @@ -100,7 +100,7 @@ func (s *sqlDatabase) DeleteEnterprise(ctx context.Context, enterpriseID string) } func (s *sqlDatabase) UpdateEnterprise(ctx context.Context, enterpriseID string, param params.UpdateEntityParams) (params.Enterprise, error) { - enterprise, err := s.getEnterpriseByID(ctx, enterpriseID) + enterprise, err := s.getEnterpriseByID(ctx, enterpriseID, "Credentials", "Endpoint") if err != nil { return params.Enterprise{}, errors.Wrap(err, "fetching enterprise") } @@ -136,8 +136,10 @@ func (s *sqlDatabase) UpdateEnterprise(ctx context.Context, enterpriseID string, func (s *sqlDatabase) getEnterprise(_ context.Context, name string) (Enterprise, error) { var enterprise Enterprise - q := s.conn.Where("name = ? COLLATE NOCASE", name) - q = q.First(&enterprise) + q := s.conn.Where("name = ? COLLATE NOCASE", name). + Preload("Credentials"). + Preload("Endpoint"). + First(&enterprise) if q.Error != nil { if errors.Is(q.Error, gorm.ErrRecordNotFound) { return Enterprise{}, runnerErrors.ErrNotFound diff --git a/database/sql/enterprise_test.go b/database/sql/enterprise_test.go index b20a1f20..7f22956b 100644 --- a/database/sql/enterprise_test.go +++ b/database/sql/enterprise_test.go @@ -173,7 +173,7 @@ func (s *EnterpriseTestSuite) TestCreateEnterprise() { s.FailNow(fmt.Sprintf("failed to get enterprise by id: %v", err)) } s.Require().Equal(storeEnterprise.Name, enterprise.Name) - s.Require().Equal(storeEnterprise.CredentialsName, enterprise.CredentialsName) + s.Require().Equal(storeEnterprise.Credentials.Name, enterprise.Credentials.Name) s.Require().Equal(storeEnterprise.WebhookSecret, enterprise.WebhookSecret) } @@ -313,7 +313,7 @@ func (s *EnterpriseTestSuite) TestUpdateEnterprise() { enterprise, err := s.Store.UpdateEnterprise(context.Background(), s.Fixtures.Enterprises[0].ID, s.Fixtures.UpdateRepoParams) s.Require().Nil(err) - s.Require().Equal(s.Fixtures.UpdateRepoParams.CredentialsName, enterprise.CredentialsName) + s.Require().Equal(s.Fixtures.UpdateRepoParams.CredentialsName, enterprise.Credentials.Name) s.Require().Equal(s.Fixtures.UpdateRepoParams.WebhookSecret, enterprise.WebhookSecret) } diff --git a/database/sql/github.go b/database/sql/github.go new file mode 100644 index 00000000..0087165f --- /dev/null +++ b/database/sql/github.go @@ -0,0 +1,473 @@ +package sql + +import ( + "context" + + "github.com/google/uuid" + "github.com/pkg/errors" + "gorm.io/gorm" + + runnerErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm-provider-common/util" + "github.com/cloudbase/garm/auth" + "github.com/cloudbase/garm/params" +) + +func (s *sqlDatabase) sqlToCommonGithubCredentials(creds GithubCredentials) (params.GithubCredentials, error) { + data, err := util.Unseal(creds.Payload, []byte(s.cfg.Passphrase)) + if err != nil { + return params.GithubCredentials{}, errors.Wrap(err, "unsealing credentials") + } + + commonCreds := params.GithubCredentials{ + ID: creds.ID, + Name: creds.Name, + Description: creds.Description, + APIBaseURL: creds.Endpoint.APIBaseURL, + BaseURL: creds.Endpoint.BaseURL, + UploadBaseURL: creds.Endpoint.UploadBaseURL, + CABundle: creds.Endpoint.CACertBundle, + AuthType: creds.AuthType, + Endpoint: creds.Endpoint.Name, + CredentialsPayload: data, + } + + for _, repo := range creds.Repositories { + commonRepo, err := s.sqlToCommonRepository(repo) + if err != nil { + return params.GithubCredentials{}, errors.Wrap(err, "converting github repository") + } + commonCreds.Repositories = append(commonCreds.Repositories, commonRepo) + } + + for _, org := range creds.Organizations { + commonOrg, err := s.sqlToCommonOrganization(org) + if err != nil { + return params.GithubCredentials{}, errors.Wrap(err, "converting github organization") + } + commonCreds.Organizations = append(commonCreds.Organizations, commonOrg) + } + + for _, ent := range creds.Enterprises { + commonEnt, err := s.sqlToCommonEnterprise(ent) + if err != nil { + return params.GithubCredentials{}, errors.Wrap(err, "converting github enterprise") + } + commonCreds.Enterprises = append(commonCreds.Enterprises, commonEnt) + } + + return commonCreds, nil +} + +func (s *sqlDatabase) sqlToCommonGithubEndpoint(ep GithubEndpoint) (params.GithubEndpoint, error) { + return params.GithubEndpoint{ + Name: ep.Name, + Description: ep.Description, + APIBaseURL: ep.APIBaseURL, + BaseURL: ep.BaseURL, + UploadBaseURL: ep.UploadBaseURL, + CACertBundle: ep.CACertBundle, + }, nil +} + +func getUIDFromContext(ctx context.Context) (uuid.UUID, error) { + userID := auth.UserID(ctx) + if userID == "" { + return uuid.Nil, errors.Wrap(runnerErrors.ErrUnauthorized, "creating github endpoint") + } + + asUUID, err := uuid.Parse(userID) + if err != nil { + return uuid.Nil, errors.Wrap(runnerErrors.ErrUnauthorized, "creating github endpoint") + } + return asUUID, nil +} + +func (s *sqlDatabase) CreateGithubEndpoint(_ context.Context, param params.CreateGithubEndpointParams) (params.GithubEndpoint, error) { + var endpoint GithubEndpoint + err := s.conn.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("name = ?", param.Name).First(&endpoint).Error; err == nil { + return errors.Wrap(runnerErrors.ErrDuplicateEntity, "github endpoint already exists") + } + endpoint = GithubEndpoint{ + Name: param.Name, + Description: param.Description, + APIBaseURL: param.APIBaseURL, + BaseURL: param.BaseURL, + UploadBaseURL: param.UploadBaseURL, + CACertBundle: param.CACertBundle, + } + + if err := tx.Create(&endpoint).Error; err != nil { + return errors.Wrap(err, "creating github endpoint") + } + return nil + }) + if err != nil { + return params.GithubEndpoint{}, errors.Wrap(err, "creating github endpoint") + } + return s.sqlToCommonGithubEndpoint(endpoint) +} + +func (s *sqlDatabase) ListGithubEndpoints(_ context.Context) ([]params.GithubEndpoint, error) { + var endpoints []GithubEndpoint + err := s.conn.Find(&endpoints).Error + if err != nil { + return nil, errors.Wrap(err, "fetching github endpoints") + } + + var ret []params.GithubEndpoint + for _, ep := range endpoints { + commonEp, err := s.sqlToCommonGithubEndpoint(ep) + if err != nil { + return nil, errors.Wrap(err, "converting github endpoint") + } + ret = append(ret, commonEp) + } + return ret, nil +} + +func (s *sqlDatabase) UpdateGithubEndpoint(_ context.Context, name string, param params.UpdateGithubEndpointParams) (params.GithubEndpoint, error) { + var endpoint GithubEndpoint + err := s.conn.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("name = ?", name).First(&endpoint).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrap(runnerErrors.ErrNotFound, "github endpoint not found") + } + return errors.Wrap(err, "fetching github endpoint") + } + if param.APIBaseURL != nil { + endpoint.APIBaseURL = *param.APIBaseURL + } + + if param.BaseURL != nil { + endpoint.BaseURL = *param.BaseURL + } + + if param.UploadBaseURL != nil { + endpoint.UploadBaseURL = *param.UploadBaseURL + } + + if param.CACertBundle != nil { + endpoint.CACertBundle = param.CACertBundle + } + + if param.Description != nil { + endpoint.Description = *param.Description + } + + if err := tx.Save(&endpoint).Error; err != nil { + return errors.Wrap(err, "updating github endpoint") + } + + return nil + }) + if err != nil { + return params.GithubEndpoint{}, errors.Wrap(err, "updating github endpoint") + } + return s.sqlToCommonGithubEndpoint(endpoint) +} + +func (s *sqlDatabase) GetGithubEndpoint(_ context.Context, name string) (params.GithubEndpoint, error) { + var endpoint GithubEndpoint + + err := s.conn.Where("name = ?", name).First(&endpoint).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return params.GithubEndpoint{}, errors.Wrap(err, "github endpoint not found") + } + return params.GithubEndpoint{}, errors.Wrap(err, "fetching github endpoint") + } + + return s.sqlToCommonGithubEndpoint(endpoint) +} + +func (s *sqlDatabase) DeleteGithubEndpoint(_ context.Context, name string) error { + err := s.conn.Transaction(func(tx *gorm.DB) error { + var endpoint GithubEndpoint + if err := tx.Where("name = ?", name).First(&endpoint).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return errors.Wrap(err, "fetching github endpoint") + } + + var credsCount int64 + if err := tx.Model(&GithubCredentials{}).Where("endpoint_name = ?", endpoint.Name).Count(&credsCount).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrap(err, "fetching github credentials") + } + } + + if credsCount > 0 { + return errors.New("cannot delete endpoint with credentials") + } + + if err := tx.Unscoped().Delete(&endpoint).Error; err != nil { + return errors.Wrap(err, "deleting github endpoint") + } + return nil + }) + if err != nil { + return errors.Wrap(err, "deleting github endpoint") + } + return nil +} + +func (s *sqlDatabase) CreateGithubCredentials(ctx context.Context, endpointName string, param params.CreateGithubCredentialsParams) (params.GithubCredentials, error) { + userID, err := getUIDFromContext(ctx) + if err != nil { + return params.GithubCredentials{}, errors.Wrap(err, "creating github credentials") + } + var creds GithubCredentials + err = s.conn.Transaction(func(tx *gorm.DB) error { + var endpoint GithubEndpoint + if err := tx.Where("name = ?", endpointName).First(&endpoint).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrap(runnerErrors.ErrNotFound, "github endpoint not found") + } + return errors.Wrap(err, "fetching github endpoint") + } + + if err := tx.Where("name = ?", param.Name).First(&creds).Error; err == nil { + return errors.New("github credentials already exists") + } + + var data []byte + var err error + switch param.AuthType { + case params.GithubAuthTypePAT: + data, err = s.marshalAndSeal(param.PAT) + case params.GithubAuthTypeApp: + data, err = s.marshalAndSeal(param.App) + } + if err != nil { + return errors.Wrap(err, "marshaling and sealing credentials") + } + + creds = GithubCredentials{ + Name: param.Name, + Description: param.Description, + EndpointName: &endpoint.Name, + AuthType: param.AuthType, + Payload: data, + UserID: &userID, + } + + if err := tx.Create(&creds).Error; err != nil { + return errors.Wrap(err, "creating github credentials") + } + // Skip making an extra query. + creds.Endpoint = endpoint + + return nil + }) + if err != nil { + return params.GithubCredentials{}, errors.Wrap(err, "creating github credentials") + } + return s.sqlToCommonGithubCredentials(creds) +} + +func (s *sqlDatabase) getGithubCredentialsByName(ctx context.Context, tx *gorm.DB, name string, detailed bool) (GithubCredentials, error) { + var creds GithubCredentials + q := tx.Preload("Endpoint") + + if detailed { + q = q. + Preload("Repositories"). + Preload("Organizations"). + Preload("Enterprises") + } + + if !auth.IsAdmin(ctx) { + userID, err := getUIDFromContext(ctx) + if err != nil { + return GithubCredentials{}, errors.Wrap(err, "fetching github credentials") + } + q = q.Where("user_id = ?", userID) + } + + err := q.Where("name = ?", name).First(&creds).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return GithubCredentials{}, errors.Wrap(runnerErrors.ErrNotFound, "github credentials not found") + } + return GithubCredentials{}, errors.Wrap(err, "fetching github credentials") + } + + return creds, nil +} + +func (s *sqlDatabase) GetGithubCredentialsByName(ctx context.Context, name string, detailed bool) (params.GithubCredentials, error) { + creds, err := s.getGithubCredentialsByName(ctx, s.conn, name, detailed) + if err != nil { + return params.GithubCredentials{}, errors.Wrap(err, "fetching github credentials") + } + + return s.sqlToCommonGithubCredentials(creds) +} + +func (s *sqlDatabase) GetGithubCredentials(ctx context.Context, id uint, detailed bool) (params.GithubCredentials, error) { + var creds GithubCredentials + q := s.conn.Preload("Endpoint") + + if detailed { + q = q. + Preload("Repositories"). + Preload("Organizations"). + Preload("Enterprises") + } + + if !auth.IsAdmin(ctx) { + userID, err := getUIDFromContext(ctx) + if err != nil { + return params.GithubCredentials{}, errors.Wrap(err, "fetching github credentials") + } + q = q.Where("user_id = ?", userID) + } + + err := q.Where("id = ?", id).First(&creds).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return params.GithubCredentials{}, errors.Wrap(runnerErrors.ErrNotFound, "github credentials not found") + } + return params.GithubCredentials{}, errors.Wrap(err, "fetching github credentials") + } + + return s.sqlToCommonGithubCredentials(creds) +} + +func (s *sqlDatabase) ListGithubCredentials(ctx context.Context) ([]params.GithubCredentials, error) { + q := s.conn.Preload("Endpoint") + if !auth.IsAdmin(ctx) { + userID, err := getUIDFromContext(ctx) + if err != nil { + return nil, errors.Wrap(err, "fetching github credentials") + } + q = q.Where("user_id = ?", userID) + } + + var creds []GithubCredentials + err := q.Preload("Endpoint").Find(&creds).Error + if err != nil { + return nil, errors.Wrap(err, "fetching github credentials") + } + + var ret []params.GithubCredentials + for _, c := range creds { + commonCreds, err := s.sqlToCommonGithubCredentials(c) + if err != nil { + return nil, errors.Wrap(err, "converting github credentials") + } + ret = append(ret, commonCreds) + } + return ret, nil +} + +func (s *sqlDatabase) UpdateGithubCredentials(ctx context.Context, id uint, param params.UpdateGithubCredentialsParams) (params.GithubCredentials, error) { + var creds GithubCredentials + err := s.conn.Transaction(func(tx *gorm.DB) error { + q := tx.Preload("Endpoint") + if !auth.IsAdmin(ctx) { + userID, err := getUIDFromContext(ctx) + if err != nil { + return errors.Wrap(err, "updating github credentials") + } + q = q.Where("user_id = ?", userID) + } + + if err := q.Where("id = ?", id).First(&creds).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrap(runnerErrors.ErrNotFound, "github credentials not found") + } + return errors.Wrap(err, "fetching github credentials") + } + + if param.Name != nil { + creds.Name = *param.Name + } + if param.Description != nil { + creds.Description = *param.Description + } + + var data []byte + var err error + switch creds.AuthType { + case params.GithubAuthTypePAT: + if param.PAT != nil { + data, err = s.marshalAndSeal(param.PAT) + } + + if param.App != nil { + return errors.New("cannot update app credentials for PAT") + } + case params.GithubAuthTypeApp: + if param.App != nil { + data, err = s.marshalAndSeal(param.App) + } + + if param.PAT != nil { + return errors.New("cannot update PAT credentials for app") + } + } + + if err != nil { + return errors.Wrap(err, "marshaling and sealing credentials") + } + if len(data) > 0 { + creds.Payload = data + } + + if err := tx.Save(&creds).Error; err != nil { + return errors.Wrap(err, "updating github credentials") + } + return nil + }) + if err != nil { + return params.GithubCredentials{}, errors.Wrap(err, "updating github credentials") + } + return s.sqlToCommonGithubCredentials(creds) +} + +func (s *sqlDatabase) DeleteGithubCredentials(ctx context.Context, id uint) error { + err := s.conn.Transaction(func(tx *gorm.DB) error { + q := tx.Where("id = ?", id). + Preload("Repositories"). + Preload("Organizations"). + Preload("Enterprises") + if !auth.IsAdmin(ctx) { + userID, err := getUIDFromContext(ctx) + if err != nil { + return errors.Wrap(err, "deleting github credentials") + } + q = q.Where("user_id = ?", userID) + } + + var creds GithubCredentials + err := q.First(&creds).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return errors.Wrap(err, "fetching github credentials") + } + if len(creds.Repositories) > 0 { + return errors.New("cannot delete credentials with repositories") + } + if len(creds.Organizations) > 0 { + return errors.New("cannot delete credentials with organizations") + } + if len(creds.Enterprises) > 0 { + return errors.New("cannot delete credentials with enterprises") + } + + if err := tx.Unscoped().Delete(&creds).Error; err != nil { + return errors.Wrap(err, "deleting github credentials") + } + return nil + }) + if err != nil { + return errors.Wrap(err, "deleting github credentials") + } + return nil +} diff --git a/database/sql/models.go b/database/sql/models.go index 874a375d..633a1b51 100644 --- a/database/sql/models.go +++ b/database/sql/models.go @@ -89,35 +89,56 @@ type Pool struct { type Repository struct { Base - CredentialsName string + CredentialsName string + + CredentialsID *uint `gorm:"index"` + Credentials GithubCredentials `gorm:"foreignKey:CredentialsID;constraint:OnDelete:SET NULL"` + Owner string `gorm:"index:idx_owner_nocase,unique,collate:nocase"` Name string `gorm:"index:idx_owner_nocase,unique,collate:nocase"` WebhookSecret []byte Pools []Pool `gorm:"foreignKey:RepoID"` Jobs []WorkflowJob `gorm:"foreignKey:RepoID;constraint:OnDelete:SET NULL"` PoolBalancerType params.PoolBalancerType `gorm:"type:varchar(64)"` + + EndpointName *string `gorm:"index:idx_owner_nocase,unique,collate:nocase"` + Endpoint GithubEndpoint `gorm:"foreignKey:EndpointName;constraint:OnDelete:SET NULL"` } type Organization struct { Base - CredentialsName string + CredentialsName string + + CredentialsID *uint `gorm:"index"` + Credentials GithubCredentials `gorm:"foreignKey:CredentialsID;constraint:OnDelete:SET NULL"` + Name string `gorm:"index:idx_org_name_nocase,collate:nocase"` WebhookSecret []byte Pools []Pool `gorm:"foreignKey:OrgID"` Jobs []WorkflowJob `gorm:"foreignKey:OrgID;constraint:OnDelete:SET NULL"` PoolBalancerType params.PoolBalancerType `gorm:"type:varchar(64)"` + + EndpointName *string `gorm:"index"` + Endpoint GithubEndpoint `gorm:"foreignKey:EndpointName;constraint:OnDelete:SET NULL"` } type Enterprise struct { Base - CredentialsName string + CredentialsName string + + CredentialsID *uint `gorm:"index"` + Credentials GithubCredentials `gorm:"foreignKey:CredentialsID;constraint:OnDelete:SET NULL"` + Name string `gorm:"index:idx_ent_name_nocase,collate:nocase"` WebhookSecret []byte Pools []Pool `gorm:"foreignKey:EnterpriseID"` Jobs []WorkflowJob `gorm:"foreignKey:EnterpriseID;constraint:OnDelete:SET NULL"` PoolBalancerType params.PoolBalancerType `gorm:"type:varchar(64)"` + + EndpointName *string `gorm:"index"` + Endpoint GithubEndpoint `gorm:"foreignKey:EndpointName;constraint:OnDelete:SET NULL"` } type Address struct { @@ -246,3 +267,35 @@ type WorkflowJob struct { UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` } + +type GithubEndpoint struct { + Name string `gorm:"type:varchar(64) collate nocase;primary_key;"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` + + Description string `gorm:"type:text"` + APIBaseURL string `gorm:"type:text collate nocase"` + UploadBaseURL string `gorm:"type:text collate nocase"` + BaseURL string `gorm:"type:text collate nocase"` + CACertBundle []byte `gorm:"type:longblob"` +} + +type GithubCredentials struct { + gorm.Model + + Name string `gorm:"index:idx_github_credentials,unique;type:varchar(64) collate nocase"` + UserID *uuid.UUID `gorm:"index:idx_github_credentials,unique"` + User User `gorm:"foreignKey:UserID"` + + Description string `gorm:"type:text"` + AuthType params.GithubAuthType `gorm:"index"` + Payload []byte `gorm:"type:longblob"` + + Endpoint GithubEndpoint `gorm:"foreignKey:EndpointName"` + EndpointName *string `gorm:"index"` + + Repositories []Repository `gorm:"foreignKey:CredentialsID"` + Organizations []Organization `gorm:"foreignKey:CredentialsID"` + Enterprises []Enterprise `gorm:"foreignKey:CredentialsID"` +} diff --git a/database/sql/organizations.go b/database/sql/organizations.go index 24704fd9..c67e85d4 100644 --- a/database/sql/organizations.go +++ b/database/sql/organizations.go @@ -72,7 +72,7 @@ func (s *sqlDatabase) GetOrganization(ctx context.Context, name string) (params. func (s *sqlDatabase) ListOrganizations(_ context.Context) ([]params.Organization, error) { var orgs []Organization - q := s.conn.Find(&orgs) + q := s.conn.Preload("Credentials").Find(&orgs) if q.Error != nil { return []params.Organization{}, errors.Wrap(q.Error, "fetching org from database") } @@ -104,7 +104,7 @@ func (s *sqlDatabase) DeleteOrganization(ctx context.Context, orgID string) erro } func (s *sqlDatabase) UpdateOrganization(ctx context.Context, orgID string, param params.UpdateEntityParams) (params.Organization, error) { - org, err := s.getOrgByID(ctx, orgID) + org, err := s.getOrgByID(ctx, orgID, "Credentials", "Endpoint") if err != nil { return params.Organization{}, errors.Wrap(err, "fetching org") } @@ -138,7 +138,7 @@ func (s *sqlDatabase) UpdateOrganization(ctx context.Context, orgID string, para } func (s *sqlDatabase) GetOrganizationByID(ctx context.Context, orgID string) (params.Organization, error) { - org, err := s.getOrgByID(ctx, orgID, "Pools") + org, err := s.getOrgByID(ctx, orgID, "Pools", "Credentials", "Endpoint") if err != nil { return params.Organization{}, errors.Wrap(err, "fetching org") } @@ -177,8 +177,10 @@ func (s *sqlDatabase) getOrgByID(_ context.Context, id string, preload ...string func (s *sqlDatabase) getOrg(_ context.Context, name string) (Organization, error) { var org Organization - q := s.conn.Where("name = ? COLLATE NOCASE", name) - q = q.First(&org) + q := s.conn.Where("name = ? COLLATE NOCASE", name). + Preload("Credentials"). + Preload("Endpoint"). + First(&org) if q.Error != nil { if errors.Is(q.Error, gorm.ErrRecordNotFound) { return Organization{}, runnerErrors.ErrNotFound diff --git a/database/sql/organizations_test.go b/database/sql/organizations_test.go index 77ebab90..28a049e5 100644 --- a/database/sql/organizations_test.go +++ b/database/sql/organizations_test.go @@ -173,7 +173,7 @@ func (s *OrgTestSuite) TestCreateOrganization() { s.FailNow(fmt.Sprintf("failed to get organization by id: %v", err)) } s.Require().Equal(storeOrg.Name, org.Name) - s.Require().Equal(storeOrg.CredentialsName, org.CredentialsName) + s.Require().Equal(storeOrg.Credentials.Name, org.Credentials.Name) s.Require().Equal(storeOrg.WebhookSecret, org.WebhookSecret) } @@ -313,7 +313,7 @@ func (s *OrgTestSuite) TestUpdateOrganization() { org, err := s.Store.UpdateOrganization(context.Background(), s.Fixtures.Orgs[0].ID, s.Fixtures.UpdateRepoParams) s.Require().Nil(err) - s.Require().Equal(s.Fixtures.UpdateRepoParams.CredentialsName, org.CredentialsName) + s.Require().Equal(s.Fixtures.UpdateRepoParams.CredentialsName, org.Credentials.Name) s.Require().Equal(s.Fixtures.UpdateRepoParams.WebhookSecret, org.WebhookSecret) } diff --git a/database/sql/repositories.go b/database/sql/repositories.go index 164c0197..396b2796 100644 --- a/database/sql/repositories.go +++ b/database/sql/repositories.go @@ -27,7 +27,7 @@ import ( "github.com/cloudbase/garm/params" ) -func (s *sqlDatabase) CreateRepository(_ context.Context, owner, name, credentialsName, webhookSecret string, poolBalancerType params.PoolBalancerType) (params.Repository, error) { +func (s *sqlDatabase) CreateRepository(ctx context.Context, owner, name, credentialsName, webhookSecret string, poolBalancerType params.PoolBalancerType) (params.Repository, error) { if webhookSecret == "" { return params.Repository{}, errors.New("creating repo: missing secret") } @@ -35,17 +35,29 @@ func (s *sqlDatabase) CreateRepository(_ context.Context, owner, name, credentia if err != nil { return params.Repository{}, fmt.Errorf("failed to encrypt string") } - newRepo := Repository{ - Name: name, - Owner: owner, - WebhookSecret: secret, - CredentialsName: credentialsName, - PoolBalancerType: poolBalancerType, - } - q := s.conn.Create(&newRepo) - if q.Error != nil { - return params.Repository{}, errors.Wrap(q.Error, "creating repository") + var newRepo Repository + err = s.conn.Transaction(func(tx *gorm.DB) error { + creds, err := s.getGithubCredentialsByName(ctx, tx, credentialsName, false) + if err != nil { + return errors.Wrap(err, "creating repository") + } + + newRepo.Name = name + newRepo.Owner = owner + newRepo.WebhookSecret = secret + newRepo.CredentialsID = &creds.ID + newRepo.PoolBalancerType = poolBalancerType + + q := tx.Create(&newRepo) + if q.Error != nil { + return errors.Wrap(q.Error, "creating repository") + } + + return nil + }) + if err != nil { + return params.Repository{}, errors.Wrap(err, "creating repository") } param, err := s.sqlToCommonRepository(newRepo) @@ -72,7 +84,7 @@ func (s *sqlDatabase) GetRepository(ctx context.Context, owner, name string) (pa func (s *sqlDatabase) ListRepositories(_ context.Context) ([]params.Repository, error) { var repos []Repository - q := s.conn.Find(&repos) + q := s.conn.Preload("Credentials").Find(&repos) if q.Error != nil { return []params.Repository{}, errors.Wrap(q.Error, "fetching user from database") } @@ -104,7 +116,7 @@ func (s *sqlDatabase) DeleteRepository(ctx context.Context, repoID string) error } func (s *sqlDatabase) UpdateRepository(ctx context.Context, repoID string, param params.UpdateEntityParams) (params.Repository, error) { - repo, err := s.getRepoByID(ctx, repoID) + repo, err := s.getRepoByID(ctx, repoID, "Credentials", "Endpoint") if err != nil { return params.Repository{}, errors.Wrap(err, "fetching repo") } @@ -138,7 +150,7 @@ func (s *sqlDatabase) UpdateRepository(ctx context.Context, repoID string, param } func (s *sqlDatabase) GetRepositoryByID(ctx context.Context, repoID string) (params.Repository, error) { - repo, err := s.getRepoByID(ctx, repoID, "Pools") + repo, err := s.getRepoByID(ctx, repoID, "Pools", "Credentials", "Endpoint") if err != nil { return params.Repository{}, errors.Wrap(err, "fetching repo") } @@ -154,6 +166,8 @@ func (s *sqlDatabase) getRepo(_ context.Context, owner, name string) (Repository var repo Repository q := s.conn.Where("name = ? COLLATE NOCASE and owner = ? COLLATE NOCASE", name, owner). + Preload("Credentials"). + Preload("Endpoint"). First(&repo) q = q.First(&repo) diff --git a/database/sql/repositories_test.go b/database/sql/repositories_test.go index 3d335b10..fbd68304 100644 --- a/database/sql/repositories_test.go +++ b/database/sql/repositories_test.go @@ -188,7 +188,7 @@ func (s *RepoTestSuite) TestCreateRepository() { } s.Require().Equal(storeRepo.Owner, repo.Owner) s.Require().Equal(storeRepo.Name, repo.Name) - s.Require().Equal(storeRepo.CredentialsName, repo.CredentialsName) + s.Require().Equal(storeRepo.Credentials.Name, repo.Credentials.Name) s.Require().Equal(storeRepo.WebhookSecret, repo.WebhookSecret) } @@ -352,7 +352,7 @@ func (s *RepoTestSuite) TestUpdateRepository() { repo, err := s.Store.UpdateRepository(context.Background(), s.Fixtures.Repos[0].ID, s.Fixtures.UpdateRepoParams) s.Require().Nil(err) - s.Require().Equal(s.Fixtures.UpdateRepoParams.CredentialsName, repo.CredentialsName) + s.Require().Equal(s.Fixtures.UpdateRepoParams.CredentialsName, repo.Credentials.Name) s.Require().Equal(s.Fixtures.UpdateRepoParams.WebhookSecret, repo.WebhookSecret) } diff --git a/database/sql/sql.go b/database/sql/sql.go index e513be42..1bc16e08 100644 --- a/database/sql/sql.go +++ b/database/sql/sql.go @@ -18,6 +18,7 @@ import ( "context" "fmt" "log/slog" + "net/url" "strings" "github.com/pkg/errors" @@ -26,8 +27,12 @@ import ( "gorm.io/gorm" "gorm.io/gorm/logger" + runnerErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/auth" "github.com/cloudbase/garm/config" "github.com/cloudbase/garm/database/common" + "github.com/cloudbase/garm/params" + "github.com/cloudbase/garm/util/appdefaults" ) // newDBConn returns a new gorm db connection, given the config @@ -190,6 +195,154 @@ func (s *sqlDatabase) cascadeMigration() error { return nil } +func (s *sqlDatabase) migrateCredentialsToDB() (err error) { + s.conn.Exec("PRAGMA foreign_keys = OFF") + defer s.conn.Exec("PRAGMA foreign_keys = ON") + + adminUser, err := s.GetAdminUser(s.ctx) + if err != nil { + if errors.Is(err, runnerErrors.ErrNotFound) { + // Admin user doesn't exist. This is a new deploy. Nothing to migrate. + return nil + } + return errors.Wrap(err, "getting admin user") + } + + // Impersonate the admin user. We're migrating from config credentials to + // database credentials. At this point, there is no other user than the admin + // user. GARM is not yet multi-user, so it's safe to assume we only have this + // one user. + adminCtx := context.Background() + adminCtx = auth.PopulateContext(adminCtx, adminUser) + + slog.Info("migrating credentials to DB") + slog.Info("creating github endpoints table") + if err := s.conn.AutoMigrate(&GithubEndpoint{}); err != nil { + return errors.Wrap(err, "migrating github endpoints") + } + + defer func() { + if err != nil { + slog.With(slog.Any("error", err)).Error("rolling back github github endpoints table") + s.conn.Migrator().DropTable(&GithubEndpoint{}) + } + }() + + slog.Info("creating github credentials table") + if err := s.conn.AutoMigrate(&GithubCredentials{}); err != nil { + return errors.Wrap(err, "migrating github credentials") + } + + defer func() { + if err != nil { + slog.With(slog.Any("error", err)).Error("rolling back github github credentials table") + s.conn.Migrator().DropTable(&GithubCredentials{}) + } + }() + + // Create the default Github endpoint. + createEndpointParams := params.CreateGithubEndpointParams{ + Name: "github.com", + Description: "The github.com endpoint", + APIBaseURL: appdefaults.GithubDefaultBaseURL, + BaseURL: appdefaults.DefaultGithubURL, + UploadBaseURL: appdefaults.GithubDefaultUploadBaseURL, + } + + if _, err := s.CreateGithubEndpoint(adminCtx, createEndpointParams); err != nil { + if !errors.Is(err, runnerErrors.ErrDuplicateEntity) { + return errors.Wrap(err, "creating default github endpoint") + } + } + + // Nothing to migrate. + if len(s.cfg.MigrateCredentials) == 0 { + return nil + } + + slog.Info("importing credentials from config") + for _, cred := range s.cfg.MigrateCredentials { + slog.Info("importing credential", "name", cred.Name) + parsed, err := url.Parse(cred.BaseEndpoint()) + if err != nil { + return errors.Wrap(err, "parsing base URL") + } + + certBundle, err := cred.CACertBundle() + if err != nil { + return errors.Wrap(err, "getting CA cert bundle") + } + hostname := parsed.Hostname() + createParams := params.CreateGithubEndpointParams{ + Name: hostname, + Description: fmt.Sprintf("Endpoint for %s", hostname), + APIBaseURL: cred.APIEndpoint(), + BaseURL: cred.BaseEndpoint(), + UploadBaseURL: cred.UploadEndpoint(), + CACertBundle: certBundle, + } + + var endpoint params.GithubEndpoint + endpoint, err = s.GetGithubEndpoint(adminCtx, hostname) + if err != nil { + if !errors.Is(err, runnerErrors.ErrNotFound) { + return errors.Wrap(err, "getting github endpoint") + } + endpoint, err = s.CreateGithubEndpoint(adminCtx, createParams) + if err != nil { + return errors.Wrap(err, "creating default github endpoint") + } + } + + credParams := params.CreateGithubCredentialsParams{ + Name: cred.Name, + Description: cred.Description, + AuthType: params.GithubAuthType(cred.AuthType), + } + switch credParams.AuthType { + case params.GithubAuthTypeApp: + keyBytes, err := cred.App.PrivateKeyBytes() + if err != nil { + return errors.Wrap(err, "getting private key bytes") + } + credParams.App = params.GithubApp{ + AppID: cred.App.AppID, + InstallationID: cred.App.InstallationID, + PrivateKeyBytes: keyBytes, + } + + if err := credParams.App.Validate(); err != nil { + return errors.Wrap(err, "validating app credentials") + } + case params.GithubAuthTypePAT: + if cred.PAT.OAuth2Token == "" { + return errors.New("missing OAuth2 token") + } + credParams.PAT = params.GithubPAT{ + OAuth2Token: cred.PAT.OAuth2Token, + } + } + + creds, err := s.CreateGithubCredentials(adminCtx, endpoint.Name, credParams) + if err != nil { + return errors.Wrap(err, "creating github credentials") + } + + if err := s.conn.Exec("update repositories set credentials_id = ?,endpoint_name = ? where credentials_name = ?", creds.ID, creds.Endpoint, creds.Name).Error; err != nil { + return errors.Wrap(err, "updating repositories") + } + + if err := s.conn.Exec("update organizations set credentials_id = ?,endpoint_name = ? where credentials_name = ?", creds.ID, creds.Endpoint, creds.Name).Error; err != nil { + return errors.Wrap(err, "updating organizations") + } + + if err := s.conn.Exec("update enterprises set credentials_id = ?,endpoint_name = ? where credentials_name = ?", creds.ID, creds.Endpoint, creds.Name).Error; err != nil { + return errors.Wrap(err, "updating enterprises") + } + } + return nil +} + func (s *sqlDatabase) migrateDB() error { if s.conn.Migrator().HasIndex(&Organization{}, "idx_organizations_name") { if err := s.conn.Migrator().DropIndex(&Organization{}, "idx_organizations_name"); err != nil { @@ -234,7 +387,15 @@ func (s *sqlDatabase) migrateDB() error { } } + var needsCredentialMigration bool + if !s.conn.Migrator().HasTable(&GithubCredentials{}) || !s.conn.Migrator().HasTable(&GithubEndpoint{}) { + needsCredentialMigration = true + } + s.conn.Exec("PRAGMA foreign_keys = OFF") if err := s.conn.AutoMigrate( + &User{}, + &GithubEndpoint{}, + &GithubCredentials{}, &Tag{}, &Pool{}, &Repository{}, @@ -244,11 +405,16 @@ func (s *sqlDatabase) migrateDB() error { &InstanceStatusUpdate{}, &Instance{}, &ControllerInfo{}, - &User{}, &WorkflowJob{}, ); err != nil { return errors.Wrap(err, "running auto migrate") } + s.conn.Exec("PRAGMA foreign_keys = ON") + if needsCredentialMigration { + if err := s.migrateCredentialsToDB(); err != nil { + return errors.Wrap(err, "migrating credentials") + } + } return nil } diff --git a/database/sql/users.go b/database/sql/users.go index 039d86fe..5fc47564 100644 --- a/database/sql/users.go +++ b/database/sql/users.go @@ -67,6 +67,10 @@ func (s *sqlDatabase) CreateUser(_ context.Context, user params.NewUserParams) ( return params.User{}, runnerErrors.NewConflictError("email already exists") } + if s.HasAdminUser(context.Background()) && user.IsAdmin { + return params.User{}, runnerErrors.NewBadRequestError("admin user already exists") + } + newUser := User{ Username: user.Username, Password: user.Password, @@ -129,3 +133,16 @@ func (s *sqlDatabase) UpdateUser(_ context.Context, user string, param params.Up return s.sqlToParamsUser(dbUser), nil } + +// GetAdminUser returns the system admin user. This is only for internal use. +func (s *sqlDatabase) GetAdminUser(_ context.Context) (params.User, error) { + var user User + q := s.conn.Model(&User{}).Where("is_admin = ?", true).First(&user) + if q.Error != nil { + if errors.Is(q.Error, gorm.ErrRecordNotFound) { + return params.User{}, runnerErrors.ErrNotFound + } + return params.User{}, errors.Wrap(q.Error, "fetching admin user") + } + return s.sqlToParamsUser(user), nil +} diff --git a/database/sql/util.go b/database/sql/util.go index aaea31fe..30946863 100644 --- a/database/sql/util.go +++ b/database/sql/util.go @@ -114,10 +114,16 @@ func (s *sqlDatabase) sqlToCommonOrganization(org Organization) (params.Organiza return params.Organization{}, errors.Wrap(err, "decrypting secret") } + creds, err := s.sqlToCommonGithubCredentials(org.Credentials) + if err != nil { + return params.Organization{}, errors.Wrap(err, "converting credentials") + } + ret := params.Organization{ ID: org.ID.String(), Name: org.Name, - CredentialsName: org.CredentialsName, + CredentialsName: creds.Name, + Credentials: creds, Pools: make([]params.Pool, len(org.Pools)), WebhookSecret: string(secret), PoolBalancerType: org.PoolBalancerType, @@ -146,10 +152,15 @@ func (s *sqlDatabase) sqlToCommonEnterprise(enterprise Enterprise) (params.Enter return params.Enterprise{}, errors.Wrap(err, "decrypting secret") } + creds, err := s.sqlToCommonGithubCredentials(enterprise.Credentials) + if err != nil { + return params.Enterprise{}, errors.Wrap(err, "converting credentials") + } ret := params.Enterprise{ ID: enterprise.ID.String(), Name: enterprise.Name, - CredentialsName: enterprise.CredentialsName, + CredentialsName: creds.Name, + Credentials: creds, Pools: make([]params.Pool, len(enterprise.Pools)), WebhookSecret: string(secret), PoolBalancerType: enterprise.PoolBalancerType, @@ -239,11 +250,16 @@ func (s *sqlDatabase) sqlToCommonRepository(repo Repository) (params.Repository, return params.Repository{}, errors.Wrap(err, "decrypting secret") } + creds, err := s.sqlToCommonGithubCredentials(repo.Credentials) + if err != nil { + return params.Repository{}, errors.Wrap(err, "converting credentials") + } ret := params.Repository{ ID: repo.ID.String(), Name: repo.Name, Owner: repo.Owner, - CredentialsName: repo.CredentialsName, + CredentialsName: creds.Name, + Credentials: creds, Pools: make([]params.Pool, len(repo.Pools)), WebhookSecret: string(secret), PoolBalancerType: repo.PoolBalancerType, diff --git a/params/params.go b/params/params.go index a2a44222..e6bf8fb6 100644 --- a/params/params.go +++ b/params/params.go @@ -16,6 +16,8 @@ package params import ( "bytes" + "context" + "crypto/tls" "crypto/x509" "encoding/json" "encoding/pem" @@ -23,8 +25,10 @@ import ( "net/http" "time" + "github.com/bradleyfalzon/ghinstallation/v2" "github.com/google/go-github/v57/github" "github.com/google/uuid" + "golang.org/x/oauth2" commonParams "github.com/cloudbase/garm-provider-common/params" "github.com/cloudbase/garm/util/appdefaults" @@ -398,6 +402,7 @@ type Repository struct { Name string `json:"name"` Pools []Pool `json:"pool,omitempty"` CredentialsName string `json:"credentials_name"` + Credentials GithubCredentials `json:"credentials"` PoolManagerStatus PoolManagerStatus `json:"pool_manager_status,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancing_type"` // Do not serialize sensitive info. @@ -439,6 +444,7 @@ type Organization struct { Name string `json:"name"` Pools []Pool `json:"pool,omitempty"` CredentialsName string `json:"credentials_name"` + Credentials GithubCredentials `json:"credentials"` PoolManagerStatus PoolManagerStatus `json:"pool_manager_status,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancing_type"` // Do not serialize sensitive info. @@ -480,6 +486,7 @@ type Enterprise struct { Name string `json:"name"` Pools []Pool `json:"pool,omitempty"` CredentialsName string `json:"credentials_name"` + Credentials GithubCredentials `json:"credentials"` PoolManagerStatus PoolManagerStatus `json:"pool_manager_status,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancing_type"` // Do not serialize sensitive info. @@ -545,6 +552,7 @@ type ControllerInfo struct { } type GithubCredentials struct { + ID uint `json:"id"` Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` APIBaseURL string `json:"api_base_url"` @@ -552,7 +560,68 @@ type GithubCredentials struct { BaseURL string `json:"base_url"` CABundle []byte `json:"ca_bundle,omitempty"` AuthType GithubAuthType `toml:"auth_type" json:"auth-type"` - HTTPClient *http.Client `json:"-"` + + Repositories []Repository `json:"repositories,omitempty"` + Organizations []Organization `json:"organizations,omitempty"` + Enterprises []Enterprise `json:"enterprises,omitempty"` + Endpoint string `json:"endpoint"` + + CredentialsPayload []byte `json:"-"` + HTTPClient *http.Client `json:"-"` +} + +func (g GithubCredentials) GetHTTPClient(ctx context.Context) (*http.Client, error) { + var roots *x509.CertPool + if g.CABundle != nil { + roots = x509.NewCertPool() + ok := roots.AppendCertsFromPEM(g.CABundle) + if !ok { + return nil, fmt.Errorf("failed to parse CA cert") + } + } + // nolint:golangci-lint,gosec,godox + // TODO: set TLS MinVersion + httpTransport := &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: roots, + }, + } + + var tc *http.Client + switch g.AuthType { + case GithubAuthTypeApp: + var app GithubApp + if err := json.Unmarshal(g.CredentialsPayload, &app); err != nil { + return nil, fmt.Errorf("failed to unmarshal github app credentials: %w", err) + } + if app.AppID == 0 || app.InstallationID == 0 || len(app.PrivateKeyBytes) == 0 { + return nil, fmt.Errorf("github app credentials are missing required fields") + } + itr, err := ghinstallation.New(httpTransport, app.AppID, app.InstallationID, app.PrivateKeyBytes) + if err != nil { + return nil, fmt.Errorf("failed to create github app installation transport: %w", err) + } + + tc = &http.Client{Transport: itr} + default: + var pat GithubPAT + if err := json.Unmarshal(g.CredentialsPayload, &pat); err != nil { + return nil, fmt.Errorf("failed to unmarshal github app credentials: %w", err) + } + httpClient := &http.Client{Transport: httpTransport} + ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) + + if pat.OAuth2Token == "" { + return nil, fmt.Errorf("github credentials are missing the OAuth2 token") + } + + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: pat.OAuth2Token}, + ) + tc = oauth2.NewClient(ctx, ts) + } + + return tc, nil } func (g GithubCredentials) RootCertificateBundle() (CertificateBundle, error) { @@ -700,10 +769,11 @@ type UpdateSystemInfoParams struct { } type GithubEntity struct { - Owner string `json:"owner"` - Name string `json:"name"` - ID string `json:"id"` - EntityType GithubEntityType `json:"entity_type"` + Owner string `json:"owner"` + Name string `json:"name"` + ID string `json:"id"` + EntityType GithubEntityType `json:"entity_type"` + Credentials GithubCredentials `json:"credentials"` WebhookSecret string `json:"-"` } @@ -729,3 +799,14 @@ func (g GithubEntity) String() string { } return "" } + +type GithubEndpoint struct { + Name string `json:"name"` + Description string `json:"description"` + APIBaseURL string `json:"api_base_url"` + UploadBaseURL string `json:"upload_base_url"` + BaseURL string `json:"base_url"` + CACertBundle []byte `json:"ca_cert_bundle"` + + Credentials []GithubCredentials `json:"credentials"` +} diff --git a/params/requests.go b/params/requests.go index 885ed678..5da66d53 100644 --- a/params/requests.go +++ b/params/requests.go @@ -15,7 +15,9 @@ package params import ( + "crypto/x509" "encoding/json" + "encoding/pem" "fmt" "github.com/cloudbase/garm-provider-common/errors" @@ -265,3 +267,68 @@ type InstanceUpdateMessage struct { Message string `json:"message"` AgentID *int64 `json:"agent_id,omitempty"` } + +type CreateGithubEndpointParams struct { + Name string `json:"name"` + Description string `json:"description"` + APIBaseURL string `json:"api_base_url"` + UploadBaseURL string `json:"upload_base_url"` + BaseURL string `json:"base_url"` + CACertBundle []byte `json:"ca_cert_bundle"` +} + +type UpdateGithubEndpointParams struct { + Description *string `json:"description"` + APIBaseURL *string `json:"api_base_url"` + UploadBaseURL *string `json:"upload_base_url"` + BaseURL *string `json:"base_url"` + CACertBundle []byte `json:"ca_cert_bundle"` +} + +type GithubPAT struct { + OAuth2Token string `json:"oauth2_token"` +} + +type GithubApp struct { + AppID int64 `json:"app_id"` + InstallationID int64 `json:"installation_id"` + PrivateKeyBytes []byte `json:"private_key_bytes"` +} + +func (g GithubApp) Validate() error { + if g.AppID == 0 { + return errors.NewBadRequestError("missing app_id") + } + + if g.InstallationID == 0 { + return errors.NewBadRequestError("missing installation_id") + } + + if len(g.PrivateKeyBytes) == 0 { + return errors.NewBadRequestError("missing private_key_bytes") + } + + block, _ := pem.Decode(g.PrivateKeyBytes) + // Parse the private key as PCKS1 + _, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return fmt.Errorf("parsing private_key_path: %w", err) + } + + return nil +} + +type CreateGithubCredentialsParams struct { + Name string `json:"name"` + Description string `json:"description"` + AuthType GithubAuthType `json:"auth_type"` + PAT GithubPAT `json:"pat,omitempty"` + App GithubApp `json:"app,omitempty"` +} + +type UpdateGithubCredentialsParams struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + PAT *GithubPAT `json:"pat,omitempty"` + App *GithubApp `json:"app,omitempty"` +} diff --git a/runner/enterprises_test.go b/runner/enterprises_test.go index dc81da5e..2ad54e5d 100644 --- a/runner/enterprises_test.go +++ b/runner/enterprises_test.go @@ -169,7 +169,7 @@ func (s *EnterpriseTestSuite) TestCreateEnterprise() { s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T()) s.Require().Nil(err) s.Require().Equal(s.Fixtures.CreateEnterpriseParams.Name, enterprise.Name) - s.Require().Equal(s.Fixtures.Credentials[s.Fixtures.CreateEnterpriseParams.CredentialsName].Name, enterprise.CredentialsName) + s.Require().Equal(s.Fixtures.Credentials[s.Fixtures.CreateEnterpriseParams.CredentialsName].Name, enterprise.Credentials.Name) s.Require().Equal(params.PoolBalancerTypeRoundRobin, enterprise.PoolBalancerType) } @@ -306,7 +306,7 @@ func (s *EnterpriseTestSuite) TestUpdateEnterprise() { s.Fixtures.PoolMgrMock.AssertExpectations(s.T()) s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T()) s.Require().Nil(err) - s.Require().Equal(s.Fixtures.UpdateRepoParams.CredentialsName, org.CredentialsName) + s.Require().Equal(s.Fixtures.UpdateRepoParams.CredentialsName, org.Credentials.Name) s.Require().Equal(s.Fixtures.UpdateRepoParams.WebhookSecret, org.WebhookSecret) s.Require().Equal(params.PoolBalancerTypePack, org.PoolBalancerType) } diff --git a/runner/organizations_test.go b/runner/organizations_test.go index d0113756..30a58882 100644 --- a/runner/organizations_test.go +++ b/runner/organizations_test.go @@ -169,7 +169,7 @@ func (s *OrgTestSuite) TestCreateOrganization() { s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T()) s.Require().Nil(err) s.Require().Equal(s.Fixtures.CreateOrgParams.Name, org.Name) - s.Require().Equal(s.Fixtures.Credentials[s.Fixtures.CreateOrgParams.CredentialsName].Name, org.CredentialsName) + s.Require().Equal(s.Fixtures.Credentials[s.Fixtures.CreateOrgParams.CredentialsName].Name, org.Credentials.Name) s.Require().Equal(params.PoolBalancerTypeRoundRobin, org.PoolBalancerType) } @@ -317,7 +317,7 @@ func (s *OrgTestSuite) TestUpdateOrganization() { s.Fixtures.PoolMgrMock.AssertExpectations(s.T()) s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T()) s.Require().Nil(err) - s.Require().Equal(s.Fixtures.UpdateRepoParams.CredentialsName, org.CredentialsName) + s.Require().Equal(s.Fixtures.UpdateRepoParams.CredentialsName, org.Credentials.Name) s.Require().Equal(s.Fixtures.UpdateRepoParams.WebhookSecret, org.WebhookSecret) } diff --git a/runner/repositories_test.go b/runner/repositories_test.go index 20814a86..74bc8a76 100644 --- a/runner/repositories_test.go +++ b/runner/repositories_test.go @@ -81,7 +81,7 @@ func (s *RepoTestSuite) SetupTest() { params.PoolBalancerTypeRoundRobin, ) if err != nil { - s.FailNow(fmt.Sprintf("failed to create database object (test-repo-%v)", i)) + s.FailNow(fmt.Sprintf("failed to create database object (test-repo-%v): %q", i, err)) } repos[name] = repo } @@ -170,7 +170,7 @@ func (s *RepoTestSuite) TestCreateRepository() { s.Require().Nil(err) s.Require().Equal(s.Fixtures.CreateRepoParams.Owner, repo.Owner) s.Require().Equal(s.Fixtures.CreateRepoParams.Name, repo.Name) - s.Require().Equal(s.Fixtures.Credentials[s.Fixtures.CreateRepoParams.CredentialsName].Name, repo.CredentialsName) + s.Require().Equal(s.Fixtures.Credentials[s.Fixtures.CreateRepoParams.CredentialsName].Name, repo.Credentials.Name) s.Require().Equal(params.PoolBalancerTypeRoundRobin, repo.PoolBalancerType) } @@ -190,7 +190,7 @@ func (s *RepoTestSuite) TestCreareRepositoryPoolBalancerTypePack() { s.Require().Nil(err) s.Require().Equal(param.Owner, repo.Owner) s.Require().Equal(param.Name, repo.Name) - s.Require().Equal(s.Fixtures.Credentials[s.Fixtures.CreateRepoParams.CredentialsName].Name, repo.CredentialsName) + s.Require().Equal(s.Fixtures.Credentials[s.Fixtures.CreateRepoParams.CredentialsName].Name, repo.Credentials.Name) s.Require().Equal(params.PoolBalancerTypePack, repo.PoolBalancerType) } @@ -327,7 +327,7 @@ func (s *RepoTestSuite) TestUpdateRepository() { s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T()) s.Fixtures.PoolMgrMock.AssertExpectations(s.T()) s.Require().Nil(err) - s.Require().Equal(s.Fixtures.UpdateRepoParams.CredentialsName, repo.CredentialsName) + s.Require().Equal(s.Fixtures.UpdateRepoParams.CredentialsName, repo.Credentials.Name) s.Require().Equal(s.Fixtures.UpdateRepoParams.WebhookSecret, repo.WebhookSecret) s.Require().Equal(params.PoolBalancerTypeRoundRobin, repo.PoolBalancerType) } @@ -343,7 +343,7 @@ func (s *RepoTestSuite) TestUpdateRepositoryBalancingType() { s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T()) s.Fixtures.PoolMgrMock.AssertExpectations(s.T()) s.Require().Nil(err) - s.Require().Equal(updateRepoParams.CredentialsName, repo.CredentialsName) + s.Require().Equal(updateRepoParams.CredentialsName, repo.Credentials.Name) s.Require().Equal(updateRepoParams.WebhookSecret, repo.WebhookSecret) s.Require().Equal(params.PoolBalancerTypePack, repo.PoolBalancerType) } diff --git a/runner/runner.go b/runner/runner.go index 1ffa508a..fd10ad78 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -67,7 +67,6 @@ func NewRunner(ctx context.Context, cfg config.Config, db dbCommon.Store) (*Runn poolManagerCtrl := &poolManagerCtrl{ controllerID: ctrlID.ControllerID.String(), config: cfg, - credentials: creds, repositories: map[string]common.PoolManager{}, organizations: map[string]common.PoolManager{}, enterprises: map[string]common.PoolManager{}, @@ -94,7 +93,6 @@ type poolManagerCtrl struct { controllerID string config config.Config - credentials map[string]config.Github repositories map[string]common.PoolManager organizations map[string]common.PoolManager @@ -105,7 +103,7 @@ func (p *poolManagerCtrl) CreateRepoPoolManager(ctx context.Context, repo params p.mux.Lock() defer p.mux.Unlock() - cfgInternal, err := p.getInternalConfig(ctx, repo.CredentialsName, repo.GetBalancerType()) + cfgInternal, err := p.getInternalConfig(ctx, repo.Credentials, repo.GetBalancerType()) if err != nil { return nil, errors.Wrap(err, "fetching internal config") } @@ -133,7 +131,7 @@ func (p *poolManagerCtrl) UpdateRepoPoolManager(ctx context.Context, repo params return nil, errors.Wrapf(runnerErrors.ErrNotFound, "repository %s/%s pool manager not loaded", repo.Owner, repo.Name) } - internalCfg, err := p.getInternalConfig(ctx, repo.CredentialsName, repo.GetBalancerType()) + internalCfg, err := p.getInternalConfig(ctx, repo.Credentials, repo.GetBalancerType()) if err != nil { return nil, errors.Wrap(err, "fetching internal config") } @@ -178,7 +176,7 @@ func (p *poolManagerCtrl) CreateOrgPoolManager(ctx context.Context, org params.O p.mux.Lock() defer p.mux.Unlock() - cfgInternal, err := p.getInternalConfig(ctx, org.CredentialsName, org.GetBalancerType()) + cfgInternal, err := p.getInternalConfig(ctx, org.Credentials, org.GetBalancerType()) if err != nil { return nil, errors.Wrap(err, "fetching internal config") } @@ -205,7 +203,7 @@ func (p *poolManagerCtrl) UpdateOrgPoolManager(ctx context.Context, org params.O return nil, errors.Wrapf(runnerErrors.ErrNotFound, "org %s pool manager not loaded", org.Name) } - internalCfg, err := p.getInternalConfig(ctx, org.CredentialsName, org.GetBalancerType()) + internalCfg, err := p.getInternalConfig(ctx, org.Credentials, org.GetBalancerType()) if err != nil { return nil, errors.Wrap(err, "fetching internal config") } @@ -250,7 +248,7 @@ func (p *poolManagerCtrl) CreateEnterprisePoolManager(ctx context.Context, enter p.mux.Lock() defer p.mux.Unlock() - cfgInternal, err := p.getInternalConfig(ctx, enterprise.CredentialsName, enterprise.GetBalancerType()) + cfgInternal, err := p.getInternalConfig(ctx, enterprise.Credentials, enterprise.GetBalancerType()) if err != nil { return nil, errors.Wrap(err, "fetching internal config") } @@ -278,7 +276,7 @@ func (p *poolManagerCtrl) UpdateEnterprisePoolManager(ctx context.Context, enter return nil, errors.Wrapf(runnerErrors.ErrNotFound, "enterprise %s pool manager not loaded", enterprise.Name) } - internalCfg, err := p.getInternalConfig(ctx, enterprise.CredentialsName, enterprise.GetBalancerType()) + internalCfg, err := p.getInternalConfig(ctx, enterprise.Credentials, enterprise.GetBalancerType()) if err != nil { return nil, errors.Wrap(err, "fetching internal config") } @@ -319,22 +317,12 @@ func (p *poolManagerCtrl) GetEnterprisePoolManagers() (map[string]common.PoolMan return p.enterprises, nil } -func (p *poolManagerCtrl) getInternalConfig(ctx context.Context, credsName string, poolBalancerType params.PoolBalancerType) (params.Internal, error) { - creds, ok := p.credentials[credsName] - if !ok { - return params.Internal{}, runnerErrors.NewBadRequestError("invalid credential name (%s)", credsName) - } - - caBundle, err := creds.CACertBundle() - if err != nil { - return params.Internal{}, fmt.Errorf("fetching CA bundle for creds: %w", err) - } - +func (p *poolManagerCtrl) getInternalConfig(ctx context.Context, creds params.GithubCredentials, poolBalancerType params.PoolBalancerType) (params.Internal, error) { var controllerWebhookURL string if p.config.Default.WebhookURL != "" { controllerWebhookURL = fmt.Sprintf("%s/%s", p.config.Default.WebhookURL, p.controllerID) } - httpClient, err := creds.HTTPClient(ctx) + httpClient, err := creds.GetHTTPClient(ctx) if err != nil { return params.Internal{}, fmt.Errorf("fetching http client for creds: %w", err) } @@ -349,10 +337,10 @@ func (p *poolManagerCtrl) getInternalConfig(ctx context.Context, credsName strin GithubCredentialsDetails: params.GithubCredentials{ Name: creds.Name, Description: creds.Description, - BaseURL: creds.BaseEndpoint(), - APIBaseURL: creds.APIEndpoint(), - UploadBaseURL: creds.UploadEndpoint(), - CABundle: caBundle, + BaseURL: creds.BaseURL, + APIBaseURL: creds.APIBaseURL, + UploadBaseURL: creds.UploadBaseURL, + CABundle: creds.CABundle, HTTPClient: httpClient, }, }, nil From 032d40f5f91c691a0f4407ccbab4d095e25206e6 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Tue, 16 Apr 2024 17:05:18 +0000 Subject: [PATCH 02/27] Fix tests Signed-off-by: Gabriel Adrian Samfira --- database/sql/enterprise.go | 109 ++++++++++++------ database/sql/enterprise_test.go | 167 ++++++++++++++++----------- database/sql/github.go | 15 ++- database/sql/instances_test.go | 65 ++++++----- database/sql/organizations.go | 102 +++++++++++------ database/sql/organizations_test.go | 176 ++++++++++++++++------------ database/sql/pools_test.go | 27 +++-- database/sql/repositories.go | 87 ++++++++------ database/sql/repositories_test.go | 177 +++++++++++++++++------------ database/sql/util.go | 51 +++++---- internal/testing/testing.go | 60 ++++++++++ runner/enterprises.go | 18 +-- runner/enterprises_test.go | 46 ++++---- runner/organizations.go | 18 +-- runner/organizations_test.go | 40 ++++--- runner/pools_test.go | 22 +++- runner/repositories.go | 18 +-- runner/repositories_test.go | 38 ++++--- runner/runner.go | 4 +- 19 files changed, 751 insertions(+), 489 deletions(-) diff --git a/database/sql/enterprise.go b/database/sql/enterprise.go index e7270faf..e8efcf8b 100644 --- a/database/sql/enterprise.go +++ b/database/sql/enterprise.go @@ -1,3 +1,17 @@ +// Copyright 2024 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 sql import ( @@ -12,7 +26,7 @@ import ( "github.com/cloudbase/garm/params" ) -func (s *sqlDatabase) CreateEnterprise(_ context.Context, name, credentialsName, webhookSecret string, poolBalancerType params.PoolBalancerType) (params.Enterprise, error) { +func (s *sqlDatabase) CreateEnterprise(ctx context.Context, name, credentialsName, webhookSecret string, poolBalancerType params.PoolBalancerType) (params.Enterprise, error) { if webhookSecret == "" { return params.Enterprise{}, errors.New("creating enterprise: missing secret") } @@ -26,13 +40,27 @@ func (s *sqlDatabase) CreateEnterprise(_ context.Context, name, credentialsName, CredentialsName: credentialsName, PoolBalancerType: poolBalancerType, } + err = s.conn.Transaction(func(tx *gorm.DB) error { + creds, err := s.getGithubCredentialsByName(ctx, tx, credentialsName, false) + if err != nil { + return errors.Wrap(err, "creating enterprise") + } + newEnterprise.CredentialsID = &creds.ID - q := s.conn.Create(&newEnterprise) - if q.Error != nil { - return params.Enterprise{}, errors.Wrap(q.Error, "creating enterprise") + q := tx.Create(&newEnterprise) + if q.Error != nil { + return errors.Wrap(q.Error, "creating enterprise") + } + + newEnterprise.Credentials = creds + + return nil + }) + if err != nil { + return params.Enterprise{}, errors.Wrap(err, "creating enterprise") } - param, err := s.sqlToCommonEnterprise(newEnterprise) + param, err := s.sqlToCommonEnterprise(newEnterprise, true) if err != nil { return params.Enterprise{}, errors.Wrap(err, "creating enterprise") } @@ -46,7 +74,7 @@ func (s *sqlDatabase) GetEnterprise(ctx context.Context, name string) (params.En return params.Enterprise{}, errors.Wrap(err, "fetching enterprise") } - param, err := s.sqlToCommonEnterprise(enterprise) + param, err := s.sqlToCommonEnterprise(enterprise, true) if err != nil { return params.Enterprise{}, errors.Wrap(err, "fetching enterprise") } @@ -54,12 +82,12 @@ func (s *sqlDatabase) GetEnterprise(ctx context.Context, name string) (params.En } func (s *sqlDatabase) GetEnterpriseByID(ctx context.Context, enterpriseID string) (params.Enterprise, error) { - enterprise, err := s.getEnterpriseByID(ctx, enterpriseID, "Pools", "Credentials", "Endpoint") + enterprise, err := s.getEnterpriseByID(ctx, s.conn, enterpriseID, "Pools", "Credentials", "Endpoint") if err != nil { return params.Enterprise{}, errors.Wrap(err, "fetching enterprise") } - param, err := s.sqlToCommonEnterprise(enterprise) + param, err := s.sqlToCommonEnterprise(enterprise, true) if err != nil { return params.Enterprise{}, errors.Wrap(err, "fetching enterprise") } @@ -76,7 +104,7 @@ func (s *sqlDatabase) ListEnterprises(_ context.Context) ([]params.Enterprise, e ret := make([]params.Enterprise, len(enterprises)) for idx, val := range enterprises { var err error - ret[idx], err = s.sqlToCommonEnterprise(val) + ret[idx], err = s.sqlToCommonEnterprise(val, true) if err != nil { return nil, errors.Wrap(err, "fetching enterprises") } @@ -86,7 +114,7 @@ func (s *sqlDatabase) ListEnterprises(_ context.Context) ([]params.Enterprise, e } func (s *sqlDatabase) DeleteEnterprise(ctx context.Context, enterpriseID string) error { - enterprise, err := s.getEnterpriseByID(ctx, enterpriseID) + enterprise, err := s.getEnterpriseByID(ctx, s.conn, enterpriseID) if err != nil { return errors.Wrap(err, "fetching enterprise") } @@ -100,33 +128,50 @@ func (s *sqlDatabase) DeleteEnterprise(ctx context.Context, enterpriseID string) } func (s *sqlDatabase) UpdateEnterprise(ctx context.Context, enterpriseID string, param params.UpdateEntityParams) (params.Enterprise, error) { - enterprise, err := s.getEnterpriseByID(ctx, enterpriseID, "Credentials", "Endpoint") - if err != nil { - return params.Enterprise{}, errors.Wrap(err, "fetching enterprise") - } + var enterprise Enterprise + var creds GithubCredentials + err := s.conn.Transaction(func(tx *gorm.DB) error { + var err error + enterprise, err = s.getEnterpriseByID(ctx, tx, enterpriseID, "Credentials", "Endpoint") + if err != nil { + return errors.Wrap(err, "fetching enterprise") + } - if param.CredentialsName != "" { - enterprise.CredentialsName = param.CredentialsName - } + if param.CredentialsName != "" { + creds, err = s.getGithubCredentialsByName(ctx, tx, param.CredentialsName, false) + if err != nil { + return errors.Wrap(err, "fetching credentials") + } + enterprise.CredentialsID = &creds.ID + } + if param.WebhookSecret != "" { + secret, err := util.Seal([]byte(param.WebhookSecret), []byte(s.cfg.Passphrase)) + if err != nil { + return errors.Wrap(err, "encoding secret") + } + enterprise.WebhookSecret = secret + } - if param.WebhookSecret != "" { - secret, err := util.Seal([]byte(param.WebhookSecret), []byte(s.cfg.Passphrase)) - if err != nil { - return params.Enterprise{}, errors.Wrap(err, "encoding secret") + if param.PoolBalancerType != "" { + enterprise.PoolBalancerType = param.PoolBalancerType } - enterprise.WebhookSecret = secret - } - if param.PoolBalancerType != "" { - enterprise.PoolBalancerType = param.PoolBalancerType - } + q := tx.Save(&enterprise) + if q.Error != nil { + return errors.Wrap(q.Error, "saving enterprise") + } - q := s.conn.Save(&enterprise) - if q.Error != nil { - return params.Enterprise{}, errors.Wrap(q.Error, "saving enterprise") + if creds.ID != 0 { + enterprise.Credentials = creds + } + + return nil + }) + if err != nil { + return params.Enterprise{}, errors.Wrap(err, "updating enterprise") } - newParams, err := s.sqlToCommonEnterprise(enterprise) + newParams, err := s.sqlToCommonEnterprise(enterprise, true) if err != nil { return params.Enterprise{}, errors.Wrap(err, "updating enterprise") } @@ -149,14 +194,14 @@ func (s *sqlDatabase) getEnterprise(_ context.Context, name string) (Enterprise, return enterprise, nil } -func (s *sqlDatabase) getEnterpriseByID(_ context.Context, id string, preload ...string) (Enterprise, error) { +func (s *sqlDatabase) getEnterpriseByID(_ context.Context, tx *gorm.DB, id string, preload ...string) (Enterprise, error) { u, err := uuid.Parse(id) if err != nil { return Enterprise{}, errors.Wrap(runnerErrors.ErrBadRequest, "parsing id") } var enterprise Enterprise - q := s.conn + q := tx if len(preload) > 0 { for _, field := range preload { q = q.Preload(field) diff --git a/database/sql/enterprise_test.go b/database/sql/enterprise_test.go index 7f22956b..3509f946 100644 --- a/database/sql/enterprise_test.go +++ b/database/sql/enterprise_test.go @@ -49,6 +49,11 @@ type EnterpriseTestSuite struct { Store dbCommon.Store StoreSQLMocked *sqlDatabase Fixtures *EnterpriseTestFixtures + + adminCtx context.Context + testCreds params.GithubCredentials + secondaryTestCreds params.GithubCredentials + githubEndpoint params.GithubEndpoint } func (s *EnterpriseTestSuite) equalInstancesByName(expected, actual []params.Instance) { @@ -77,18 +82,25 @@ func (s *EnterpriseTestSuite) SetupTest() { } s.Store = db + adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T()) + s.adminCtx = adminCtx + + s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) + s.testCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), s.githubEndpoint) + s.secondaryTestCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "secondary-creds", db, s.T(), s.githubEndpoint) + // create some enterprise objects in the database, for testing purposes enterprises := []params.Enterprise{} for i := 1; i <= 3; i++ { enterprise, err := db.CreateEnterprise( - context.Background(), + s.adminCtx, fmt.Sprintf("test-enterprise-%d", i), - fmt.Sprintf("test-creds-%d", i), + s.testCreds.Name, fmt.Sprintf("test-webhook-secret-%d", i), params.PoolBalancerTypeRoundRobin, ) if err != nil { - s.FailNow(fmt.Sprintf("failed to create database object (test-enterprise-%d)", i)) + s.FailNow(fmt.Sprintf("failed to create database object (test-enterprise-%d): %q", i, err)) } enterprises = append(enterprises, enterprise) @@ -124,7 +136,7 @@ func (s *EnterpriseTestSuite) SetupTest() { Enterprises: enterprises, CreateEnterpriseParams: params.CreateEnterpriseParams{ Name: "new-test-enterprise", - CredentialsName: "new-creds", + CredentialsName: s.testCreds.Name, WebhookSecret: "new-webhook-secret", }, CreatePoolParams: params.CreatePoolParams{ @@ -143,7 +155,7 @@ func (s *EnterpriseTestSuite) SetupTest() { OSType: "linux", }, UpdateRepoParams: params.UpdateEntityParams{ - CredentialsName: "test-update-creds", + CredentialsName: s.secondaryTestCreds.Name, WebhookSecret: "test-update-repo-webhook-secret", }, UpdatePoolParams: params.UpdatePoolParams{ @@ -160,7 +172,7 @@ func (s *EnterpriseTestSuite) SetupTest() { func (s *EnterpriseTestSuite) TestCreateEnterprise() { // call tested function enterprise, err := s.Store.CreateEnterprise( - context.Background(), + s.adminCtx, s.Fixtures.CreateEnterpriseParams.Name, s.Fixtures.CreateEnterpriseParams.CredentialsName, s.Fixtures.CreateEnterpriseParams.WebhookSecret, @@ -168,7 +180,7 @@ func (s *EnterpriseTestSuite) TestCreateEnterprise() { // assertions s.Require().Nil(err) - storeEnterprise, err := s.Store.GetEnterpriseByID(context.Background(), enterprise.ID) + storeEnterprise, err := s.Store.GetEnterpriseByID(s.adminCtx, enterprise.ID) if err != nil { s.FailNow(fmt.Sprintf("failed to get enterprise by id: %v", err)) } @@ -191,7 +203,7 @@ func (s *EnterpriseTestSuite) TestCreateEnterpriseInvalidDBPassphrase() { } _, err = sqlDB.CreateEnterprise( - context.Background(), + s.adminCtx, s.Fixtures.CreateEnterpriseParams.Name, s.Fixtures.CreateEnterpriseParams.CredentialsName, s.Fixtures.CreateEnterpriseParams.WebhookSecret, @@ -203,25 +215,29 @@ func (s *EnterpriseTestSuite) TestCreateEnterpriseInvalidDBPassphrase() { func (s *EnterpriseTestSuite) TestCreateEnterpriseDBCreateErr() { s.Fixtures.SQLMock.ExpectBegin() + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). + WithArgs(s.Fixtures.Enterprises[0].CredentialsName). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.testCreds.ID)) s.Fixtures.SQLMock. ExpectExec(regexp.QuoteMeta("INSERT INTO `enterprises`")). WillReturnError(fmt.Errorf("creating enterprise mock error")) s.Fixtures.SQLMock.ExpectRollback() _, err := s.StoreSQLMocked.CreateEnterprise( - context.Background(), + s.adminCtx, s.Fixtures.CreateEnterpriseParams.Name, s.Fixtures.CreateEnterpriseParams.CredentialsName, s.Fixtures.CreateEnterpriseParams.WebhookSecret, params.PoolBalancerTypeRoundRobin) - s.assertSQLMockExpectations() s.Require().NotNil(err) - s.Require().Equal("creating enterprise: creating enterprise mock error", err.Error()) + s.Require().Equal("creating enterprise: creating enterprise: creating enterprise mock error", err.Error()) + s.assertSQLMockExpectations() } func (s *EnterpriseTestSuite) TestGetEnterprise() { - enterprise, err := s.Store.GetEnterprise(context.Background(), s.Fixtures.Enterprises[0].Name) + enterprise, err := s.Store.GetEnterprise(s.adminCtx, s.Fixtures.Enterprises[0].Name) s.Require().Nil(err) s.Require().Equal(s.Fixtures.Enterprises[0].Name, enterprise.Name) @@ -229,14 +245,14 @@ func (s *EnterpriseTestSuite) TestGetEnterprise() { } func (s *EnterpriseTestSuite) TestGetEnterpriseCaseInsensitive() { - enterprise, err := s.Store.GetEnterprise(context.Background(), "TeSt-eNtErPriSe-1") + enterprise, err := s.Store.GetEnterprise(s.adminCtx, "TeSt-eNtErPriSe-1") s.Require().Nil(err) s.Require().Equal("test-enterprise-1", enterprise.Name) } func (s *EnterpriseTestSuite) TestGetEnterpriseNotFound() { - _, err := s.Store.GetEnterprise(context.Background(), "dummy-name") + _, err := s.Store.GetEnterprise(s.adminCtx, "dummy-name") s.Require().NotNil(err) s.Require().Equal("fetching enterprise: not found", err.Error()) @@ -248,7 +264,7 @@ func (s *EnterpriseTestSuite) TestGetEnterpriseDBDecryptingErr() { WithArgs(s.Fixtures.Enterprises[0].Name, 1). WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow(s.Fixtures.Enterprises[0].Name)) - _, err := s.StoreSQLMocked.GetEnterprise(context.Background(), s.Fixtures.Enterprises[0].Name) + _, err := s.StoreSQLMocked.GetEnterprise(s.adminCtx, s.Fixtures.Enterprises[0].Name) s.assertSQLMockExpectations() s.Require().NotNil(err) @@ -256,7 +272,7 @@ func (s *EnterpriseTestSuite) TestGetEnterpriseDBDecryptingErr() { } func (s *EnterpriseTestSuite) TestListEnterprises() { - enterprises, err := s.Store.ListEnterprises(context.Background()) + enterprises, err := s.Store.ListEnterprises(s.adminCtx) s.Require().Nil(err) garmTesting.EqualDBEntityByName(s.T(), s.Fixtures.Enterprises, enterprises) @@ -267,7 +283,7 @@ func (s *EnterpriseTestSuite) TestListEnterprisesDBFetchErr() { ExpectQuery(regexp.QuoteMeta("SELECT * FROM `enterprises` WHERE `enterprises`.`deleted_at` IS NULL")). WillReturnError(fmt.Errorf("fetching user from database mock error")) - _, err := s.StoreSQLMocked.ListEnterprises(context.Background()) + _, err := s.StoreSQLMocked.ListEnterprises(s.adminCtx) s.assertSQLMockExpectations() s.Require().NotNil(err) @@ -275,16 +291,16 @@ func (s *EnterpriseTestSuite) TestListEnterprisesDBFetchErr() { } func (s *EnterpriseTestSuite) TestDeleteEnterprise() { - err := s.Store.DeleteEnterprise(context.Background(), s.Fixtures.Enterprises[0].ID) + err := s.Store.DeleteEnterprise(s.adminCtx, s.Fixtures.Enterprises[0].ID) s.Require().Nil(err) - _, err = s.Store.GetEnterpriseByID(context.Background(), s.Fixtures.Enterprises[0].ID) + _, err = s.Store.GetEnterpriseByID(s.adminCtx, s.Fixtures.Enterprises[0].ID) s.Require().NotNil(err) s.Require().Equal("fetching enterprise: not found", err.Error()) } func (s *EnterpriseTestSuite) TestDeleteEnterpriseInvalidEnterpriseID() { - err := s.Store.DeleteEnterprise(context.Background(), "dummy-enterprise-id") + err := s.Store.DeleteEnterprise(s.adminCtx, "dummy-enterprise-id") s.Require().NotNil(err) s.Require().Equal("fetching enterprise: parsing id: invalid request", err.Error()) @@ -302,15 +318,15 @@ func (s *EnterpriseTestSuite) TestDeleteEnterpriseDBDeleteErr() { WillReturnError(fmt.Errorf("mocked delete enterprise error")) s.Fixtures.SQLMock.ExpectRollback() - err := s.StoreSQLMocked.DeleteEnterprise(context.Background(), s.Fixtures.Enterprises[0].ID) + err := s.StoreSQLMocked.DeleteEnterprise(s.adminCtx, s.Fixtures.Enterprises[0].ID) - s.assertSQLMockExpectations() s.Require().NotNil(err) s.Require().Equal("deleting enterprise: mocked delete enterprise error", err.Error()) + s.assertSQLMockExpectations() } func (s *EnterpriseTestSuite) TestUpdateEnterprise() { - enterprise, err := s.Store.UpdateEnterprise(context.Background(), s.Fixtures.Enterprises[0].ID, s.Fixtures.UpdateRepoParams) + enterprise, err := s.Store.UpdateEnterprise(s.adminCtx, s.Fixtures.Enterprises[0].ID, s.Fixtures.UpdateRepoParams) s.Require().Nil(err) s.Require().Equal(s.Fixtures.UpdateRepoParams.CredentialsName, enterprise.Credentials.Name) @@ -318,70 +334,85 @@ func (s *EnterpriseTestSuite) TestUpdateEnterprise() { } func (s *EnterpriseTestSuite) TestUpdateEnterpriseInvalidEnterpriseID() { - _, err := s.Store.UpdateEnterprise(context.Background(), "dummy-enterprise-id", s.Fixtures.UpdateRepoParams) + _, err := s.Store.UpdateEnterprise(s.adminCtx, "dummy-enterprise-id", s.Fixtures.UpdateRepoParams) s.Require().NotNil(err) - s.Require().Equal("fetching enterprise: parsing id: invalid request", err.Error()) + s.Require().Equal("updating enterprise: fetching enterprise: parsing id: invalid request", err.Error()) } func (s *EnterpriseTestSuite) TestUpdateEnterpriseDBEncryptErr() { s.StoreSQLMocked.cfg.Passphrase = wrongPassphrase - + s.Fixtures.SQLMock.ExpectBegin() s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `enterprises` WHERE id = ? AND `enterprises`.`deleted_at` IS NULL ORDER BY `enterprises`.`id` LIMIT ?")). WithArgs(s.Fixtures.Enterprises[0].ID, 1). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Enterprises[0].ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). + WithArgs(s.secondaryTestCreds.Name). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) + s.Fixtures.SQLMock.ExpectRollback() - _, err := s.StoreSQLMocked.UpdateEnterprise(context.Background(), s.Fixtures.Enterprises[0].ID, s.Fixtures.UpdateRepoParams) + _, err := s.StoreSQLMocked.UpdateEnterprise(s.adminCtx, s.Fixtures.Enterprises[0].ID, s.Fixtures.UpdateRepoParams) - s.assertSQLMockExpectations() s.Require().NotNil(err) - s.Require().Equal("encoding secret: invalid passphrase length (expected length 32 characters)", err.Error()) + s.Require().Equal("updating enterprise: encoding secret: invalid passphrase length (expected length 32 characters)", err.Error()) + s.assertSQLMockExpectations() } func (s *EnterpriseTestSuite) TestUpdateEnterpriseDBSaveErr() { + s.Fixtures.SQLMock.ExpectBegin() s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `enterprises` WHERE id = ? AND `enterprises`.`deleted_at` IS NULL ORDER BY `enterprises`.`id` LIMIT ?")). WithArgs(s.Fixtures.Enterprises[0].ID, 1). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Enterprises[0].ID)) - s.Fixtures.SQLMock.ExpectBegin() + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). + WithArgs(s.secondaryTestCreds.Name). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) s.Fixtures.SQLMock. ExpectExec(("UPDATE `enterprises` SET")). WillReturnError(fmt.Errorf("saving enterprise mock error")) s.Fixtures.SQLMock.ExpectRollback() - _, err := s.StoreSQLMocked.UpdateEnterprise(context.Background(), s.Fixtures.Enterprises[0].ID, s.Fixtures.UpdateRepoParams) + _, err := s.StoreSQLMocked.UpdateEnterprise(s.adminCtx, s.Fixtures.Enterprises[0].ID, s.Fixtures.UpdateRepoParams) - s.assertSQLMockExpectations() s.Require().NotNil(err) - s.Require().Equal("saving enterprise: saving enterprise mock error", err.Error()) + s.Require().Equal("updating enterprise: saving enterprise: saving enterprise mock error", err.Error()) + s.assertSQLMockExpectations() } func (s *EnterpriseTestSuite) TestUpdateEnterpriseDBDecryptingErr() { s.StoreSQLMocked.cfg.Passphrase = wrongPassphrase s.Fixtures.UpdateRepoParams.WebhookSecret = webhookSecret + s.Fixtures.SQLMock.ExpectBegin() s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `enterprises` WHERE id = ? AND `enterprises`.`deleted_at` IS NULL ORDER BY `enterprises`.`id` LIMIT ?")). WithArgs(s.Fixtures.Enterprises[0].ID, 1). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Enterprises[0].ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). + WithArgs(s.secondaryTestCreds.Name). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) + s.Fixtures.SQLMock.ExpectRollback() - _, err := s.StoreSQLMocked.UpdateEnterprise(context.Background(), s.Fixtures.Enterprises[0].ID, s.Fixtures.UpdateRepoParams) + _, err := s.StoreSQLMocked.UpdateEnterprise(s.adminCtx, s.Fixtures.Enterprises[0].ID, s.Fixtures.UpdateRepoParams) - s.assertSQLMockExpectations() s.Require().NotNil(err) - s.Require().Equal("encoding secret: invalid passphrase length (expected length 32 characters)", err.Error()) + s.Require().Equal("updating enterprise: encoding secret: invalid passphrase length (expected length 32 characters)", err.Error()) + s.assertSQLMockExpectations() } func (s *EnterpriseTestSuite) TestGetEnterpriseByID() { - enterprise, err := s.Store.GetEnterpriseByID(context.Background(), s.Fixtures.Enterprises[0].ID) + enterprise, err := s.Store.GetEnterpriseByID(s.adminCtx, s.Fixtures.Enterprises[0].ID) s.Require().Nil(err) s.Require().Equal(s.Fixtures.Enterprises[0].ID, enterprise.ID) } func (s *EnterpriseTestSuite) TestGetEnterpriseByIDInvalidEnterpriseID() { - _, err := s.Store.GetEnterpriseByID(context.Background(), "dummy-enterprise-id") + _, err := s.Store.GetEnterpriseByID(s.adminCtx, "dummy-enterprise-id") s.Require().NotNil(err) s.Require().Equal("fetching enterprise: parsing id: invalid request", err.Error()) @@ -397,7 +428,7 @@ func (s *EnterpriseTestSuite) TestGetEnterpriseByIDDBDecryptingErr() { WithArgs(s.Fixtures.Enterprises[0].ID). WillReturnRows(sqlmock.NewRows([]string{"enterprise_id"}).AddRow(s.Fixtures.Enterprises[0].ID)) - _, err := s.StoreSQLMocked.GetEnterpriseByID(context.Background(), s.Fixtures.Enterprises[0].ID) + _, err := s.StoreSQLMocked.GetEnterpriseByID(s.adminCtx, s.Fixtures.Enterprises[0].ID) s.assertSQLMockExpectations() s.Require().NotNil(err) @@ -407,11 +438,11 @@ func (s *EnterpriseTestSuite) TestGetEnterpriseByIDDBDecryptingErr() { func (s *EnterpriseTestSuite) TestCreateEnterprisePool() { entity, err := s.Fixtures.Enterprises[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().Nil(err) - enterprise, err := s.Store.GetEnterpriseByID(context.Background(), s.Fixtures.Enterprises[0].ID) + enterprise, err := s.Store.GetEnterpriseByID(s.adminCtx, s.Fixtures.Enterprises[0].ID) if err != nil { s.FailNow(fmt.Sprintf("cannot get enterprise by ID: %v", err)) } @@ -426,7 +457,7 @@ func (s *EnterpriseTestSuite) TestCreateEnterprisePoolMissingTags() { s.Fixtures.CreatePoolParams.Tags = []string{} entity, err := s.Fixtures.Enterprises[0].GetEntity() s.Require().Nil(err) - _, err = s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("no tags specified", err.Error()) @@ -437,7 +468,7 @@ func (s *EnterpriseTestSuite) TestCreateEnterprisePoolInvalidEnterpriseID() { ID: "dummy-enterprise-id", EntityType: params.GithubEntityTypeEnterprise, } - _, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("parsing id: invalid request", err.Error()) @@ -455,7 +486,7 @@ func (s *EnterpriseTestSuite) TestCreateEnterprisePoolDBCreateErr() { entity, err := s.Fixtures.Enterprises[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("checking pool existence: mocked creating pool error", err.Error()) @@ -484,7 +515,7 @@ func (s *EnterpriseTestSuite) TestCreateEnterpriseDBPoolAlreadyExistErr() { entity, err := s.Fixtures.Enterprises[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal(runnerErrors.NewConflictError("pool with the same image and flavor already exists on this provider"), err) @@ -511,7 +542,7 @@ func (s *EnterpriseTestSuite) TestCreateEnterprisePoolDBFetchTagErr() { entity, err := s.Fixtures.Enterprises[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("creating tag: fetching tag from database: mocked fetching tag error", err.Error()) @@ -546,7 +577,7 @@ func (s *EnterpriseTestSuite) TestCreateEnterprisePoolDBAddingPoolErr() { entity, err := s.Fixtures.Enterprises[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("creating pool: mocked adding pool error", err.Error()) @@ -585,7 +616,7 @@ func (s *EnterpriseTestSuite) TestCreateEnterprisePoolDBSaveTagErr() { entity, err := s.Fixtures.Enterprises[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("associating tags: mocked saving tag error", err.Error()) @@ -633,7 +664,7 @@ func (s *EnterpriseTestSuite) TestCreateEnterprisePoolDBFetchPoolErr() { entity, err := s.Fixtures.Enterprises[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("fetching pool: not found", err.Error()) @@ -646,14 +677,14 @@ func (s *EnterpriseTestSuite) TestListEnterprisePools() { s.Require().Nil(err) for i := 1; i <= 2; i++ { s.Fixtures.CreatePoolParams.Flavor = fmt.Sprintf("test-flavor-%v", i) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create enterprise pool: %v", err)) } enterprisePools = append(enterprisePools, pool) } - pools, err := s.Store.ListEntityPools(context.Background(), entity) + pools, err := s.Store.ListEntityPools(s.adminCtx, entity) s.Require().Nil(err) garmTesting.EqualDBEntityID(s.T(), enterprisePools, pools) @@ -664,7 +695,7 @@ func (s *EnterpriseTestSuite) TestListEnterprisePoolsInvalidEnterpriseID() { ID: "dummy-enterprise-id", EntityType: params.GithubEntityTypeEnterprise, } - _, err := s.Store.ListEntityPools(context.Background(), entity) + _, err := s.Store.ListEntityPools(s.adminCtx, entity) s.Require().NotNil(err) s.Require().Equal("fetching pools: parsing id: invalid request", err.Error()) @@ -673,12 +704,12 @@ func (s *EnterpriseTestSuite) TestListEnterprisePoolsInvalidEnterpriseID() { func (s *EnterpriseTestSuite) TestGetEnterprisePool() { entity, err := s.Fixtures.Enterprises[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create enterprise pool: %v", err)) } - enterprisePool, err := s.Store.GetEntityPool(context.Background(), entity, pool.ID) + enterprisePool, err := s.Store.GetEntityPool(s.adminCtx, entity, pool.ID) s.Require().Nil(err) s.Require().Equal(enterprisePool.ID, pool.ID) @@ -689,7 +720,7 @@ func (s *EnterpriseTestSuite) TestGetEnterprisePoolInvalidEnterpriseID() { ID: "dummy-enterprise-id", EntityType: params.GithubEntityTypeEnterprise, } - _, err := s.Store.GetEntityPool(context.Background(), entity, "dummy-pool-id") + _, err := s.Store.GetEntityPool(s.adminCtx, entity, "dummy-pool-id") s.Require().NotNil(err) s.Require().Equal("fetching pool: parsing id: invalid request", err.Error()) @@ -698,15 +729,15 @@ func (s *EnterpriseTestSuite) TestGetEnterprisePoolInvalidEnterpriseID() { func (s *EnterpriseTestSuite) TestDeleteEnterprisePool() { entity, err := s.Fixtures.Enterprises[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create enterprise pool: %v", err)) } - err = s.Store.DeleteEntityPool(context.Background(), entity, pool.ID) + err = s.Store.DeleteEntityPool(s.adminCtx, entity, pool.ID) s.Require().Nil(err) - _, err = s.Store.GetEntityPool(context.Background(), entity, pool.ID) + _, err = s.Store.GetEntityPool(s.adminCtx, entity, pool.ID) s.Require().Equal("fetching pool: finding pool: not found", err.Error()) } @@ -715,7 +746,7 @@ func (s *EnterpriseTestSuite) TestDeleteEnterprisePoolInvalidEnterpriseID() { ID: "dummy-enterprise-id", EntityType: params.GithubEntityTypeEnterprise, } - err := s.Store.DeleteEntityPool(context.Background(), entity, "dummy-pool-id") + err := s.Store.DeleteEntityPool(s.adminCtx, entity, "dummy-pool-id") s.Require().NotNil(err) s.Require().Equal("parsing id: invalid request", err.Error()) @@ -724,7 +755,7 @@ func (s *EnterpriseTestSuite) TestDeleteEnterprisePoolInvalidEnterpriseID() { func (s *EnterpriseTestSuite) TestDeleteEnterprisePoolDBDeleteErr() { entity, err := s.Fixtures.Enterprises[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create enterprise pool: %v", err)) } @@ -736,7 +767,7 @@ func (s *EnterpriseTestSuite) TestDeleteEnterprisePoolDBDeleteErr() { WillReturnError(fmt.Errorf("mocked deleting pool error")) s.Fixtures.SQLMock.ExpectRollback() - err = s.StoreSQLMocked.DeleteEntityPool(context.Background(), entity, pool.ID) + err = s.StoreSQLMocked.DeleteEntityPool(s.adminCtx, entity, pool.ID) s.Require().NotNil(err) s.Require().Equal("removing pool: mocked deleting pool error", err.Error()) s.assertSQLMockExpectations() @@ -745,21 +776,21 @@ func (s *EnterpriseTestSuite) TestDeleteEnterprisePoolDBDeleteErr() { func (s *EnterpriseTestSuite) TestListEnterpriseInstances() { entity, err := s.Fixtures.Enterprises[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create enterprise pool: %v", err)) } poolInstances := []params.Instance{} for i := 1; i <= 3; i++ { s.Fixtures.CreateInstanceParams.Name = fmt.Sprintf("test-enterprise-%v", i) - instance, err := s.Store.CreateInstance(context.Background(), pool.ID, s.Fixtures.CreateInstanceParams) + instance, err := s.Store.CreateInstance(s.adminCtx, pool.ID, s.Fixtures.CreateInstanceParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create instance: %s", err)) } poolInstances = append(poolInstances, instance) } - instances, err := s.Store.ListEntityInstances(context.Background(), entity) + instances, err := s.Store.ListEntityInstances(s.adminCtx, entity) s.Require().Nil(err) s.equalInstancesByName(poolInstances, instances) @@ -770,7 +801,7 @@ func (s *EnterpriseTestSuite) TestListEnterpriseInstancesInvalidEnterpriseID() { ID: "dummy-enterprise-id", EntityType: params.GithubEntityTypeEnterprise, } - _, err := s.Store.ListEntityInstances(context.Background(), entity) + _, err := s.Store.ListEntityInstances(s.adminCtx, entity) s.Require().NotNil(err) s.Require().Equal("fetching entity: parsing id: invalid request", err.Error()) @@ -779,12 +810,12 @@ func (s *EnterpriseTestSuite) TestListEnterpriseInstancesInvalidEnterpriseID() { func (s *EnterpriseTestSuite) TestUpdateEnterprisePool() { entity, err := s.Fixtures.Enterprises[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create enterprise pool: %v", err)) } - pool, err = s.Store.UpdateEntityPool(context.Background(), entity, pool.ID, s.Fixtures.UpdatePoolParams) + pool, err = s.Store.UpdateEntityPool(s.adminCtx, entity, pool.ID, s.Fixtures.UpdatePoolParams) s.Require().Nil(err) s.Require().Equal(*s.Fixtures.UpdatePoolParams.MaxRunners, pool.MaxRunners) @@ -798,7 +829,7 @@ func (s *EnterpriseTestSuite) TestUpdateEnterprisePoolInvalidEnterpriseID() { ID: "dummy-enterprise-id", EntityType: params.GithubEntityTypeEnterprise, } - _, err := s.Store.UpdateEntityPool(context.Background(), entity, "dummy-pool-id", s.Fixtures.UpdatePoolParams) + _, err := s.Store.UpdateEntityPool(s.adminCtx, entity, "dummy-pool-id", s.Fixtures.UpdatePoolParams) s.Require().NotNil(err) s.Require().Equal("fetching pool: parsing id: invalid request", err.Error()) diff --git a/database/sql/github.go b/database/sql/github.go index 0087165f..300c0ef4 100644 --- a/database/sql/github.go +++ b/database/sql/github.go @@ -14,6 +14,9 @@ import ( ) func (s *sqlDatabase) sqlToCommonGithubCredentials(creds GithubCredentials) (params.GithubCredentials, error) { + if len(creds.Payload) == 0 { + return params.GithubCredentials{}, errors.New("empty credentials payload") + } data, err := util.Unseal(creds.Payload, []byte(s.cfg.Passphrase)) if err != nil { return params.GithubCredentials{}, errors.Wrap(err, "unsealing credentials") @@ -33,7 +36,7 @@ func (s *sqlDatabase) sqlToCommonGithubCredentials(creds GithubCredentials) (par } for _, repo := range creds.Repositories { - commonRepo, err := s.sqlToCommonRepository(repo) + commonRepo, err := s.sqlToCommonRepository(repo, false) if err != nil { return params.GithubCredentials{}, errors.Wrap(err, "converting github repository") } @@ -41,7 +44,7 @@ func (s *sqlDatabase) sqlToCommonGithubCredentials(creds GithubCredentials) (par } for _, org := range creds.Organizations { - commonOrg, err := s.sqlToCommonOrganization(org) + commonOrg, err := s.sqlToCommonOrganization(org, false) if err != nil { return params.GithubCredentials{}, errors.Wrap(err, "converting github organization") } @@ -49,9 +52,9 @@ func (s *sqlDatabase) sqlToCommonGithubCredentials(creds GithubCredentials) (par } for _, ent := range creds.Enterprises { - commonEnt, err := s.sqlToCommonEnterprise(ent) + commonEnt, err := s.sqlToCommonEnterprise(ent, false) if err != nil { - return params.GithubCredentials{}, errors.Wrap(err, "converting github enterprise") + return params.GithubCredentials{}, errors.Wrapf(err, "converting github enterprise: %s", ent.Name) } commonCreds.Enterprises = append(commonCreds.Enterprises, commonEnt) } @@ -73,12 +76,12 @@ func (s *sqlDatabase) sqlToCommonGithubEndpoint(ep GithubEndpoint) (params.Githu func getUIDFromContext(ctx context.Context) (uuid.UUID, error) { userID := auth.UserID(ctx) if userID == "" { - return uuid.Nil, errors.Wrap(runnerErrors.ErrUnauthorized, "creating github endpoint") + return uuid.Nil, errors.Wrap(runnerErrors.ErrUnauthorized, "getting UID from context") } asUUID, err := uuid.Parse(userID) if err != nil { - return uuid.Nil, errors.Wrap(runnerErrors.ErrUnauthorized, "creating github endpoint") + return uuid.Nil, errors.Wrap(runnerErrors.ErrUnauthorized, "parsing UID from context") } return asUUID, nil } diff --git a/database/sql/instances_test.go b/database/sql/instances_test.go index 74fd8c65..82b5cb46 100644 --- a/database/sql/instances_test.go +++ b/database/sql/instances_test.go @@ -48,6 +48,7 @@ type InstancesTestSuite struct { Store dbCommon.Store StoreSQLMocked *sqlDatabase Fixtures *InstancesTestFixtures + adminCtx context.Context } func (s *InstancesTestSuite) equalInstancesByName(expected, actual []params.Instance) { @@ -76,8 +77,14 @@ func (s *InstancesTestSuite) SetupTest() { } s.Store = db + adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T()) + s.adminCtx = adminCtx + + githubEndpoint := garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) + creds := garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), githubEndpoint) + // create an organization for testing purposes - org, err := s.Store.CreateOrganization(context.Background(), "test-org", "test-creds", "test-webhookSecret", params.PoolBalancerTypeRoundRobin) + org, err := s.Store.CreateOrganization(s.adminCtx, "test-org", creds.Name, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) if err != nil { s.FailNow(fmt.Sprintf("failed to create org: %s", err)) } @@ -94,7 +101,7 @@ func (s *InstancesTestSuite) SetupTest() { } entity, err := org.GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, createPoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, createPoolParams) if err != nil { s.FailNow(fmt.Sprintf("failed to create org pool: %s", err)) } @@ -103,7 +110,7 @@ func (s *InstancesTestSuite) SetupTest() { instances := []params.Instance{} for i := 1; i <= 3; i++ { instance, err := db.CreateInstance( - context.Background(), + s.adminCtx, pool.ID, params.CreateInstanceParams{ Name: fmt.Sprintf("test-instance-%d", i), @@ -179,11 +186,11 @@ func (s *InstancesTestSuite) SetupTest() { func (s *InstancesTestSuite) TestCreateInstance() { // call tested function - instance, err := s.Store.CreateInstance(context.Background(), s.Fixtures.Pool.ID, s.Fixtures.CreateInstanceParams) + instance, err := s.Store.CreateInstance(s.adminCtx, s.Fixtures.Pool.ID, s.Fixtures.CreateInstanceParams) // assertions s.Require().Nil(err) - storeInstance, err := s.Store.GetInstanceByName(context.Background(), s.Fixtures.CreateInstanceParams.Name) + storeInstance, err := s.Store.GetInstanceByName(s.adminCtx, s.Fixtures.CreateInstanceParams.Name) if err != nil { s.FailNow(fmt.Sprintf("failed to get instance: %v", err)) } @@ -195,7 +202,7 @@ func (s *InstancesTestSuite) TestCreateInstance() { } func (s *InstancesTestSuite) TestCreateInstanceInvalidPoolID() { - _, err := s.Store.CreateInstance(context.Background(), "dummy-pool-id", params.CreateInstanceParams{}) + _, err := s.Store.CreateInstance(s.adminCtx, "dummy-pool-id", params.CreateInstanceParams{}) s.Require().Equal("fetching pool: parsing id: invalid request", err.Error()) } @@ -216,7 +223,7 @@ func (s *InstancesTestSuite) TestCreateInstanceDBCreateErr() { WillReturnError(fmt.Errorf("mocked insert instance error")) s.Fixtures.SQLMock.ExpectRollback() - _, err := s.StoreSQLMocked.CreateInstance(context.Background(), pool.ID, s.Fixtures.CreateInstanceParams) + _, err := s.StoreSQLMocked.CreateInstance(s.adminCtx, pool.ID, s.Fixtures.CreateInstanceParams) s.assertSQLMockExpectations() s.Require().NotNil(err) @@ -226,7 +233,7 @@ func (s *InstancesTestSuite) TestCreateInstanceDBCreateErr() { func (s *InstancesTestSuite) TestGetPoolInstanceByName() { storeInstance := s.Fixtures.Instances[0] // this is already created in `SetupTest()` - instance, err := s.Store.GetPoolInstanceByName(context.Background(), s.Fixtures.Pool.ID, storeInstance.Name) + instance, err := s.Store.GetPoolInstanceByName(s.adminCtx, s.Fixtures.Pool.ID, storeInstance.Name) s.Require().Nil(err) s.Require().Equal(storeInstance.Name, instance.Name) @@ -237,7 +244,7 @@ func (s *InstancesTestSuite) TestGetPoolInstanceByName() { } func (s *InstancesTestSuite) TestGetPoolInstanceByNameNotFound() { - _, err := s.Store.GetPoolInstanceByName(context.Background(), s.Fixtures.Pool.ID, "not-existent-instance-name") + _, err := s.Store.GetPoolInstanceByName(s.adminCtx, s.Fixtures.Pool.ID, "not-existent-instance-name") s.Require().Equal("fetching instance: fetching pool instance by name: not found", err.Error()) } @@ -245,7 +252,7 @@ func (s *InstancesTestSuite) TestGetPoolInstanceByNameNotFound() { func (s *InstancesTestSuite) TestGetInstanceByName() { storeInstance := s.Fixtures.Instances[1] - instance, err := s.Store.GetInstanceByName(context.Background(), storeInstance.Name) + instance, err := s.Store.GetInstanceByName(s.adminCtx, storeInstance.Name) s.Require().Nil(err) s.Require().Equal(storeInstance.Name, instance.Name) @@ -256,7 +263,7 @@ func (s *InstancesTestSuite) TestGetInstanceByName() { } func (s *InstancesTestSuite) TestGetInstanceByNameFetchInstanceFailed() { - _, err := s.Store.GetInstanceByName(context.Background(), "not-existent-instance-name") + _, err := s.Store.GetInstanceByName(s.adminCtx, "not-existent-instance-name") s.Require().Equal("fetching instance: fetching instance by name: not found", err.Error()) } @@ -264,16 +271,16 @@ func (s *InstancesTestSuite) TestGetInstanceByNameFetchInstanceFailed() { func (s *InstancesTestSuite) TestDeleteInstance() { storeInstance := s.Fixtures.Instances[0] - err := s.Store.DeleteInstance(context.Background(), s.Fixtures.Pool.ID, storeInstance.Name) + err := s.Store.DeleteInstance(s.adminCtx, s.Fixtures.Pool.ID, storeInstance.Name) s.Require().Nil(err) - _, err = s.Store.GetPoolInstanceByName(context.Background(), s.Fixtures.Pool.ID, storeInstance.Name) + _, err = s.Store.GetPoolInstanceByName(s.adminCtx, s.Fixtures.Pool.ID, storeInstance.Name) s.Require().Equal("fetching instance: fetching pool instance by name: not found", err.Error()) } func (s *InstancesTestSuite) TestDeleteInstanceInvalidPoolID() { - err := s.Store.DeleteInstance(context.Background(), "dummy-pool-id", "dummy-instance-name") + err := s.Store.DeleteInstance(s.adminCtx, "dummy-pool-id", "dummy-instance-name") s.Require().Equal("deleting instance: fetching pool: parsing id: invalid request", err.Error()) } @@ -309,7 +316,7 @@ func (s *InstancesTestSuite) TestDeleteInstanceDBRecordNotFoundErr() { WillReturnError(gorm.ErrRecordNotFound) s.Fixtures.SQLMock.ExpectRollback() - err := s.StoreSQLMocked.DeleteInstance(context.Background(), pool.ID, instance.Name) + err := s.StoreSQLMocked.DeleteInstance(s.adminCtx, pool.ID, instance.Name) s.assertSQLMockExpectations() s.Require().Nil(err) @@ -346,7 +353,7 @@ func (s *InstancesTestSuite) TestDeleteInstanceDBDeleteErr() { WillReturnError(fmt.Errorf("mocked delete instance error")) s.Fixtures.SQLMock.ExpectRollback() - err := s.StoreSQLMocked.DeleteInstance(context.Background(), pool.ID, instance.Name) + err := s.StoreSQLMocked.DeleteInstance(s.adminCtx, pool.ID, instance.Name) s.assertSQLMockExpectations() s.Require().NotNil(err) @@ -357,10 +364,10 @@ func (s *InstancesTestSuite) TestAddInstanceEvent() { storeInstance := s.Fixtures.Instances[0] statusMsg := "test-status-message" - err := s.Store.AddInstanceEvent(context.Background(), storeInstance.Name, params.StatusEvent, params.EventInfo, statusMsg) + err := s.Store.AddInstanceEvent(s.adminCtx, storeInstance.Name, params.StatusEvent, params.EventInfo, statusMsg) s.Require().Nil(err) - instance, err := s.Store.GetInstanceByName(context.Background(), storeInstance.Name) + instance, err := s.Store.GetInstanceByName(s.adminCtx, storeInstance.Name) if err != nil { s.FailNow(fmt.Sprintf("failed to get db instance: %s", err)) } @@ -398,7 +405,7 @@ func (s *InstancesTestSuite) TestAddInstanceEventDBUpdateErr() { WillReturnError(fmt.Errorf("mocked add status message error")) s.Fixtures.SQLMock.ExpectRollback() - err := s.StoreSQLMocked.AddInstanceEvent(context.Background(), instance.Name, params.StatusEvent, params.EventInfo, statusMsg) + err := s.StoreSQLMocked.AddInstanceEvent(s.adminCtx, instance.Name, params.StatusEvent, params.EventInfo, statusMsg) s.Require().NotNil(err) s.Require().Equal("adding status message: mocked add status message error", err.Error()) @@ -406,7 +413,7 @@ func (s *InstancesTestSuite) TestAddInstanceEventDBUpdateErr() { } func (s *InstancesTestSuite) TestUpdateInstance() { - instance, err := s.Store.UpdateInstance(context.Background(), s.Fixtures.Instances[0].Name, s.Fixtures.UpdateInstanceParams) + instance, err := s.Store.UpdateInstance(s.adminCtx, s.Fixtures.Instances[0].Name, s.Fixtures.UpdateInstanceParams) s.Require().Nil(err) s.Require().Equal(s.Fixtures.UpdateInstanceParams.ProviderID, instance.ProviderID) @@ -443,7 +450,7 @@ func (s *InstancesTestSuite) TestUpdateInstanceDBUpdateInstanceErr() { WillReturnError(fmt.Errorf("mocked update instance error")) s.Fixtures.SQLMock.ExpectRollback() - _, err := s.StoreSQLMocked.UpdateInstance(context.Background(), instance.Name, s.Fixtures.UpdateInstanceParams) + _, err := s.StoreSQLMocked.UpdateInstance(s.adminCtx, instance.Name, s.Fixtures.UpdateInstanceParams) s.Require().NotNil(err) s.Require().Equal("updating instance: mocked update instance error", err.Error()) @@ -489,7 +496,7 @@ func (s *InstancesTestSuite) TestUpdateInstanceDBUpdateAddressErr() { WillReturnError(fmt.Errorf("update addresses mock error")) s.Fixtures.SQLMock.ExpectRollback() - _, err := s.StoreSQLMocked.UpdateInstance(context.Background(), instance.Name, s.Fixtures.UpdateInstanceParams) + _, err := s.StoreSQLMocked.UpdateInstance(s.adminCtx, instance.Name, s.Fixtures.UpdateInstanceParams) s.Require().NotNil(err) s.Require().Equal("updating addresses: update addresses mock error", err.Error()) @@ -497,20 +504,20 @@ func (s *InstancesTestSuite) TestUpdateInstanceDBUpdateAddressErr() { } func (s *InstancesTestSuite) TestListPoolInstances() { - instances, err := s.Store.ListPoolInstances(context.Background(), s.Fixtures.Pool.ID) + instances, err := s.Store.ListPoolInstances(s.adminCtx, s.Fixtures.Pool.ID) s.Require().Nil(err) s.equalInstancesByName(s.Fixtures.Instances, instances) } func (s *InstancesTestSuite) TestListPoolInstancesInvalidPoolID() { - _, err := s.Store.ListPoolInstances(context.Background(), "dummy-pool-id") + _, err := s.Store.ListPoolInstances(s.adminCtx, "dummy-pool-id") s.Require().Equal("parsing id: invalid request", err.Error()) } func (s *InstancesTestSuite) TestListAllInstances() { - instances, err := s.Store.ListAllInstances(context.Background()) + instances, err := s.Store.ListAllInstances(s.adminCtx) s.Require().Nil(err) s.equalInstancesByName(s.Fixtures.Instances, instances) @@ -521,7 +528,7 @@ func (s *InstancesTestSuite) TestListAllInstancesDBFetchErr() { ExpectQuery(regexp.QuoteMeta("SELECT * FROM `instances` WHERE `instances`.`deleted_at` IS NULL")). WillReturnError(fmt.Errorf("fetch instances mock error")) - _, err := s.StoreSQLMocked.ListAllInstances(context.Background()) + _, err := s.StoreSQLMocked.ListAllInstances(s.adminCtx) s.assertSQLMockExpectations() s.Require().NotNil(err) @@ -529,14 +536,14 @@ func (s *InstancesTestSuite) TestListAllInstancesDBFetchErr() { } func (s *InstancesTestSuite) TestPoolInstanceCount() { - instancesCount, err := s.Store.PoolInstanceCount(context.Background(), s.Fixtures.Pool.ID) + instancesCount, err := s.Store.PoolInstanceCount(s.adminCtx, s.Fixtures.Pool.ID) s.Require().Nil(err) s.Require().Equal(int64(len(s.Fixtures.Instances)), instancesCount) } func (s *InstancesTestSuite) TestPoolInstanceCountInvalidPoolID() { - _, err := s.Store.PoolInstanceCount(context.Background(), "dummy-pool-id") + _, err := s.Store.PoolInstanceCount(s.adminCtx, "dummy-pool-id") s.Require().Equal("fetching pool: parsing id: invalid request", err.Error()) } @@ -553,7 +560,7 @@ func (s *InstancesTestSuite) TestPoolInstanceCountDBCountErr() { WithArgs(pool.ID). WillReturnError(fmt.Errorf("count mock error")) - _, err := s.StoreSQLMocked.PoolInstanceCount(context.Background(), pool.ID) + _, err := s.StoreSQLMocked.PoolInstanceCount(s.adminCtx, pool.ID) s.assertSQLMockExpectations() s.Require().NotNil(err) diff --git a/database/sql/organizations.go b/database/sql/organizations.go index c67e85d4..0019ad39 100644 --- a/database/sql/organizations.go +++ b/database/sql/organizations.go @@ -27,13 +27,13 @@ import ( "github.com/cloudbase/garm/params" ) -func (s *sqlDatabase) CreateOrganization(_ context.Context, name, credentialsName, webhookSecret string, poolBalancerType params.PoolBalancerType) (params.Organization, error) { +func (s *sqlDatabase) CreateOrganization(ctx context.Context, name, credentialsName, webhookSecret string, poolBalancerType params.PoolBalancerType) (params.Organization, error) { if webhookSecret == "" { return params.Organization{}, errors.New("creating org: missing secret") } secret, err := util.Seal([]byte(webhookSecret), []byte(s.cfg.Passphrase)) if err != nil { - return params.Organization{}, fmt.Errorf("failed to encrypt string") + return params.Organization{}, errors.Wrap(err, "encoding secret") } newOrg := Organization{ Name: name, @@ -42,12 +42,27 @@ func (s *sqlDatabase) CreateOrganization(_ context.Context, name, credentialsNam PoolBalancerType: poolBalancerType, } - q := s.conn.Create(&newOrg) - if q.Error != nil { - return params.Organization{}, errors.Wrap(q.Error, "creating org") + err = s.conn.Transaction(func(tx *gorm.DB) error { + creds, err := s.getGithubCredentialsByName(ctx, tx, credentialsName, false) + if err != nil { + return errors.Wrap(err, "creating org") + } + newOrg.CredentialsID = &creds.ID + + q := tx.Create(&newOrg) + if q.Error != nil { + return errors.Wrap(q.Error, "creating org") + } + + newOrg.Credentials = creds + + return nil + }) + if err != nil { + return params.Organization{}, errors.Wrap(err, "creating org") } - param, err := s.sqlToCommonOrganization(newOrg) + param, err := s.sqlToCommonOrganization(newOrg, true) if err != nil { return params.Organization{}, errors.Wrap(err, "creating org") } @@ -62,7 +77,7 @@ func (s *sqlDatabase) GetOrganization(ctx context.Context, name string) (params. return params.Organization{}, errors.Wrap(err, "fetching org") } - param, err := s.sqlToCommonOrganization(org) + param, err := s.sqlToCommonOrganization(org, true) if err != nil { return params.Organization{}, errors.Wrap(err, "fetching org") } @@ -80,7 +95,7 @@ func (s *sqlDatabase) ListOrganizations(_ context.Context) ([]params.Organizatio ret := make([]params.Organization, len(orgs)) for idx, val := range orgs { var err error - ret[idx], err = s.sqlToCommonOrganization(val) + ret[idx], err = s.sqlToCommonOrganization(val, true) if err != nil { return nil, errors.Wrap(err, "fetching org") } @@ -90,7 +105,7 @@ func (s *sqlDatabase) ListOrganizations(_ context.Context) ([]params.Organizatio } func (s *sqlDatabase) DeleteOrganization(ctx context.Context, orgID string) error { - org, err := s.getOrgByID(ctx, orgID) + org, err := s.getOrgByID(ctx, s.conn, orgID) if err != nil { return errors.Wrap(err, "fetching org") } @@ -104,33 +119,52 @@ func (s *sqlDatabase) DeleteOrganization(ctx context.Context, orgID string) erro } func (s *sqlDatabase) UpdateOrganization(ctx context.Context, orgID string, param params.UpdateEntityParams) (params.Organization, error) { - org, err := s.getOrgByID(ctx, orgID, "Credentials", "Endpoint") - if err != nil { - return params.Organization{}, errors.Wrap(err, "fetching org") - } + var org Organization + var creds GithubCredentials + err := s.conn.Transaction(func(tx *gorm.DB) error { + var err error + org, err = s.getOrgByID(ctx, tx, orgID, "Credentials", "Endpoint") + if err != nil { + return errors.Wrap(err, "fetching org") + } - if param.CredentialsName != "" { - org.CredentialsName = param.CredentialsName - } + if param.CredentialsName != "" { + org.CredentialsName = param.CredentialsName + creds, err = s.getGithubCredentialsByName(ctx, tx, param.CredentialsName, false) + if err != nil { + return errors.Wrap(err, "fetching credentials") + } + org.CredentialsID = &creds.ID + } - if param.WebhookSecret != "" { - secret, err := util.Seal([]byte(param.WebhookSecret), []byte(s.cfg.Passphrase)) - if err != nil { - return params.Organization{}, fmt.Errorf("saving org: failed to encrypt string: %w", err) + if param.WebhookSecret != "" { + secret, err := util.Seal([]byte(param.WebhookSecret), []byte(s.cfg.Passphrase)) + if err != nil { + return fmt.Errorf("saving org: failed to encrypt string: %w", err) + } + org.WebhookSecret = secret } - org.WebhookSecret = secret - } - if param.PoolBalancerType != "" { - org.PoolBalancerType = param.PoolBalancerType - } + if param.PoolBalancerType != "" { + org.PoolBalancerType = param.PoolBalancerType + } - q := s.conn.Save(&org) - if q.Error != nil { - return params.Organization{}, errors.Wrap(q.Error, "saving org") + q := tx.Save(&org) + if q.Error != nil { + return errors.Wrap(q.Error, "saving org") + } + + if creds.ID != 0 { + org.Credentials = creds + } + + return nil + }) + if err != nil { + return params.Organization{}, errors.Wrap(err, "saving org") } - newParams, err := s.sqlToCommonOrganization(org) + newParams, err := s.sqlToCommonOrganization(org, true) if err != nil { return params.Organization{}, errors.Wrap(err, "saving org") } @@ -138,26 +172,26 @@ func (s *sqlDatabase) UpdateOrganization(ctx context.Context, orgID string, para } func (s *sqlDatabase) GetOrganizationByID(ctx context.Context, orgID string) (params.Organization, error) { - org, err := s.getOrgByID(ctx, orgID, "Pools", "Credentials", "Endpoint") + org, err := s.getOrgByID(ctx, s.conn, orgID, "Pools", "Credentials", "Endpoint") if err != nil { return params.Organization{}, errors.Wrap(err, "fetching org") } - param, err := s.sqlToCommonOrganization(org) + param, err := s.sqlToCommonOrganization(org, true) if err != nil { - return params.Organization{}, errors.Wrap(err, "fetching enterprise") + return params.Organization{}, errors.Wrap(err, "fetching org") } return param, nil } -func (s *sqlDatabase) getOrgByID(_ context.Context, id string, preload ...string) (Organization, error) { +func (s *sqlDatabase) getOrgByID(_ context.Context, db *gorm.DB, id string, preload ...string) (Organization, error) { u, err := uuid.Parse(id) if err != nil { return Organization{}, errors.Wrap(runnerErrors.ErrBadRequest, "parsing id") } var org Organization - q := s.conn + q := db if len(preload) > 0 { for _, field := range preload { q = q.Preload(field) diff --git a/database/sql/organizations_test.go b/database/sql/organizations_test.go index 28a049e5..55e791db 100644 --- a/database/sql/organizations_test.go +++ b/database/sql/organizations_test.go @@ -49,6 +49,11 @@ type OrgTestSuite struct { Store dbCommon.Store StoreSQLMocked *sqlDatabase Fixtures *OrgTestFixtures + + adminCtx context.Context + testCreds params.GithubCredentials + secondaryTestCreds params.GithubCredentials + githubEndpoint params.GithubEndpoint } func (s *OrgTestSuite) equalInstancesByName(expected, actual []params.Instance) { @@ -71,24 +76,32 @@ func (s *OrgTestSuite) assertSQLMockExpectations() { func (s *OrgTestSuite) SetupTest() { // create testing sqlite database - db, err := NewSQLDatabase(context.Background(), garmTesting.GetTestSqliteDBConfig(s.T())) + dbConfig := garmTesting.GetTestSqliteDBConfig(s.T()) + db, err := NewSQLDatabase(context.Background(), dbConfig) if err != nil { s.FailNow(fmt.Sprintf("failed to create db connection: %s", err)) } s.Store = db + adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T()) + s.adminCtx = adminCtx + + s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) + s.testCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), s.githubEndpoint) + s.secondaryTestCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "secondary-creds", db, s.T(), s.githubEndpoint) + // create some organization objects in the database, for testing purposes orgs := []params.Organization{} for i := 1; i <= 3; i++ { org, err := db.CreateOrganization( - context.Background(), + s.adminCtx, fmt.Sprintf("test-org-%d", i), - fmt.Sprintf("test-creds-%d", i), + s.testCreds.Name, fmt.Sprintf("test-webhook-secret-%d", i), params.PoolBalancerTypeRoundRobin, ) if err != nil { - s.FailNow(fmt.Sprintf("failed to create database object (test-org-%d)", i)) + s.FailNow(fmt.Sprintf("failed to create database object (test-org-%d): %q", i, err)) } orgs = append(orgs, org) @@ -114,7 +127,7 @@ func (s *OrgTestSuite) SetupTest() { } s.StoreSQLMocked = &sqlDatabase{ conn: gormConn, - cfg: garmTesting.GetTestSqliteDBConfig(s.T()), + cfg: dbConfig, } // setup test fixtures @@ -123,8 +136,8 @@ func (s *OrgTestSuite) SetupTest() { fixtures := &OrgTestFixtures{ Orgs: orgs, CreateOrgParams: params.CreateOrgParams{ - Name: "new-test-org", - CredentialsName: "new-creds", + Name: s.testCreds.Name, + CredentialsName: s.testCreds.Name, WebhookSecret: "new-webhook-secret", }, CreatePoolParams: params.CreatePoolParams{ @@ -143,7 +156,7 @@ func (s *OrgTestSuite) SetupTest() { OSType: "linux", }, UpdateRepoParams: params.UpdateEntityParams{ - CredentialsName: "test-update-creds", + CredentialsName: s.secondaryTestCreds.Name, WebhookSecret: "test-update-repo-webhook-secret", }, UpdatePoolParams: params.UpdatePoolParams{ @@ -160,7 +173,7 @@ func (s *OrgTestSuite) SetupTest() { func (s *OrgTestSuite) TestCreateOrganization() { // call tested function org, err := s.Store.CreateOrganization( - context.Background(), + s.adminCtx, s.Fixtures.CreateOrgParams.Name, s.Fixtures.CreateOrgParams.CredentialsName, s.Fixtures.CreateOrgParams.WebhookSecret, @@ -168,7 +181,7 @@ func (s *OrgTestSuite) TestCreateOrganization() { // assertions s.Require().Nil(err) - storeOrg, err := s.Store.GetOrganizationByID(context.Background(), org.ID) + storeOrg, err := s.Store.GetOrganizationByID(s.adminCtx, org.ID) if err != nil { s.FailNow(fmt.Sprintf("failed to get organization by id: %v", err)) } @@ -191,37 +204,41 @@ func (s *OrgTestSuite) TestCreateOrganizationInvalidDBPassphrase() { } _, err = sqlDB.CreateOrganization( - context.Background(), + s.adminCtx, s.Fixtures.CreateOrgParams.Name, s.Fixtures.CreateOrgParams.CredentialsName, s.Fixtures.CreateOrgParams.WebhookSecret, params.PoolBalancerTypeRoundRobin) s.Require().NotNil(err) - s.Require().Equal("failed to encrypt string", err.Error()) + s.Require().Equal("encoding secret: invalid passphrase length (expected length 32 characters)", err.Error()) } func (s *OrgTestSuite) TestCreateOrganizationDBCreateErr() { s.Fixtures.SQLMock.ExpectBegin() + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). + WithArgs(s.Fixtures.Orgs[0].CredentialsName). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.testCreds.ID)) s.Fixtures.SQLMock. ExpectExec(regexp.QuoteMeta("INSERT INTO `organizations`")). WillReturnError(fmt.Errorf("creating org mock error")) s.Fixtures.SQLMock.ExpectRollback() _, err := s.StoreSQLMocked.CreateOrganization( - context.Background(), + s.adminCtx, s.Fixtures.CreateOrgParams.Name, s.Fixtures.CreateOrgParams.CredentialsName, s.Fixtures.CreateOrgParams.WebhookSecret, params.PoolBalancerTypeRoundRobin) - s.assertSQLMockExpectations() s.Require().NotNil(err) - s.Require().Equal("creating org: creating org mock error", err.Error()) + s.Require().Equal("creating org: creating org: creating org mock error", err.Error()) + s.assertSQLMockExpectations() } func (s *OrgTestSuite) TestGetOrganization() { - org, err := s.Store.GetOrganization(context.Background(), s.Fixtures.Orgs[0].Name) + org, err := s.Store.GetOrganization(s.adminCtx, s.Fixtures.Orgs[0].Name) s.Require().Nil(err) s.Require().Equal(s.Fixtures.Orgs[0].Name, org.Name) @@ -229,14 +246,14 @@ func (s *OrgTestSuite) TestGetOrganization() { } func (s *OrgTestSuite) TestGetOrganizationCaseInsensitive() { - org, err := s.Store.GetOrganization(context.Background(), "TeSt-oRg-1") + org, err := s.Store.GetOrganization(s.adminCtx, "TeSt-oRg-1") s.Require().Nil(err) s.Require().Equal("test-org-1", org.Name) } func (s *OrgTestSuite) TestGetOrganizationNotFound() { - _, err := s.Store.GetOrganization(context.Background(), "dummy-name") + _, err := s.Store.GetOrganization(s.adminCtx, "dummy-name") s.Require().NotNil(err) s.Require().Equal("fetching org: not found", err.Error()) @@ -248,7 +265,7 @@ func (s *OrgTestSuite) TestGetOrganizationDBDecryptingErr() { WithArgs(s.Fixtures.Orgs[0].Name, 1). WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow(s.Fixtures.Orgs[0].Name)) - _, err := s.StoreSQLMocked.GetOrganization(context.Background(), s.Fixtures.Orgs[0].Name) + _, err := s.StoreSQLMocked.GetOrganization(s.adminCtx, s.Fixtures.Orgs[0].Name) s.assertSQLMockExpectations() s.Require().NotNil(err) @@ -256,7 +273,7 @@ func (s *OrgTestSuite) TestGetOrganizationDBDecryptingErr() { } func (s *OrgTestSuite) TestListOrganizations() { - orgs, err := s.Store.ListOrganizations(context.Background()) + orgs, err := s.Store.ListOrganizations(s.adminCtx) s.Require().Nil(err) garmTesting.EqualDBEntityByName(s.T(), s.Fixtures.Orgs, orgs) @@ -267,7 +284,7 @@ func (s *OrgTestSuite) TestListOrganizationsDBFetchErr() { ExpectQuery(regexp.QuoteMeta("SELECT * FROM `organizations` WHERE `organizations`.`deleted_at` IS NULL")). WillReturnError(fmt.Errorf("fetching user from database mock error")) - _, err := s.StoreSQLMocked.ListOrganizations(context.Background()) + _, err := s.StoreSQLMocked.ListOrganizations(s.adminCtx) s.assertSQLMockExpectations() s.Require().NotNil(err) @@ -275,16 +292,16 @@ func (s *OrgTestSuite) TestListOrganizationsDBFetchErr() { } func (s *OrgTestSuite) TestDeleteOrganization() { - err := s.Store.DeleteOrganization(context.Background(), s.Fixtures.Orgs[0].ID) + err := s.Store.DeleteOrganization(s.adminCtx, s.Fixtures.Orgs[0].ID) s.Require().Nil(err) - _, err = s.Store.GetOrganizationByID(context.Background(), s.Fixtures.Orgs[0].ID) + _, err = s.Store.GetOrganizationByID(s.adminCtx, s.Fixtures.Orgs[0].ID) s.Require().NotNil(err) s.Require().Equal("fetching org: not found", err.Error()) } func (s *OrgTestSuite) TestDeleteOrganizationInvalidOrgID() { - err := s.Store.DeleteOrganization(context.Background(), "dummy-org-id") + err := s.Store.DeleteOrganization(s.adminCtx, "dummy-org-id") s.Require().NotNil(err) s.Require().Equal("fetching org: parsing id: invalid request", err.Error()) @@ -302,7 +319,7 @@ func (s *OrgTestSuite) TestDeleteOrganizationDBDeleteErr() { WillReturnError(fmt.Errorf("mocked delete org error")) s.Fixtures.SQLMock.ExpectRollback() - err := s.StoreSQLMocked.DeleteOrganization(context.Background(), s.Fixtures.Orgs[0].ID) + err := s.StoreSQLMocked.DeleteOrganization(s.adminCtx, s.Fixtures.Orgs[0].ID) s.assertSQLMockExpectations() s.Require().NotNil(err) @@ -310,7 +327,7 @@ func (s *OrgTestSuite) TestDeleteOrganizationDBDeleteErr() { } func (s *OrgTestSuite) TestUpdateOrganization() { - org, err := s.Store.UpdateOrganization(context.Background(), s.Fixtures.Orgs[0].ID, s.Fixtures.UpdateRepoParams) + org, err := s.Store.UpdateOrganization(s.adminCtx, s.Fixtures.Orgs[0].ID, s.Fixtures.UpdateRepoParams) s.Require().Nil(err) s.Require().Equal(s.Fixtures.UpdateRepoParams.CredentialsName, org.Credentials.Name) @@ -318,70 +335,85 @@ func (s *OrgTestSuite) TestUpdateOrganization() { } func (s *OrgTestSuite) TestUpdateOrganizationInvalidOrgID() { - _, err := s.Store.UpdateOrganization(context.Background(), "dummy-org-id", s.Fixtures.UpdateRepoParams) + _, err := s.Store.UpdateOrganization(s.adminCtx, "dummy-org-id", s.Fixtures.UpdateRepoParams) s.Require().NotNil(err) - s.Require().Equal("fetching org: parsing id: invalid request", err.Error()) + s.Require().Equal("saving org: fetching org: parsing id: invalid request", err.Error()) } func (s *OrgTestSuite) TestUpdateOrganizationDBEncryptErr() { s.StoreSQLMocked.cfg.Passphrase = wrongPassphrase - + s.Fixtures.SQLMock.ExpectBegin() s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `organizations` WHERE id = ? AND `organizations`.`deleted_at` IS NULL ORDER BY `organizations`.`id` LIMIT ?")). WithArgs(s.Fixtures.Orgs[0].ID, 1). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Orgs[0].ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). + WithArgs(s.secondaryTestCreds.Name). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) + s.Fixtures.SQLMock.ExpectRollback() - _, err := s.StoreSQLMocked.UpdateOrganization(context.Background(), s.Fixtures.Orgs[0].ID, s.Fixtures.UpdateRepoParams) + _, err := s.StoreSQLMocked.UpdateOrganization(s.adminCtx, s.Fixtures.Orgs[0].ID, s.Fixtures.UpdateRepoParams) - s.assertSQLMockExpectations() s.Require().NotNil(err) - s.Require().Equal("saving org: failed to encrypt string: invalid passphrase length (expected length 32 characters)", err.Error()) + s.Require().Equal("saving org: saving org: failed to encrypt string: invalid passphrase length (expected length 32 characters)", err.Error()) + s.assertSQLMockExpectations() } func (s *OrgTestSuite) TestUpdateOrganizationDBSaveErr() { + s.Fixtures.SQLMock.ExpectBegin() s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `organizations` WHERE id = ? AND `organizations`.`deleted_at` IS NULL ORDER BY `organizations`.`id` LIMIT ?")). WithArgs(s.Fixtures.Orgs[0].ID, 1). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Orgs[0].ID)) - s.Fixtures.SQLMock.ExpectBegin() + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). + WithArgs(s.secondaryTestCreds.Name). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) s.Fixtures.SQLMock. ExpectExec(("UPDATE `organizations` SET")). WillReturnError(fmt.Errorf("saving org mock error")) s.Fixtures.SQLMock.ExpectRollback() - _, err := s.StoreSQLMocked.UpdateOrganization(context.Background(), s.Fixtures.Orgs[0].ID, s.Fixtures.UpdateRepoParams) + _, err := s.StoreSQLMocked.UpdateOrganization(s.adminCtx, s.Fixtures.Orgs[0].ID, s.Fixtures.UpdateRepoParams) - s.assertSQLMockExpectations() s.Require().NotNil(err) - s.Require().Equal("saving org: saving org mock error", err.Error()) + s.Require().Equal("saving org: saving org: saving org mock error", err.Error()) + s.assertSQLMockExpectations() } func (s *OrgTestSuite) TestUpdateOrganizationDBDecryptingErr() { s.StoreSQLMocked.cfg.Passphrase = wrongPassphrase s.Fixtures.UpdateRepoParams.WebhookSecret = webhookSecret + s.Fixtures.SQLMock.ExpectBegin() s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `organizations` WHERE id = ? AND `organizations`.`deleted_at` IS NULL ORDER BY `organizations`.`id` LIMIT ?")). WithArgs(s.Fixtures.Orgs[0].ID, 1). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Orgs[0].ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). + WithArgs(s.secondaryTestCreds.Name). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) + s.Fixtures.SQLMock.ExpectRollback() - _, err := s.StoreSQLMocked.UpdateOrganization(context.Background(), s.Fixtures.Orgs[0].ID, s.Fixtures.UpdateRepoParams) + _, err := s.StoreSQLMocked.UpdateOrganization(s.adminCtx, s.Fixtures.Orgs[0].ID, s.Fixtures.UpdateRepoParams) - s.assertSQLMockExpectations() s.Require().NotNil(err) - s.Require().Equal("saving org: failed to encrypt string: invalid passphrase length (expected length 32 characters)", err.Error()) + s.Require().Equal("saving org: saving org: failed to encrypt string: invalid passphrase length (expected length 32 characters)", err.Error()) + s.assertSQLMockExpectations() } func (s *OrgTestSuite) TestGetOrganizationByID() { - org, err := s.Store.GetOrganizationByID(context.Background(), s.Fixtures.Orgs[0].ID) + org, err := s.Store.GetOrganizationByID(s.adminCtx, s.Fixtures.Orgs[0].ID) s.Require().Nil(err) s.Require().Equal(s.Fixtures.Orgs[0].ID, org.ID) } func (s *OrgTestSuite) TestGetOrganizationByIDInvalidOrgID() { - _, err := s.Store.GetOrganizationByID(context.Background(), "dummy-org-id") + _, err := s.Store.GetOrganizationByID(s.adminCtx, "dummy-org-id") s.Require().NotNil(err) s.Require().Equal("fetching org: parsing id: invalid request", err.Error()) @@ -397,21 +429,21 @@ func (s *OrgTestSuite) TestGetOrganizationByIDDBDecryptingErr() { WithArgs(s.Fixtures.Orgs[0].ID). WillReturnRows(sqlmock.NewRows([]string{"org_id"}).AddRow(s.Fixtures.Orgs[0].ID)) - _, err := s.StoreSQLMocked.GetOrganizationByID(context.Background(), s.Fixtures.Orgs[0].ID) + _, err := s.StoreSQLMocked.GetOrganizationByID(s.adminCtx, s.Fixtures.Orgs[0].ID) s.assertSQLMockExpectations() s.Require().NotNil(err) - s.Require().Equal("fetching enterprise: missing secret", err.Error()) + s.Require().Equal("fetching org: missing secret", err.Error()) } func (s *OrgTestSuite) TestCreateOrganizationPool() { entity, err := s.Fixtures.Orgs[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().Nil(err) - org, err := s.Store.GetOrganizationByID(context.Background(), s.Fixtures.Orgs[0].ID) + org, err := s.Store.GetOrganizationByID(s.adminCtx, s.Fixtures.Orgs[0].ID) if err != nil { s.FailNow(fmt.Sprintf("cannot get org by ID: %v", err)) } @@ -426,7 +458,7 @@ func (s *OrgTestSuite) TestCreateOrganizationPoolMissingTags() { s.Fixtures.CreatePoolParams.Tags = []string{} entity, err := s.Fixtures.Orgs[0].GetEntity() s.Require().Nil(err) - _, err = s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("no tags specified", err.Error()) @@ -437,7 +469,7 @@ func (s *OrgTestSuite) TestCreateOrganizationPoolInvalidOrgID() { ID: "dummy-org-id", EntityType: params.GithubEntityTypeOrganization, } - _, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("parsing id: invalid request", err.Error()) @@ -455,7 +487,7 @@ func (s *OrgTestSuite) TestCreateOrganizationPoolDBCreateErr() { entity, err := s.Fixtures.Orgs[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("checking pool existence: mocked creating pool error", err.Error()) @@ -484,7 +516,7 @@ func (s *OrgTestSuite) TestCreateOrganizationDBPoolAlreadyExistErr() { entity, err := s.Fixtures.Orgs[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal(runnerErrors.NewConflictError("pool with the same image and flavor already exists on this provider"), err) @@ -511,7 +543,7 @@ func (s *OrgTestSuite) TestCreateOrganizationPoolDBFetchTagErr() { entity, err := s.Fixtures.Orgs[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("creating tag: fetching tag from database: mocked fetching tag error", err.Error()) @@ -547,7 +579,7 @@ func (s *OrgTestSuite) TestCreateOrganizationPoolDBAddingPoolErr() { entity, err := s.Fixtures.Orgs[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("creating pool: mocked adding pool error", err.Error()) @@ -586,7 +618,7 @@ func (s *OrgTestSuite) TestCreateOrganizationPoolDBSaveTagErr() { entity, err := s.Fixtures.Orgs[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("associating tags: mocked saving tag error", err.Error()) @@ -635,7 +667,7 @@ func (s *OrgTestSuite) TestCreateOrganizationPoolDBFetchPoolErr() { entity, err := s.Fixtures.Orgs[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("fetching pool: not found", err.Error()) @@ -648,13 +680,13 @@ func (s *OrgTestSuite) TestListOrgPools() { s.Require().Nil(err) for i := 1; i <= 2; i++ { s.Fixtures.CreatePoolParams.Flavor = fmt.Sprintf("test-flavor-%v", i) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create org pool: %v", err)) } orgPools = append(orgPools, pool) } - pools, err := s.Store.ListEntityPools(context.Background(), entity) + pools, err := s.Store.ListEntityPools(s.adminCtx, entity) s.Require().Nil(err) garmTesting.EqualDBEntityID(s.T(), orgPools, pools) @@ -665,7 +697,7 @@ func (s *OrgTestSuite) TestListOrgPoolsInvalidOrgID() { ID: "dummy-org-id", EntityType: params.GithubEntityTypeOrganization, } - _, err := s.Store.ListEntityPools(context.Background(), entity) + _, err := s.Store.ListEntityPools(s.adminCtx, entity) s.Require().NotNil(err) s.Require().Equal("fetching pools: parsing id: invalid request", err.Error()) @@ -674,12 +706,12 @@ func (s *OrgTestSuite) TestListOrgPoolsInvalidOrgID() { func (s *OrgTestSuite) TestGetOrganizationPool() { entity, err := s.Fixtures.Orgs[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create org pool: %v", err)) } - orgPool, err := s.Store.GetEntityPool(context.Background(), entity, pool.ID) + orgPool, err := s.Store.GetEntityPool(s.adminCtx, entity, pool.ID) s.Require().Nil(err) s.Require().Equal(orgPool.ID, pool.ID) @@ -690,7 +722,7 @@ func (s *OrgTestSuite) TestGetOrganizationPoolInvalidOrgID() { ID: "dummy-org-id", EntityType: params.GithubEntityTypeOrganization, } - _, err := s.Store.GetEntityPool(context.Background(), entity, "dummy-pool-id") + _, err := s.Store.GetEntityPool(s.adminCtx, entity, "dummy-pool-id") s.Require().NotNil(err) s.Require().Equal("fetching pool: parsing id: invalid request", err.Error()) @@ -699,15 +731,15 @@ func (s *OrgTestSuite) TestGetOrganizationPoolInvalidOrgID() { func (s *OrgTestSuite) TestDeleteOrganizationPool() { entity, err := s.Fixtures.Orgs[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create org pool: %v", err)) } - err = s.Store.DeleteEntityPool(context.Background(), entity, pool.ID) + err = s.Store.DeleteEntityPool(s.adminCtx, entity, pool.ID) s.Require().Nil(err) - _, err = s.Store.GetEntityPool(context.Background(), entity, pool.ID) + _, err = s.Store.GetEntityPool(s.adminCtx, entity, pool.ID) s.Require().Equal("fetching pool: finding pool: not found", err.Error()) } @@ -716,7 +748,7 @@ func (s *OrgTestSuite) TestDeleteOrganizationPoolInvalidOrgID() { ID: "dummy-org-id", EntityType: params.GithubEntityTypeOrganization, } - err := s.Store.DeleteEntityPool(context.Background(), entity, "dummy-pool-id") + err := s.Store.DeleteEntityPool(s.adminCtx, entity, "dummy-pool-id") s.Require().NotNil(err) s.Require().Equal("parsing id: invalid request", err.Error()) @@ -726,7 +758,7 @@ func (s *OrgTestSuite) TestDeleteOrganizationPoolDBDeleteErr() { entity, err := s.Fixtures.Orgs[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create org pool: %v", err)) } @@ -738,7 +770,7 @@ func (s *OrgTestSuite) TestDeleteOrganizationPoolDBDeleteErr() { WillReturnError(fmt.Errorf("mocked deleting pool error")) s.Fixtures.SQLMock.ExpectRollback() - err = s.StoreSQLMocked.DeleteEntityPool(context.Background(), entity, pool.ID) + err = s.StoreSQLMocked.DeleteEntityPool(s.adminCtx, entity, pool.ID) s.Require().NotNil(err) s.Require().Equal("removing pool: mocked deleting pool error", err.Error()) @@ -748,21 +780,21 @@ func (s *OrgTestSuite) TestDeleteOrganizationPoolDBDeleteErr() { func (s *OrgTestSuite) TestListOrgInstances() { entity, err := s.Fixtures.Orgs[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create org pool: %v", err)) } poolInstances := []params.Instance{} for i := 1; i <= 3; i++ { s.Fixtures.CreateInstanceParams.Name = fmt.Sprintf("test-org-%v", i) - instance, err := s.Store.CreateInstance(context.Background(), pool.ID, s.Fixtures.CreateInstanceParams) + instance, err := s.Store.CreateInstance(s.adminCtx, pool.ID, s.Fixtures.CreateInstanceParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create instance: %s", err)) } poolInstances = append(poolInstances, instance) } - instances, err := s.Store.ListEntityInstances(context.Background(), entity) + instances, err := s.Store.ListEntityInstances(s.adminCtx, entity) s.Require().Nil(err) s.equalInstancesByName(poolInstances, instances) @@ -773,7 +805,7 @@ func (s *OrgTestSuite) TestListOrgInstancesInvalidOrgID() { ID: "dummy-org-id", EntityType: params.GithubEntityTypeOrganization, } - _, err := s.Store.ListEntityInstances(context.Background(), entity) + _, err := s.Store.ListEntityInstances(s.adminCtx, entity) s.Require().NotNil(err) s.Require().Equal("fetching entity: parsing id: invalid request", err.Error()) @@ -782,12 +814,12 @@ func (s *OrgTestSuite) TestListOrgInstancesInvalidOrgID() { func (s *OrgTestSuite) TestUpdateOrganizationPool() { entity, err := s.Fixtures.Orgs[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create org pool: %v", err)) } - pool, err = s.Store.UpdateEntityPool(context.Background(), entity, pool.ID, s.Fixtures.UpdatePoolParams) + pool, err = s.Store.UpdateEntityPool(s.adminCtx, entity, pool.ID, s.Fixtures.UpdatePoolParams) s.Require().Nil(err) s.Require().Equal(*s.Fixtures.UpdatePoolParams.MaxRunners, pool.MaxRunners) @@ -801,7 +833,7 @@ func (s *OrgTestSuite) TestUpdateOrganizationPoolInvalidOrgID() { ID: "dummy-org-id", EntityType: params.GithubEntityTypeOrganization, } - _, err := s.Store.UpdateEntityPool(context.Background(), entity, "dummy-pool-id", s.Fixtures.UpdatePoolParams) + _, err := s.Store.UpdateEntityPool(s.adminCtx, entity, "dummy-pool-id", s.Fixtures.UpdatePoolParams) s.Require().NotNil(err) s.Require().Equal("fetching pool: parsing id: invalid request", err.Error()) diff --git a/database/sql/pools_test.go b/database/sql/pools_test.go index af4fa2cf..97dbdf71 100644 --- a/database/sql/pools_test.go +++ b/database/sql/pools_test.go @@ -43,6 +43,7 @@ type PoolsTestSuite struct { Store dbCommon.Store StoreSQLMocked *sqlDatabase Fixtures *PoolsTestFixtures + adminCtx context.Context } func (s *PoolsTestSuite) assertSQLMockExpectations() { @@ -60,8 +61,14 @@ func (s *PoolsTestSuite) SetupTest() { } s.Store = db + adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T()) + s.adminCtx = adminCtx + + githubEndpoint := garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) + creds := garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), githubEndpoint) + // create an organization for testing purposes - org, err := s.Store.CreateOrganization(context.Background(), "test-org", "test-creds", "test-webhookSecret", params.PoolBalancerTypeRoundRobin) + org, err := s.Store.CreateOrganization(s.adminCtx, "test-org", creds.Name, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) if err != nil { s.FailNow(fmt.Sprintf("failed to create org: %s", err)) } @@ -72,7 +79,7 @@ func (s *PoolsTestSuite) SetupTest() { orgPools := []params.Pool{} for i := 1; i <= 3; i++ { pool, err := db.CreateEntityPool( - context.Background(), + s.adminCtx, entity, params.CreatePoolParams{ ProviderName: "test-provider", @@ -122,7 +129,7 @@ func (s *PoolsTestSuite) SetupTest() { } func (s *PoolsTestSuite) TestListAllPools() { - pools, err := s.Store.ListAllPools(context.Background()) + pools, err := s.Store.ListAllPools(s.adminCtx) s.Require().Nil(err) garmTesting.EqualDBEntityID(s.T(), s.Fixtures.Pools, pools) @@ -133,7 +140,7 @@ func (s *PoolsTestSuite) TestListAllPoolsDBFetchErr() { ExpectQuery(regexp.QuoteMeta("SELECT `pools`.`id`,`pools`.`created_at`,`pools`.`updated_at`,`pools`.`deleted_at`,`pools`.`provider_name`,`pools`.`runner_prefix`,`pools`.`max_runners`,`pools`.`min_idle_runners`,`pools`.`runner_bootstrap_timeout`,`pools`.`image`,`pools`.`flavor`,`pools`.`os_type`,`pools`.`os_arch`,`pools`.`enabled`,`pools`.`git_hub_runner_group`,`pools`.`repo_id`,`pools`.`org_id`,`pools`.`enterprise_id`,`pools`.`priority` FROM `pools` WHERE `pools`.`deleted_at` IS NULL")). WillReturnError(fmt.Errorf("mocked fetching all pools error")) - _, err := s.StoreSQLMocked.ListAllPools(context.Background()) + _, err := s.StoreSQLMocked.ListAllPools(s.adminCtx) s.assertSQLMockExpectations() s.Require().NotNil(err) @@ -141,29 +148,29 @@ func (s *PoolsTestSuite) TestListAllPoolsDBFetchErr() { } func (s *PoolsTestSuite) TestGetPoolByID() { - pool, err := s.Store.GetPoolByID(context.Background(), s.Fixtures.Pools[0].ID) + pool, err := s.Store.GetPoolByID(s.adminCtx, s.Fixtures.Pools[0].ID) s.Require().Nil(err) s.Require().Equal(s.Fixtures.Pools[0].ID, pool.ID) } func (s *PoolsTestSuite) TestGetPoolByIDInvalidPoolID() { - _, err := s.Store.GetPoolByID(context.Background(), "dummy-pool-id") + _, err := s.Store.GetPoolByID(s.adminCtx, "dummy-pool-id") s.Require().NotNil(err) s.Require().Equal("fetching pool by ID: parsing id: invalid request", err.Error()) } func (s *PoolsTestSuite) TestDeletePoolByID() { - err := s.Store.DeletePoolByID(context.Background(), s.Fixtures.Pools[0].ID) + err := s.Store.DeletePoolByID(s.adminCtx, s.Fixtures.Pools[0].ID) s.Require().Nil(err) - _, err = s.Store.GetPoolByID(context.Background(), s.Fixtures.Pools[0].ID) + _, err = s.Store.GetPoolByID(s.adminCtx, s.Fixtures.Pools[0].ID) s.Require().Equal("fetching pool by ID: not found", err.Error()) } func (s *PoolsTestSuite) TestDeletePoolByIDInvalidPoolID() { - err := s.Store.DeletePoolByID(context.Background(), "dummy-pool-id") + err := s.Store.DeletePoolByID(s.adminCtx, "dummy-pool-id") s.Require().NotNil(err) s.Require().Equal("fetching pool by ID: parsing id: invalid request", err.Error()) @@ -180,7 +187,7 @@ func (s *PoolsTestSuite) TestDeletePoolByIDDBRemoveErr() { WillReturnError(fmt.Errorf("mocked removing pool error")) s.Fixtures.SQLMock.ExpectRollback() - err := s.StoreSQLMocked.DeletePoolByID(context.Background(), s.Fixtures.Pools[0].ID) + err := s.StoreSQLMocked.DeletePoolByID(s.adminCtx, s.Fixtures.Pools[0].ID) s.assertSQLMockExpectations() s.Require().NotNil(err) diff --git a/database/sql/repositories.go b/database/sql/repositories.go index 396b2796..18284e1b 100644 --- a/database/sql/repositories.go +++ b/database/sql/repositories.go @@ -36,31 +36,32 @@ func (s *sqlDatabase) CreateRepository(ctx context.Context, owner, name, credent return params.Repository{}, fmt.Errorf("failed to encrypt string") } - var newRepo Repository + newRepo := Repository{ + Name: name, + Owner: owner, + WebhookSecret: secret, + PoolBalancerType: poolBalancerType, + } err = s.conn.Transaction(func(tx *gorm.DB) error { creds, err := s.getGithubCredentialsByName(ctx, tx, credentialsName, false) if err != nil { return errors.Wrap(err, "creating repository") } - - newRepo.Name = name - newRepo.Owner = owner - newRepo.WebhookSecret = secret newRepo.CredentialsID = &creds.ID - newRepo.PoolBalancerType = poolBalancerType q := tx.Create(&newRepo) if q.Error != nil { return errors.Wrap(q.Error, "creating repository") } + newRepo.Credentials = creds return nil }) if err != nil { return params.Repository{}, errors.Wrap(err, "creating repository") } - param, err := s.sqlToCommonRepository(newRepo) + param, err := s.sqlToCommonRepository(newRepo, true) if err != nil { return params.Repository{}, errors.Wrap(err, "creating repository") } @@ -74,7 +75,7 @@ func (s *sqlDatabase) GetRepository(ctx context.Context, owner, name string) (pa return params.Repository{}, errors.Wrap(err, "fetching repo") } - param, err := s.sqlToCommonRepository(repo) + param, err := s.sqlToCommonRepository(repo, true) if err != nil { return params.Repository{}, errors.Wrap(err, "fetching repo") } @@ -92,7 +93,7 @@ func (s *sqlDatabase) ListRepositories(_ context.Context) ([]params.Repository, ret := make([]params.Repository, len(repos)) for idx, val := range repos { var err error - ret[idx], err = s.sqlToCommonRepository(val) + ret[idx], err = s.sqlToCommonRepository(val, true) if err != nil { return nil, errors.Wrap(err, "fetching repositories") } @@ -102,7 +103,7 @@ func (s *sqlDatabase) ListRepositories(_ context.Context) ([]params.Repository, } func (s *sqlDatabase) DeleteRepository(ctx context.Context, repoID string) error { - repo, err := s.getRepoByID(ctx, repoID) + repo, err := s.getRepoByID(ctx, s.conn, repoID) if err != nil { return errors.Wrap(err, "fetching repo") } @@ -116,33 +117,51 @@ func (s *sqlDatabase) DeleteRepository(ctx context.Context, repoID string) error } func (s *sqlDatabase) UpdateRepository(ctx context.Context, repoID string, param params.UpdateEntityParams) (params.Repository, error) { - repo, err := s.getRepoByID(ctx, repoID, "Credentials", "Endpoint") - if err != nil { - return params.Repository{}, errors.Wrap(err, "fetching repo") - } + var repo Repository + var creds GithubCredentials + err := s.conn.Transaction(func(tx *gorm.DB) error { + var err error + repo, err = s.getRepoByID(ctx, tx, repoID, "Credentials", "Endpoint") + if err != nil { + return errors.Wrap(err, "fetching repo") + } - if param.CredentialsName != "" { - repo.CredentialsName = param.CredentialsName - } + if param.CredentialsName != "" { + repo.CredentialsName = param.CredentialsName + creds, err = s.getGithubCredentialsByName(ctx, tx, param.CredentialsName, false) + if err != nil { + return errors.Wrap(err, "fetching credentials") + } + repo.CredentialsID = &creds.ID + } - if param.WebhookSecret != "" { - secret, err := util.Seal([]byte(param.WebhookSecret), []byte(s.cfg.Passphrase)) - if err != nil { - return params.Repository{}, fmt.Errorf("saving repo: failed to encrypt string: %w", err) + if param.WebhookSecret != "" { + secret, err := util.Seal([]byte(param.WebhookSecret), []byte(s.cfg.Passphrase)) + if err != nil { + return fmt.Errorf("saving repo: failed to encrypt string: %w", err) + } + repo.WebhookSecret = secret } - repo.WebhookSecret = secret - } - if param.PoolBalancerType != "" { - repo.PoolBalancerType = param.PoolBalancerType - } + if param.PoolBalancerType != "" { + repo.PoolBalancerType = param.PoolBalancerType + } - q := s.conn.Save(&repo) - if q.Error != nil { - return params.Repository{}, errors.Wrap(q.Error, "saving repo") + q := tx.Save(&repo) + if q.Error != nil { + return errors.Wrap(q.Error, "saving repo") + } + + if creds.ID != 0 { + repo.Credentials = creds + } + return nil + }) + if err != nil { + return params.Repository{}, errors.Wrap(err, "saving repo") } - newParams, err := s.sqlToCommonRepository(repo) + newParams, err := s.sqlToCommonRepository(repo, true) if err != nil { return params.Repository{}, errors.Wrap(err, "saving repo") } @@ -150,12 +169,12 @@ func (s *sqlDatabase) UpdateRepository(ctx context.Context, repoID string, param } func (s *sqlDatabase) GetRepositoryByID(ctx context.Context, repoID string) (params.Repository, error) { - repo, err := s.getRepoByID(ctx, repoID, "Pools", "Credentials", "Endpoint") + repo, err := s.getRepoByID(ctx, s.conn, repoID, "Pools", "Credentials", "Endpoint") if err != nil { return params.Repository{}, errors.Wrap(err, "fetching repo") } - param, err := s.sqlToCommonRepository(repo) + param, err := s.sqlToCommonRepository(repo, true) if err != nil { return params.Repository{}, errors.Wrap(err, "fetching repo") } @@ -206,14 +225,14 @@ func (s *sqlDatabase) getEntityPoolByUniqueFields(tx *gorm.DB, entity params.Git return Pool{}, nil } -func (s *sqlDatabase) getRepoByID(_ context.Context, id string, preload ...string) (Repository, error) { +func (s *sqlDatabase) getRepoByID(_ context.Context, tx *gorm.DB, id string, preload ...string) (Repository, error) { u, err := uuid.Parse(id) if err != nil { return Repository{}, errors.Wrap(runnerErrors.ErrBadRequest, "parsing id") } var repo Repository - q := s.conn + q := tx if len(preload) > 0 { for _, field := range preload { q = q.Preload(field) diff --git a/database/sql/repositories_test.go b/database/sql/repositories_test.go index fbd68304..09f65a7a 100644 --- a/database/sql/repositories_test.go +++ b/database/sql/repositories_test.go @@ -48,6 +48,11 @@ type RepoTestSuite struct { Store dbCommon.Store StoreSQLMocked *sqlDatabase Fixtures *RepoTestFixtures + + adminCtx context.Context + testCreds params.GithubCredentials + secondaryTestCreds params.GithubCredentials + githubEndpoint params.GithubEndpoint } func (s *RepoTestSuite) equalReposByName(expected, actual []params.Repository) { @@ -87,14 +92,21 @@ func (s *RepoTestSuite) SetupTest() { } s.Store = db + adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T()) + s.adminCtx = adminCtx + + s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) + s.testCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), s.githubEndpoint) + s.secondaryTestCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "secondary-creds", db, s.T(), s.githubEndpoint) + // create some repository objects in the database, for testing purposes repos := []params.Repository{} for i := 1; i <= 3; i++ { repo, err := db.CreateRepository( - context.Background(), + adminCtx, fmt.Sprintf("test-owner-%d", i), fmt.Sprintf("test-repo-%d", i), - fmt.Sprintf("test-creds-%d", i), + s.testCreds.Name, fmt.Sprintf("test-webhook-secret-%d", i), params.PoolBalancerTypeRoundRobin, ) @@ -136,7 +148,7 @@ func (s *RepoTestSuite) SetupTest() { CreateRepoParams: params.CreateRepoParams{ Owner: "test-owner-repo", Name: "test-repo", - CredentialsName: "test-creds-repo", + CredentialsName: s.testCreds.Name, WebhookSecret: "test-webhook-secret", }, CreatePoolParams: params.CreatePoolParams{ @@ -155,7 +167,7 @@ func (s *RepoTestSuite) SetupTest() { OSType: "linux", }, UpdateRepoParams: params.UpdateEntityParams{ - CredentialsName: "test-update-creds", + CredentialsName: s.secondaryTestCreds.Name, WebhookSecret: "test-update-webhook-secret", }, UpdatePoolParams: params.UpdatePoolParams{ @@ -172,7 +184,7 @@ func (s *RepoTestSuite) SetupTest() { func (s *RepoTestSuite) TestCreateRepository() { // call tested function repo, err := s.Store.CreateRepository( - context.Background(), + s.adminCtx, s.Fixtures.CreateRepoParams.Owner, s.Fixtures.CreateRepoParams.Name, s.Fixtures.CreateRepoParams.CredentialsName, @@ -182,7 +194,7 @@ func (s *RepoTestSuite) TestCreateRepository() { // assertions s.Require().Nil(err) - storeRepo, err := s.Store.GetRepositoryByID(context.Background(), repo.ID) + storeRepo, err := s.Store.GetRepositoryByID(s.adminCtx, repo.ID) if err != nil { s.FailNow(fmt.Sprintf("failed to get repository by id: %v", err)) } @@ -206,7 +218,7 @@ func (s *RepoTestSuite) TestCreateRepositoryInvalidDBPassphrase() { } _, err = sqlDB.CreateRepository( - context.Background(), + s.adminCtx, s.Fixtures.CreateRepoParams.Owner, s.Fixtures.CreateRepoParams.Name, s.Fixtures.CreateRepoParams.CredentialsName, @@ -220,13 +232,17 @@ func (s *RepoTestSuite) TestCreateRepositoryInvalidDBPassphrase() { func (s *RepoTestSuite) TestCreateRepositoryInvalidDBCreateErr() { s.Fixtures.SQLMock.ExpectBegin() + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). + WithArgs(s.Fixtures.Repos[0].CredentialsName). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.testCreds.ID)) s.Fixtures.SQLMock. ExpectExec(regexp.QuoteMeta("INSERT INTO `repositories`")). WillReturnError(fmt.Errorf("creating repo mock error")) s.Fixtures.SQLMock.ExpectRollback() _, err := s.StoreSQLMocked.CreateRepository( - context.Background(), + s.adminCtx, s.Fixtures.CreateRepoParams.Owner, s.Fixtures.CreateRepoParams.Name, s.Fixtures.CreateRepoParams.CredentialsName, @@ -234,13 +250,13 @@ func (s *RepoTestSuite) TestCreateRepositoryInvalidDBCreateErr() { params.PoolBalancerTypeRoundRobin, ) - s.assertSQLMockExpectations() s.Require().NotNil(err) - s.Require().Equal("creating repository: creating repo mock error", err.Error()) + s.Require().Equal("creating repository: creating repository: creating repo mock error", err.Error()) + s.assertSQLMockExpectations() } func (s *RepoTestSuite) TestGetRepository() { - repo, err := s.Store.GetRepository(context.Background(), s.Fixtures.Repos[0].Owner, s.Fixtures.Repos[0].Name) + repo, err := s.Store.GetRepository(s.adminCtx, s.Fixtures.Repos[0].Owner, s.Fixtures.Repos[0].Name) s.Require().Nil(err) s.Require().Equal(s.Fixtures.Repos[0].Owner, repo.Owner) @@ -249,7 +265,7 @@ func (s *RepoTestSuite) TestGetRepository() { } func (s *RepoTestSuite) TestGetRepositoryCaseInsensitive() { - repo, err := s.Store.GetRepository(context.Background(), "TeSt-oWnEr-1", "TeSt-rEpO-1") + repo, err := s.Store.GetRepository(s.adminCtx, "TeSt-oWnEr-1", "TeSt-rEpO-1") s.Require().Nil(err) s.Require().Equal("test-owner-1", repo.Owner) @@ -257,7 +273,7 @@ func (s *RepoTestSuite) TestGetRepositoryCaseInsensitive() { } func (s *RepoTestSuite) TestGetRepositoryNotFound() { - _, err := s.Store.GetRepository(context.Background(), "dummy-owner", "dummy-name") + _, err := s.Store.GetRepository(s.adminCtx, "dummy-owner", "dummy-name") s.Require().NotNil(err) s.Require().Equal("fetching repo: not found", err.Error()) @@ -273,15 +289,15 @@ func (s *RepoTestSuite) TestGetRepositoryDBDecryptingErr() { WithArgs(s.Fixtures.Repos[0].Name, s.Fixtures.Repos[0].Owner, 1). WillReturnRows(sqlmock.NewRows([]string{"name", "owner"}).AddRow(s.Fixtures.Repos[0].Name, s.Fixtures.Repos[0].Owner)) - _, err := s.StoreSQLMocked.GetRepository(context.Background(), s.Fixtures.Repos[0].Owner, s.Fixtures.Repos[0].Name) + _, err := s.StoreSQLMocked.GetRepository(s.adminCtx, s.Fixtures.Repos[0].Owner, s.Fixtures.Repos[0].Name) - s.assertSQLMockExpectations() s.Require().NotNil(err) s.Require().Equal("fetching repo: missing secret", err.Error()) + s.assertSQLMockExpectations() } func (s *RepoTestSuite) TestListRepositories() { - repos, err := s.Store.ListRepositories((context.Background())) + repos, err := s.Store.ListRepositories(s.adminCtx) s.Require().Nil(err) s.equalReposByName(s.Fixtures.Repos, repos) @@ -292,11 +308,11 @@ func (s *RepoTestSuite) TestListRepositoriesDBFetchErr() { ExpectQuery(regexp.QuoteMeta("SELECT * FROM `repositories` WHERE `repositories`.`deleted_at` IS NULL")). WillReturnError(fmt.Errorf("fetching user from database mock error")) - _, err := s.StoreSQLMocked.ListRepositories(context.Background()) + _, err := s.StoreSQLMocked.ListRepositories(s.adminCtx) - s.assertSQLMockExpectations() s.Require().NotNil(err) s.Require().Equal("fetching user from database: fetching user from database mock error", err.Error()) + s.assertSQLMockExpectations() } func (s *RepoTestSuite) TestListRepositoriesDBDecryptingErr() { @@ -306,24 +322,24 @@ func (s *RepoTestSuite) TestListRepositoriesDBDecryptingErr() { ExpectQuery(regexp.QuoteMeta("SELECT * FROM `repositories` WHERE `repositories`.`deleted_at` IS NULL")). WillReturnRows(sqlmock.NewRows([]string{"id", "webhook_secret"}).AddRow(s.Fixtures.Repos[0].ID, s.Fixtures.Repos[0].WebhookSecret)) - _, err := s.StoreSQLMocked.ListRepositories(context.Background()) + _, err := s.StoreSQLMocked.ListRepositories(s.adminCtx) - s.assertSQLMockExpectations() s.Require().NotNil(err) s.Require().Equal("fetching repositories: decrypting secret: invalid passphrase length (expected length 32 characters)", err.Error()) + s.assertSQLMockExpectations() } func (s *RepoTestSuite) TestDeleteRepository() { - err := s.Store.DeleteRepository(context.Background(), s.Fixtures.Repos[0].ID) + err := s.Store.DeleteRepository(s.adminCtx, s.Fixtures.Repos[0].ID) s.Require().Nil(err) - _, err = s.Store.GetRepositoryByID(context.Background(), s.Fixtures.Repos[0].ID) + _, err = s.Store.GetRepositoryByID(s.adminCtx, s.Fixtures.Repos[0].ID) s.Require().NotNil(err) s.Require().Equal("fetching repo: not found", err.Error()) } func (s *RepoTestSuite) TestDeleteRepositoryInvalidRepoID() { - err := s.Store.DeleteRepository(context.Background(), "dummy-repo-id") + err := s.Store.DeleteRepository(s.adminCtx, "dummy-repo-id") s.Require().NotNil(err) s.Require().Equal("fetching repo: parsing id: invalid request", err.Error()) @@ -341,15 +357,15 @@ func (s *RepoTestSuite) TestDeleteRepositoryDBRemoveErr() { WillReturnError(fmt.Errorf("mocked deleting repo error")) s.Fixtures.SQLMock.ExpectRollback() - err := s.StoreSQLMocked.DeleteRepository(context.Background(), s.Fixtures.Repos[0].ID) + err := s.StoreSQLMocked.DeleteRepository(s.adminCtx, s.Fixtures.Repos[0].ID) - s.assertSQLMockExpectations() s.Require().NotNil(err) s.Require().Equal("deleting repo: mocked deleting repo error", err.Error()) + s.assertSQLMockExpectations() } func (s *RepoTestSuite) TestUpdateRepository() { - repo, err := s.Store.UpdateRepository(context.Background(), s.Fixtures.Repos[0].ID, s.Fixtures.UpdateRepoParams) + repo, err := s.Store.UpdateRepository(s.adminCtx, s.Fixtures.Repos[0].ID, s.Fixtures.UpdateRepoParams) s.Require().Nil(err) s.Require().Equal(s.Fixtures.UpdateRepoParams.CredentialsName, repo.Credentials.Name) @@ -357,69 +373,84 @@ func (s *RepoTestSuite) TestUpdateRepository() { } func (s *RepoTestSuite) TestUpdateRepositoryInvalidRepoID() { - _, err := s.Store.UpdateRepository(context.Background(), "dummy-repo-id", s.Fixtures.UpdateRepoParams) + _, err := s.Store.UpdateRepository(s.adminCtx, "dummy-repo-id", s.Fixtures.UpdateRepoParams) s.Require().NotNil(err) - s.Require().Equal("fetching repo: parsing id: invalid request", err.Error()) + s.Require().Equal("saving repo: fetching repo: parsing id: invalid request", err.Error()) } func (s *RepoTestSuite) TestUpdateRepositoryDBEncryptErr() { s.StoreSQLMocked.cfg.Passphrase = wrongPassphrase - + s.Fixtures.SQLMock.ExpectBegin() s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `repositories` WHERE id = ? AND `repositories`.`deleted_at` IS NULL ORDER BY `repositories`.`id` LIMIT ?")). WithArgs(s.Fixtures.Repos[0].ID, 1). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Repos[0].ID)) - _, err := s.StoreSQLMocked.UpdateRepository(context.Background(), s.Fixtures.Repos[0].ID, s.Fixtures.UpdateRepoParams) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). + WithArgs(s.secondaryTestCreds.Name). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) + s.Fixtures.SQLMock.ExpectRollback() + + _, err := s.StoreSQLMocked.UpdateRepository(s.adminCtx, s.Fixtures.Repos[0].ID, s.Fixtures.UpdateRepoParams) - s.assertSQLMockExpectations() s.Require().NotNil(err) - s.Require().Equal("saving repo: failed to encrypt string: invalid passphrase length (expected length 32 characters)", err.Error()) + s.Require().Equal("saving repo: saving repo: failed to encrypt string: invalid passphrase length (expected length 32 characters)", err.Error()) + s.assertSQLMockExpectations() } func (s *RepoTestSuite) TestUpdateRepositoryDBSaveErr() { + s.Fixtures.SQLMock.ExpectBegin() s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `repositories` WHERE id = ? AND `repositories`.`deleted_at` IS NULL ORDER BY `repositories`.`id` LIMIT ?")). WithArgs(s.Fixtures.Repos[0].ID, 1). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Repos[0].ID)) - s.Fixtures.SQLMock.ExpectBegin() + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). + WithArgs(s.secondaryTestCreds.Name). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) s.Fixtures.SQLMock. ExpectExec(("UPDATE `repositories` SET")). WillReturnError(fmt.Errorf("saving repo mock error")) s.Fixtures.SQLMock.ExpectRollback() - _, err := s.StoreSQLMocked.UpdateRepository(context.Background(), s.Fixtures.Repos[0].ID, s.Fixtures.UpdateRepoParams) + _, err := s.StoreSQLMocked.UpdateRepository(s.adminCtx, s.Fixtures.Repos[0].ID, s.Fixtures.UpdateRepoParams) - s.assertSQLMockExpectations() s.Require().NotNil(err) - s.Require().Equal("saving repo: saving repo mock error", err.Error()) + s.Require().Equal("saving repo: saving repo: saving repo mock error", err.Error()) + s.assertSQLMockExpectations() } func (s *RepoTestSuite) TestUpdateRepositoryDBDecryptingErr() { s.StoreSQLMocked.cfg.Passphrase = wrongPassphrase s.Fixtures.UpdateRepoParams.WebhookSecret = webhookSecret - + s.Fixtures.SQLMock.ExpectBegin() s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `repositories` WHERE id = ? AND `repositories`.`deleted_at` IS NULL ORDER BY `repositories`.`id` LIMIT ?")). WithArgs(s.Fixtures.Repos[0].ID, 1). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Repos[0].ID)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). + WithArgs(s.secondaryTestCreds.Name). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) + s.Fixtures.SQLMock.ExpectRollback() - _, err := s.StoreSQLMocked.UpdateRepository(context.Background(), s.Fixtures.Repos[0].ID, s.Fixtures.UpdateRepoParams) + _, err := s.StoreSQLMocked.UpdateRepository(s.adminCtx, s.Fixtures.Repos[0].ID, s.Fixtures.UpdateRepoParams) - s.assertSQLMockExpectations() s.Require().NotNil(err) - s.Require().Equal("saving repo: failed to encrypt string: invalid passphrase length (expected length 32 characters)", err.Error()) + s.Require().Equal("saving repo: saving repo: failed to encrypt string: invalid passphrase length (expected length 32 characters)", err.Error()) + s.assertSQLMockExpectations() } func (s *RepoTestSuite) TestGetRepositoryByID() { - repo, err := s.Store.GetRepositoryByID(context.Background(), s.Fixtures.Repos[0].ID) + repo, err := s.Store.GetRepositoryByID(s.adminCtx, s.Fixtures.Repos[0].ID) s.Require().Nil(err) s.Require().Equal(s.Fixtures.Repos[0].ID, repo.ID) } func (s *RepoTestSuite) TestGetRepositoryByIDInvalidRepoID() { - _, err := s.Store.GetRepositoryByID(context.Background(), "dummy-repo-id") + _, err := s.Store.GetRepositoryByID(s.adminCtx, "dummy-repo-id") s.Require().NotNil(err) s.Require().Equal("fetching repo: parsing id: invalid request", err.Error()) @@ -435,20 +466,20 @@ func (s *RepoTestSuite) TestGetRepositoryByIDDBDecryptingErr() { WithArgs(s.Fixtures.Repos[0].ID). WillReturnRows(sqlmock.NewRows([]string{"repo_id"}).AddRow(s.Fixtures.Repos[0].ID)) - _, err := s.StoreSQLMocked.GetRepositoryByID(context.Background(), s.Fixtures.Repos[0].ID) + _, err := s.StoreSQLMocked.GetRepositoryByID(s.adminCtx, s.Fixtures.Repos[0].ID) - s.assertSQLMockExpectations() s.Require().NotNil(err) s.Require().Equal("fetching repo: missing secret", err.Error()) + s.assertSQLMockExpectations() } func (s *RepoTestSuite) TestCreateRepositoryPool() { entity, err := s.Fixtures.Repos[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().Nil(err) - repo, err := s.Store.GetRepositoryByID(context.Background(), s.Fixtures.Repos[0].ID) + repo, err := s.Store.GetRepositoryByID(s.adminCtx, s.Fixtures.Repos[0].ID) if err != nil { s.FailNow(fmt.Sprintf("cannot get repo by ID: %v", err)) } @@ -463,7 +494,7 @@ func (s *RepoTestSuite) TestCreateRepositoryPoolMissingTags() { s.Fixtures.CreatePoolParams.Tags = []string{} entity, err := s.Fixtures.Repos[0].GetEntity() s.Require().Nil(err) - _, err = s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("no tags specified", err.Error()) @@ -474,7 +505,7 @@ func (s *RepoTestSuite) TestCreateRepositoryPoolInvalidRepoID() { ID: "dummy-repo-id", EntityType: params.GithubEntityTypeRepository, } - _, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("parsing id: invalid request", err.Error()) @@ -492,7 +523,7 @@ func (s *RepoTestSuite) TestCreateRepositoryPoolDBCreateErr() { entity, err := s.Fixtures.Repos[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("checking pool existence: mocked creating pool error", err.Error()) @@ -522,7 +553,7 @@ func (s *RepoTestSuite) TestCreateRepositoryPoolDBPoolAlreadyExistErr() { entity, err := s.Fixtures.Repos[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("pool with the same image and flavor already exists on this provider", err.Error()) @@ -550,7 +581,7 @@ func (s *RepoTestSuite) TestCreateRepositoryPoolDBFetchTagErr() { entity, err := s.Fixtures.Repos[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("creating tag: fetching tag from database: mocked fetching tag error", err.Error()) @@ -587,7 +618,7 @@ func (s *RepoTestSuite) TestCreateRepositoryPoolDBAddingPoolErr() { entity, err := s.Fixtures.Repos[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("creating pool: mocked adding pool error", err.Error()) @@ -627,7 +658,7 @@ func (s *RepoTestSuite) TestCreateRepositoryPoolDBSaveTagErr() { entity, err := s.Fixtures.Repos[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("associating tags: mocked saving tag error", err.Error()) s.assertSQLMockExpectations() @@ -675,7 +706,7 @@ func (s *RepoTestSuite) TestCreateRepositoryPoolDBFetchPoolErr() { entity, err := s.Fixtures.Repos[0].GetEntity() s.Require().Nil(err) - _, err = s.StoreSQLMocked.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + _, err = s.StoreSQLMocked.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) s.Require().NotNil(err) s.Require().Equal("fetching pool: not found", err.Error()) @@ -688,14 +719,14 @@ func (s *RepoTestSuite) TestListRepoPools() { repoPools := []params.Pool{} for i := 1; i <= 2; i++ { s.Fixtures.CreatePoolParams.Flavor = fmt.Sprintf("test-flavor-%d", i) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create repo pool: %v", err)) } repoPools = append(repoPools, pool) } - pools, err := s.Store.ListEntityPools(context.Background(), entity) + pools, err := s.Store.ListEntityPools(s.adminCtx, entity) s.Require().Nil(err) garmTesting.EqualDBEntityID(s.T(), repoPools, pools) @@ -706,7 +737,7 @@ func (s *RepoTestSuite) TestListRepoPoolsInvalidRepoID() { ID: "dummy-repo-id", EntityType: params.GithubEntityTypeRepository, } - _, err := s.Store.ListEntityPools(context.Background(), entity) + _, err := s.Store.ListEntityPools(s.adminCtx, entity) s.Require().NotNil(err) s.Require().Equal("fetching pools: parsing id: invalid request", err.Error()) @@ -715,12 +746,12 @@ func (s *RepoTestSuite) TestListRepoPoolsInvalidRepoID() { func (s *RepoTestSuite) TestGetRepositoryPool() { entity, err := s.Fixtures.Repos[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create repo pool: %v", err)) } - repoPool, err := s.Store.GetEntityPool(context.Background(), entity, pool.ID) + repoPool, err := s.Store.GetEntityPool(s.adminCtx, entity, pool.ID) s.Require().Nil(err) s.Require().Equal(repoPool.ID, pool.ID) @@ -731,7 +762,7 @@ func (s *RepoTestSuite) TestGetRepositoryPoolInvalidRepoID() { ID: "dummy-repo-id", EntityType: params.GithubEntityTypeRepository, } - _, err := s.Store.GetEntityPool(context.Background(), entity, "dummy-pool-id") + _, err := s.Store.GetEntityPool(s.adminCtx, entity, "dummy-pool-id") s.Require().NotNil(err) s.Require().Equal("fetching pool: parsing id: invalid request", err.Error()) @@ -740,15 +771,15 @@ func (s *RepoTestSuite) TestGetRepositoryPoolInvalidRepoID() { func (s *RepoTestSuite) TestDeleteRepositoryPool() { entity, err := s.Fixtures.Repos[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create repo pool: %v", err)) } - err = s.Store.DeleteEntityPool(context.Background(), entity, pool.ID) + err = s.Store.DeleteEntityPool(s.adminCtx, entity, pool.ID) s.Require().Nil(err) - _, err = s.Store.GetEntityPool(context.Background(), entity, pool.ID) + _, err = s.Store.GetEntityPool(s.adminCtx, entity, pool.ID) s.Require().Equal("fetching pool: finding pool: not found", err.Error()) } @@ -757,7 +788,7 @@ func (s *RepoTestSuite) TestDeleteRepositoryPoolInvalidRepoID() { ID: "dummy-repo-id", EntityType: params.GithubEntityTypeRepository, } - err := s.Store.DeleteEntityPool(context.Background(), entity, "dummy-pool-id") + err := s.Store.DeleteEntityPool(s.adminCtx, entity, "dummy-pool-id") s.Require().NotNil(err) s.Require().Equal("parsing id: invalid request", err.Error()) @@ -767,7 +798,7 @@ func (s *RepoTestSuite) TestDeleteRepositoryPoolDBDeleteErr() { entity, err := s.Fixtures.Repos[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create repo pool: %v", err)) } @@ -779,7 +810,7 @@ func (s *RepoTestSuite) TestDeleteRepositoryPoolDBDeleteErr() { WillReturnError(fmt.Errorf("mocked deleting pool error")) s.Fixtures.SQLMock.ExpectRollback() - err = s.StoreSQLMocked.DeleteEntityPool(context.Background(), entity, pool.ID) + err = s.StoreSQLMocked.DeleteEntityPool(s.adminCtx, entity, pool.ID) s.Require().NotNil(err) s.Require().Equal("removing pool: mocked deleting pool error", err.Error()) s.assertSQLMockExpectations() @@ -788,21 +819,21 @@ func (s *RepoTestSuite) TestDeleteRepositoryPoolDBDeleteErr() { func (s *RepoTestSuite) TestListRepoInstances() { entity, err := s.Fixtures.Repos[0].GetEntity() s.Require().Nil(err) - pool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + pool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create repo pool: %v", err)) } poolInstances := []params.Instance{} for i := 1; i <= 3; i++ { s.Fixtures.CreateInstanceParams.Name = fmt.Sprintf("test-repo-%d", i) - instance, err := s.Store.CreateInstance(context.Background(), pool.ID, s.Fixtures.CreateInstanceParams) + instance, err := s.Store.CreateInstance(s.adminCtx, pool.ID, s.Fixtures.CreateInstanceParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create instance: %s", err)) } poolInstances = append(poolInstances, instance) } - instances, err := s.Store.ListEntityInstances(context.Background(), entity) + instances, err := s.Store.ListEntityInstances(s.adminCtx, entity) s.Require().Nil(err) s.equalInstancesByID(poolInstances, instances) @@ -813,7 +844,7 @@ func (s *RepoTestSuite) TestListRepoInstancesInvalidRepoID() { ID: "dummy-repo-id", EntityType: params.GithubEntityTypeRepository, } - _, err := s.Store.ListEntityInstances(context.Background(), entity) + _, err := s.Store.ListEntityInstances(s.adminCtx, entity) s.Require().NotNil(err) s.Require().Equal("fetching entity: parsing id: invalid request", err.Error()) @@ -822,12 +853,12 @@ func (s *RepoTestSuite) TestListRepoInstancesInvalidRepoID() { func (s *RepoTestSuite) TestUpdateRepositoryPool() { entity, err := s.Fixtures.Repos[0].GetEntity() s.Require().Nil(err) - repoPool, err := s.Store.CreateEntityPool(context.Background(), entity, s.Fixtures.CreatePoolParams) + repoPool, err := s.Store.CreateEntityPool(s.adminCtx, entity, s.Fixtures.CreatePoolParams) if err != nil { s.FailNow(fmt.Sprintf("cannot create repo pool: %v", err)) } - pool, err := s.Store.UpdateEntityPool(context.Background(), entity, repoPool.ID, s.Fixtures.UpdatePoolParams) + pool, err := s.Store.UpdateEntityPool(s.adminCtx, entity, repoPool.ID, s.Fixtures.UpdatePoolParams) s.Require().Nil(err) s.Require().Equal(*s.Fixtures.UpdatePoolParams.MaxRunners, pool.MaxRunners) @@ -841,7 +872,7 @@ func (s *RepoTestSuite) TestUpdateRepositoryPoolInvalidRepoID() { ID: "dummy-repo-id", EntityType: params.GithubEntityTypeRepository, } - _, err := s.Store.UpdateEntityPool(context.Background(), entity, "dummy-repo-id", s.Fixtures.UpdatePoolParams) + _, err := s.Store.UpdateEntityPool(s.adminCtx, entity, "dummy-repo-id", s.Fixtures.UpdatePoolParams) s.Require().NotNil(err) s.Require().Equal("fetching pool: parsing id: invalid request", err.Error()) diff --git a/database/sql/util.go b/database/sql/util.go index 30946863..bc09142d 100644 --- a/database/sql/util.go +++ b/database/sql/util.go @@ -105,7 +105,7 @@ func (s *sqlDatabase) sqlAddressToParamsAddress(addr Address) commonParams.Addre } } -func (s *sqlDatabase) sqlToCommonOrganization(org Organization) (params.Organization, error) { +func (s *sqlDatabase) sqlToCommonOrganization(org Organization, detailed bool) (params.Organization, error) { if len(org.WebhookSecret) == 0 { return params.Organization{}, errors.New("missing secret") } @@ -114,20 +114,21 @@ func (s *sqlDatabase) sqlToCommonOrganization(org Organization) (params.Organiza return params.Organization{}, errors.Wrap(err, "decrypting secret") } - creds, err := s.sqlToCommonGithubCredentials(org.Credentials) - if err != nil { - return params.Organization{}, errors.Wrap(err, "converting credentials") - } - ret := params.Organization{ ID: org.ID.String(), Name: org.Name, - CredentialsName: creds.Name, - Credentials: creds, + CredentialsName: org.Credentials.Name, Pools: make([]params.Pool, len(org.Pools)), WebhookSecret: string(secret), PoolBalancerType: org.PoolBalancerType, } + if detailed { + creds, err := s.sqlToCommonGithubCredentials(org.Credentials) + if err != nil { + return params.Organization{}, errors.Wrap(err, "converting credentials") + } + ret.Credentials = creds + } if ret.PoolBalancerType == "" { ret.PoolBalancerType = params.PoolBalancerTypeRoundRobin @@ -143,7 +144,7 @@ func (s *sqlDatabase) sqlToCommonOrganization(org Organization) (params.Organiza return ret, nil } -func (s *sqlDatabase) sqlToCommonEnterprise(enterprise Enterprise) (params.Enterprise, error) { +func (s *sqlDatabase) sqlToCommonEnterprise(enterprise Enterprise, detailed bool) (params.Enterprise, error) { if len(enterprise.WebhookSecret) == 0 { return params.Enterprise{}, errors.New("missing secret") } @@ -152,20 +153,23 @@ func (s *sqlDatabase) sqlToCommonEnterprise(enterprise Enterprise) (params.Enter return params.Enterprise{}, errors.Wrap(err, "decrypting secret") } - creds, err := s.sqlToCommonGithubCredentials(enterprise.Credentials) - if err != nil { - return params.Enterprise{}, errors.Wrap(err, "converting credentials") - } ret := params.Enterprise{ ID: enterprise.ID.String(), Name: enterprise.Name, - CredentialsName: creds.Name, - Credentials: creds, + CredentialsName: enterprise.Credentials.Name, Pools: make([]params.Pool, len(enterprise.Pools)), WebhookSecret: string(secret), PoolBalancerType: enterprise.PoolBalancerType, } + if detailed { + creds, err := s.sqlToCommonGithubCredentials(enterprise.Credentials) + if err != nil { + return params.Enterprise{}, errors.Wrap(err, "converting credentials") + } + ret.Credentials = creds + } + if ret.PoolBalancerType == "" { ret.PoolBalancerType = params.PoolBalancerTypeRoundRobin } @@ -241,7 +245,7 @@ func (s *sqlDatabase) sqlToCommonTags(tag Tag) params.Tag { } } -func (s *sqlDatabase) sqlToCommonRepository(repo Repository) (params.Repository, error) { +func (s *sqlDatabase) sqlToCommonRepository(repo Repository, detailed bool) (params.Repository, error) { if len(repo.WebhookSecret) == 0 { return params.Repository{}, errors.New("missing secret") } @@ -250,21 +254,24 @@ func (s *sqlDatabase) sqlToCommonRepository(repo Repository) (params.Repository, return params.Repository{}, errors.Wrap(err, "decrypting secret") } - creds, err := s.sqlToCommonGithubCredentials(repo.Credentials) - if err != nil { - return params.Repository{}, errors.Wrap(err, "converting credentials") - } ret := params.Repository{ ID: repo.ID.String(), Name: repo.Name, Owner: repo.Owner, - CredentialsName: creds.Name, - Credentials: creds, + CredentialsName: repo.Credentials.Name, Pools: make([]params.Pool, len(repo.Pools)), WebhookSecret: string(secret), PoolBalancerType: repo.PoolBalancerType, } + if detailed { + creds, err := s.sqlToCommonGithubCredentials(repo.Credentials) + if err != nil { + return params.Repository{}, errors.Wrap(err, "converting credentials") + } + ret.Credentials = creds + } + if ret.PoolBalancerType == "" { ret.PoolBalancerType = params.PoolBalancerTypeRoundRobin } diff --git a/internal/testing/testing.go b/internal/testing/testing.go index 5f8624e6..d599aca4 100644 --- a/internal/testing/testing.go +++ b/internal/testing/testing.go @@ -18,19 +18,79 @@ package testing import ( + "context" "os" "path/filepath" "sort" "testing" + "github.com/pkg/errors" "github.com/stretchr/testify/require" + runnerErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/auth" "github.com/cloudbase/garm/config" + "github.com/cloudbase/garm/database/common" + "github.com/cloudbase/garm/params" + "github.com/cloudbase/garm/util/appdefaults" ) //nolint:golangci-lint,gosec var encryptionPassphrase = "bocyasicgatEtenOubwonIbsudNutDom" +func ImpersonateAdminContext(ctx context.Context, db common.Store, s *testing.T) context.Context { + adminUser, err := db.GetAdminUser(ctx) + if err != nil { + if !errors.Is(err, runnerErrors.ErrNotFound) { + s.Fatalf("failed to get admin user: %v", err) + } + newUserParams := params.NewUserParams{ + Email: "admin@localhost", + Username: "admin", + Password: "superSecretAdminPassword@123", + IsAdmin: true, + Enabled: true, + } + adminUser, err = db.CreateUser(ctx, newUserParams) + if err != nil { + s.Fatalf("failed to create admin user: %v", err) + } + } + ctx = auth.PopulateContext(ctx, adminUser) + return ctx +} + +func CreateDefaultGithubEndpoint(ctx context.Context, db common.Store, s *testing.T) params.GithubEndpoint { + endpointParams := params.CreateGithubEndpointParams{ + Name: "github.com", + Description: "github endpoint", + APIBaseURL: appdefaults.GithubDefaultBaseURL, + UploadBaseURL: appdefaults.GithubDefaultUploadBaseURL, + BaseURL: appdefaults.DefaultGithubURL, + } + endpoint, err := db.CreateGithubEndpoint(ctx, endpointParams) + if err != nil { + s.Fatalf("failed to create database object (github.com): %v", err) + } + return endpoint +} + +func CreateTestGithubCredentials(ctx context.Context, credsName string, db common.Store, s *testing.T, endpoint params.GithubEndpoint) params.GithubCredentials { + newCredsParams := params.CreateGithubCredentialsParams{ + Name: credsName, + Description: "Test creds", + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test-token", + }, + } + newCreds, err := db.CreateGithubCredentials(ctx, endpoint.Name, newCredsParams) + if err != nil { + s.Fatalf("failed to create database object (new-creds): %v", err) + } + return newCreds +} + func GetTestSqliteDBConfig(t *testing.T) config.Database { dir, err := os.MkdirTemp("", "garm-config-test") if err != nil { diff --git a/runner/enterprises.go b/runner/enterprises.go index c5274e09..8f8dbf31 100644 --- a/runner/enterprises.go +++ b/runner/enterprises.go @@ -25,8 +25,8 @@ func (r *Runner) CreateEnterprise(ctx context.Context, param params.CreateEnterp return params.Enterprise{}, errors.Wrap(err, "validating params") } - creds, ok := r.credentials[param.CredentialsName] - if !ok { + creds, err := r.store.GetGithubCredentialsByName(ctx, param.CredentialsName, true) + if err != nil { return params.Enterprise{}, runnerErrors.NewBadRequestError("credentials %s not defined", param.CredentialsName) } @@ -161,25 +161,13 @@ func (r *Runner) UpdateEnterprise(ctx context.Context, enterpriseID string, para r.mux.Lock() defer r.mux.Unlock() - enterprise, err := r.store.GetEnterpriseByID(ctx, enterpriseID) - if err != nil { - return params.Enterprise{}, errors.Wrap(err, "fetching enterprise") - } - - if param.CredentialsName != "" { - // Check that credentials are set before saving to db - if _, ok := r.credentials[param.CredentialsName]; !ok { - return params.Enterprise{}, runnerErrors.NewBadRequestError("invalid credentials (%s) for enterprise %s", param.CredentialsName, enterprise.Name) - } - } - switch param.PoolBalancerType { case params.PoolBalancerTypeRoundRobin, params.PoolBalancerTypePack, params.PoolBalancerTypeNone: default: return params.Enterprise{}, runnerErrors.NewBadRequestError("invalid pool balancer type: %s", param.PoolBalancerType) } - enterprise, err = r.store.UpdateEnterprise(ctx, enterpriseID, param) + enterprise, err := r.store.UpdateEnterprise(ctx, enterpriseID, param) if err != nil { return params.Enterprise{}, errors.Wrap(err, "updating enterprise") } diff --git a/runner/enterprises_test.go b/runner/enterprises_test.go index 2ad54e5d..501d96a8 100644 --- a/runner/enterprises_test.go +++ b/runner/enterprises_test.go @@ -19,12 +19,11 @@ import ( "fmt" "testing" + "github.com/pkg/errors" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" runnerErrors "github.com/cloudbase/garm-provider-common/errors" - "github.com/cloudbase/garm/auth" - "github.com/cloudbase/garm/config" "github.com/cloudbase/garm/database" dbCommon "github.com/cloudbase/garm/database/common" garmTesting "github.com/cloudbase/garm/internal/testing" //nolint:typecheck @@ -40,7 +39,7 @@ type EnterpriseTestFixtures struct { Store dbCommon.Store StoreEnterprises map[string]params.Enterprise Providers map[string]common.Provider - Credentials map[string]config.Github + Credentials map[string]params.GithubCredentials CreateEnterpriseParams params.CreateEnterpriseParams CreatePoolParams params.CreatePoolParams CreateInstanceParams params.CreateInstanceParams @@ -57,18 +56,25 @@ type EnterpriseTestSuite struct { suite.Suite Fixtures *EnterpriseTestFixtures Runner *Runner + + testCreds params.GithubCredentials + secondaryTestCreds params.GithubCredentials + githubEndpoint params.GithubEndpoint } func (s *EnterpriseTestSuite) SetupTest() { - adminCtx := auth.GetAdminContext(context.Background()) - // create testing sqlite database dbCfg := garmTesting.GetTestSqliteDBConfig(s.T()) - db, err := database.NewDatabase(adminCtx, dbCfg) + db, err := database.NewDatabase(context.Background(), dbCfg) if err != nil { s.FailNow(fmt.Sprintf("failed to create db connection: %s", err)) } + adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T()) + s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) + s.testCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), s.githubEndpoint) + s.secondaryTestCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "secondary-creds", db, s.T(), s.githubEndpoint) + // create some organization objects in the database, for testing purposes enterprises := map[string]params.Enterprise{} for i := 1; i <= 3; i++ { @@ -76,12 +82,12 @@ func (s *EnterpriseTestSuite) SetupTest() { enterprise, err := db.CreateEnterprise( adminCtx, name, - fmt.Sprintf("test-creds-%v", i), + s.testCreds.Name, fmt.Sprintf("test-webhook-secret-%v", i), params.PoolBalancerTypeRoundRobin, ) if err != nil { - s.FailNow(fmt.Sprintf("failed to create database object (test-enterprise-%v)", i)) + s.FailNow(fmt.Sprintf("failed to create database object (test-enterprise-%v): %+v", i, err)) } enterprises[name] = enterprise } @@ -98,16 +104,13 @@ func (s *EnterpriseTestSuite) SetupTest() { Providers: map[string]common.Provider{ "test-provider": providerMock, }, - Credentials: map[string]config.Github{ - "test-creds": { - Name: "test-creds-name", - Description: "test-creds-description", - OAuth2Token: "test-creds-oauth2-token", - }, + Credentials: map[string]params.GithubCredentials{ + s.testCreds.Name: s.testCreds, + s.secondaryTestCreds.Name: s.secondaryTestCreds, }, CreateEnterpriseParams: params.CreateEnterpriseParams{ Name: "test-enterprise-create", - CredentialsName: "test-creds", + CredentialsName: s.testCreds.Name, WebhookSecret: "test-create-enterprise-webhook-secret", }, CreatePoolParams: params.CreatePoolParams{ @@ -126,7 +129,7 @@ func (s *EnterpriseTestSuite) SetupTest() { OSType: "linux", }, UpdateRepoParams: params.UpdateEntityParams{ - CredentialsName: "test-creds", + CredentialsName: s.testCreds.Name, WebhookSecret: "test-update-repo-webhook-secret", }, UpdatePoolParams: params.UpdatePoolParams{ @@ -148,7 +151,6 @@ func (s *EnterpriseTestSuite) SetupTest() { // setup test runner runner := &Runner{ providers: fixtures.Providers, - credentials: fixtures.Credentials, ctx: fixtures.AdminContext, store: fixtures.Store, poolManagerCtrl: fixtures.PoolMgrCtrlMock, @@ -164,13 +166,13 @@ func (s *EnterpriseTestSuite) TestCreateEnterprise() { // call tested function enterprise, err := s.Runner.CreateEnterprise(s.Fixtures.AdminContext, s.Fixtures.CreateEnterpriseParams) - // assertions - s.Fixtures.PoolMgrMock.AssertExpectations(s.T()) - s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T()) s.Require().Nil(err) s.Require().Equal(s.Fixtures.CreateEnterpriseParams.Name, enterprise.Name) s.Require().Equal(s.Fixtures.Credentials[s.Fixtures.CreateEnterpriseParams.CredentialsName].Name, enterprise.Credentials.Name) s.Require().Equal(params.PoolBalancerTypeRoundRobin, enterprise.PoolBalancerType) + // assertions + s.Fixtures.PoolMgrMock.AssertExpectations(s.T()) + s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T()) } func (s *EnterpriseTestSuite) TestCreateEnterpriseErrUnauthorized() { @@ -322,7 +324,9 @@ func (s *EnterpriseTestSuite) TestUpdateEnterpriseInvalidCreds() { _, err := s.Runner.UpdateEnterprise(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, s.Fixtures.UpdateRepoParams) - s.Require().Equal(runnerErrors.NewBadRequestError("invalid credentials (%s) for enterprise %s", s.Fixtures.UpdateRepoParams.CredentialsName, s.Fixtures.StoreEnterprises["test-enterprise-1"].Name), err) + if !errors.Is(err, runnerErrors.ErrNotFound) { + s.FailNow(fmt.Sprintf("expected error: %v", runnerErrors.ErrNotFound)) + } } func (s *EnterpriseTestSuite) TestUpdateEnterprisePoolMgrFailed() { diff --git a/runner/organizations.go b/runner/organizations.go index 40847ccf..c0663505 100644 --- a/runner/organizations.go +++ b/runner/organizations.go @@ -38,8 +38,8 @@ func (r *Runner) CreateOrganization(ctx context.Context, param params.CreateOrgP return params.Organization{}, errors.Wrap(err, "validating params") } - creds, ok := r.credentials[param.CredentialsName] - if !ok { + creds, err := r.store.GetGithubCredentialsByName(ctx, param.CredentialsName, true) + if err != nil { return params.Organization{}, runnerErrors.NewBadRequestError("credentials %s not defined", param.CredentialsName) } @@ -190,25 +190,13 @@ func (r *Runner) UpdateOrganization(ctx context.Context, orgID string, param par r.mux.Lock() defer r.mux.Unlock() - org, err := r.store.GetOrganizationByID(ctx, orgID) - if err != nil { - return params.Organization{}, errors.Wrap(err, "fetching org") - } - - if param.CredentialsName != "" { - // Check that credentials are set before saving to db - if _, ok := r.credentials[param.CredentialsName]; !ok { - return params.Organization{}, runnerErrors.NewBadRequestError("invalid credentials (%s) for org %s", param.CredentialsName, org.Name) - } - } - switch param.PoolBalancerType { case params.PoolBalancerTypeRoundRobin, params.PoolBalancerTypePack, params.PoolBalancerTypeNone: default: return params.Organization{}, runnerErrors.NewBadRequestError("invalid pool balancer type: %s", param.PoolBalancerType) } - org, err = r.store.UpdateOrganization(ctx, orgID, param) + org, err := r.store.UpdateOrganization(ctx, orgID, param) if err != nil { return params.Organization{}, errors.Wrap(err, "updating org") } diff --git a/runner/organizations_test.go b/runner/organizations_test.go index 30a58882..7954d2e7 100644 --- a/runner/organizations_test.go +++ b/runner/organizations_test.go @@ -19,12 +19,11 @@ import ( "fmt" "testing" + "github.com/pkg/errors" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" runnerErrors "github.com/cloudbase/garm-provider-common/errors" - "github.com/cloudbase/garm/auth" - "github.com/cloudbase/garm/config" "github.com/cloudbase/garm/database" dbCommon "github.com/cloudbase/garm/database/common" garmTesting "github.com/cloudbase/garm/internal/testing" @@ -40,7 +39,7 @@ type OrgTestFixtures struct { Store dbCommon.Store StoreOrgs map[string]params.Organization Providers map[string]common.Provider - Credentials map[string]config.Github + Credentials map[string]params.GithubCredentials CreateOrgParams params.CreateOrgParams CreatePoolParams params.CreatePoolParams CreateInstanceParams params.CreateInstanceParams @@ -57,18 +56,26 @@ type OrgTestSuite struct { suite.Suite Fixtures *OrgTestFixtures Runner *Runner + + testCreds params.GithubCredentials + secondaryTestCreds params.GithubCredentials + githubEndpoint params.GithubEndpoint } func (s *OrgTestSuite) SetupTest() { - adminCtx := auth.GetAdminContext(context.Background()) - // create testing sqlite database dbCfg := garmTesting.GetTestSqliteDBConfig(s.T()) - db, err := database.NewDatabase(adminCtx, dbCfg) + db, err := database.NewDatabase(context.Background(), dbCfg) if err != nil { s.FailNow(fmt.Sprintf("failed to create db connection: %s", err)) } + adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T()) + + s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) + s.testCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), s.githubEndpoint) + s.secondaryTestCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "secondary-creds", db, s.T(), s.githubEndpoint) + // create some organization objects in the database, for testing purposes orgs := map[string]params.Organization{} for i := 1; i <= 3; i++ { @@ -76,7 +83,7 @@ func (s *OrgTestSuite) SetupTest() { org, err := db.CreateOrganization( adminCtx, name, - fmt.Sprintf("test-creds-%v", i), + s.testCreds.Name, fmt.Sprintf("test-webhook-secret-%v", i), params.PoolBalancerTypeRoundRobin, ) @@ -98,16 +105,13 @@ func (s *OrgTestSuite) SetupTest() { Providers: map[string]common.Provider{ "test-provider": providerMock, }, - Credentials: map[string]config.Github{ - "test-creds": { - Name: "test-creds-name", - Description: "test-creds-description", - OAuth2Token: "test-creds-oauth2-token", - }, + Credentials: map[string]params.GithubCredentials{ + s.testCreds.Name: s.testCreds, + s.secondaryTestCreds.Name: s.secondaryTestCreds, }, CreateOrgParams: params.CreateOrgParams{ Name: "test-org-create", - CredentialsName: "test-creds", + CredentialsName: s.testCreds.Name, WebhookSecret: "test-create-org-webhook-secret", }, CreatePoolParams: params.CreatePoolParams{ @@ -126,7 +130,7 @@ func (s *OrgTestSuite) SetupTest() { OSType: "linux", }, UpdateRepoParams: params.UpdateEntityParams{ - CredentialsName: "test-creds", + CredentialsName: s.testCreds.Name, WebhookSecret: "test-update-repo-webhook-secret", }, UpdatePoolParams: params.UpdatePoolParams{ @@ -148,7 +152,6 @@ func (s *OrgTestSuite) SetupTest() { // setup test runner runner := &Runner{ providers: fixtures.Providers, - credentials: fixtures.Credentials, ctx: fixtures.AdminContext, store: fixtures.Store, poolManagerCtrl: fixtures.PoolMgrCtrlMock, @@ -346,8 +349,9 @@ func (s *OrgTestSuite) TestUpdateOrganizationInvalidCreds() { s.Fixtures.UpdateRepoParams.CredentialsName = invalidCredentialsName _, err := s.Runner.UpdateOrganization(s.Fixtures.AdminContext, s.Fixtures.StoreOrgs["test-org-1"].ID, s.Fixtures.UpdateRepoParams) - - s.Require().Equal(runnerErrors.NewBadRequestError("invalid credentials (%s) for org %s", s.Fixtures.UpdateRepoParams.CredentialsName, s.Fixtures.StoreOrgs["test-org-1"].Name), err) + if !errors.Is(err, runnerErrors.ErrNotFound) { + s.FailNow(fmt.Sprintf("expected error: %v", runnerErrors.ErrNotFound)) + } } func (s *OrgTestSuite) TestUpdateOrganizationPoolMgrFailed() { diff --git a/runner/pools_test.go b/runner/pools_test.go index e2b269a0..49ca5a5c 100644 --- a/runner/pools_test.go +++ b/runner/pools_test.go @@ -45,6 +45,11 @@ type PoolTestSuite struct { suite.Suite Fixtures *PoolTestFixtures Runner *Runner + + adminCtx context.Context + testCreds params.GithubCredentials + secondaryTestCreds params.GithubCredentials + githubEndpoint params.GithubEndpoint } func (s *PoolTestSuite) SetupTest() { @@ -57,8 +62,14 @@ func (s *PoolTestSuite) SetupTest() { s.FailNow(fmt.Sprintf("failed to create db connection: %s", err)) } + s.adminCtx = garmTesting.ImpersonateAdminContext(adminCtx, db, s.T()) + + s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(s.adminCtx, db, s.T()) + s.testCreds = garmTesting.CreateTestGithubCredentials(s.adminCtx, "new-creds", db, s.T(), s.githubEndpoint) + s.secondaryTestCreds = garmTesting.CreateTestGithubCredentials(s.adminCtx, "secondary-creds", db, s.T(), s.githubEndpoint) + // create an organization for testing purposes - org, err := db.CreateOrganization(context.Background(), "test-org", "test-creds", "test-webhookSecret", params.PoolBalancerTypeRoundRobin) + org, err := db.CreateOrganization(s.adminCtx, "test-org", s.testCreds.Name, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) if err != nil { s.FailNow(fmt.Sprintf("failed to create org: %s", err)) } @@ -71,7 +82,7 @@ func (s *PoolTestSuite) SetupTest() { orgPools := []params.Pool{} for i := 1; i <= 3; i++ { pool, err := db.CreateEntityPool( - context.Background(), + adminCtx, entity, params.CreatePoolParams{ ProviderName: "test-provider", @@ -112,10 +123,9 @@ func (s *PoolTestSuite) SetupTest() { // setup test runner runner := &Runner{ - providers: fixtures.Providers, - credentials: fixtures.Credentials, - store: fixtures.Store, - ctx: fixtures.AdminContext, + providers: fixtures.Providers, + store: fixtures.Store, + ctx: fixtures.AdminContext, } s.Runner = runner } diff --git a/runner/repositories.go b/runner/repositories.go index f7692b69..b2a6ef54 100644 --- a/runner/repositories.go +++ b/runner/repositories.go @@ -38,8 +38,8 @@ func (r *Runner) CreateRepository(ctx context.Context, param params.CreateRepoPa return params.Repository{}, errors.Wrap(err, "validating params") } - creds, ok := r.credentials[param.CredentialsName] - if !ok { + creds, err := r.store.GetGithubCredentialsByName(ctx, param.CredentialsName, true) + if err != nil { return params.Repository{}, runnerErrors.NewBadRequestError("credentials %s not defined", param.CredentialsName) } @@ -189,25 +189,13 @@ func (r *Runner) UpdateRepository(ctx context.Context, repoID string, param para r.mux.Lock() defer r.mux.Unlock() - repo, err := r.store.GetRepositoryByID(ctx, repoID) - if err != nil { - return params.Repository{}, errors.Wrap(err, "fetching repo") - } - - if param.CredentialsName != "" { - // Check that credentials are set before saving to db - if _, ok := r.credentials[param.CredentialsName]; !ok { - return params.Repository{}, runnerErrors.NewBadRequestError("invalid credentials (%s) for repo %s/%s", param.CredentialsName, repo.Owner, repo.Name) - } - } - switch param.PoolBalancerType { case params.PoolBalancerTypeRoundRobin, params.PoolBalancerTypePack, params.PoolBalancerTypeNone: default: return params.Repository{}, runnerErrors.NewBadRequestError("invalid pool balancer type: %s", param.PoolBalancerType) } - repo, err = r.store.UpdateRepository(ctx, repoID, param) + repo, err := r.store.UpdateRepository(ctx, repoID, param) if err != nil { return params.Repository{}, errors.Wrap(err, "updating repo") } diff --git a/runner/repositories_test.go b/runner/repositories_test.go index 74bc8a76..aa8da725 100644 --- a/runner/repositories_test.go +++ b/runner/repositories_test.go @@ -19,12 +19,12 @@ import ( "fmt" "testing" + "github.com/pkg/errors" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" runnerErrors "github.com/cloudbase/garm-provider-common/errors" "github.com/cloudbase/garm/auth" - "github.com/cloudbase/garm/config" "github.com/cloudbase/garm/database" dbCommon "github.com/cloudbase/garm/database/common" garmTesting "github.com/cloudbase/garm/internal/testing" @@ -39,7 +39,7 @@ type RepoTestFixtures struct { Store dbCommon.Store StoreRepos map[string]params.Repository Providers map[string]common.Provider - Credentials map[string]config.Github + Credentials map[string]params.GithubCredentials CreateRepoParams params.CreateRepoParams CreatePoolParams params.CreatePoolParams CreateInstanceParams params.CreateInstanceParams @@ -56,18 +56,25 @@ type RepoTestSuite struct { suite.Suite Fixtures *RepoTestFixtures Runner *Runner + + testCreds params.GithubCredentials + secondaryTestCreds params.GithubCredentials + githubEndpoint params.GithubEndpoint } func (s *RepoTestSuite) SetupTest() { - adminCtx := auth.GetAdminContext(context.Background()) - // create testing sqlite database dbCfg := garmTesting.GetTestSqliteDBConfig(s.T()) - db, err := database.NewDatabase(adminCtx, dbCfg) + db, err := database.NewDatabase(context.Background(), dbCfg) if err != nil { s.FailNow(fmt.Sprintf("failed to create db connection: %s", err)) } + adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T()) + s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) + s.testCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), s.githubEndpoint) + s.secondaryTestCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "secondary-creds", db, s.T(), s.githubEndpoint) + // create some repository objects in the database, for testing purposes repos := map[string]params.Repository{} for i := 1; i <= 3; i++ { @@ -76,7 +83,7 @@ func (s *RepoTestSuite) SetupTest() { adminCtx, fmt.Sprintf("test-owner-%v", i), name, - fmt.Sprintf("test-creds-%v", i), + s.testCreds.Name, fmt.Sprintf("test-webhook-secret-%v", i), params.PoolBalancerTypeRoundRobin, ) @@ -97,17 +104,14 @@ func (s *RepoTestSuite) SetupTest() { Providers: map[string]common.Provider{ "test-provider": providerMock, }, - Credentials: map[string]config.Github{ - "test-creds": { - Name: "test-creds-name", - Description: "test-creds-description", - OAuth2Token: "test-creds-oauth2-token", - }, + Credentials: map[string]params.GithubCredentials{ + s.testCreds.Name: s.testCreds, + s.secondaryTestCreds.Name: s.secondaryTestCreds, }, CreateRepoParams: params.CreateRepoParams{ Owner: "test-owner-create", Name: "test-repo-create", - CredentialsName: "test-creds", + CredentialsName: s.testCreds.Name, WebhookSecret: "test-create-repo-webhook-secret", }, CreatePoolParams: params.CreatePoolParams{ @@ -126,7 +130,7 @@ func (s *RepoTestSuite) SetupTest() { OSType: "linux", }, UpdateRepoParams: params.UpdateEntityParams{ - CredentialsName: "test-creds", + CredentialsName: s.testCreds.Name, WebhookSecret: "test-update-repo-webhook-secret", }, UpdatePoolParams: params.UpdatePoolParams{ @@ -148,7 +152,6 @@ func (s *RepoTestSuite) SetupTest() { // setup test runner runner := &Runner{ providers: fixtures.Providers, - credentials: fixtures.Credentials, ctx: fixtures.AdminContext, store: fixtures.Store, poolManagerCtrl: fixtures.PoolMgrCtrlMock, @@ -167,6 +170,7 @@ func (s *RepoTestSuite) TestCreateRepository() { // assertions s.Fixtures.PoolMgrMock.AssertExpectations(s.T()) s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T()) + s.Require().Nil(err) s.Require().Equal(s.Fixtures.CreateRepoParams.Owner, repo.Owner) s.Require().Equal(s.Fixtures.CreateRepoParams.Name, repo.Name) @@ -358,7 +362,9 @@ func (s *RepoTestSuite) TestUpdateRepositoryInvalidCreds() { _, err := s.Runner.UpdateRepository(s.Fixtures.AdminContext, s.Fixtures.StoreRepos["test-repo-1"].ID, s.Fixtures.UpdateRepoParams) - s.Require().Equal(runnerErrors.NewBadRequestError("invalid credentials (%s) for repo %s/%s", s.Fixtures.UpdateRepoParams.CredentialsName, s.Fixtures.StoreRepos["test-repo-1"].Owner, s.Fixtures.StoreRepos["test-repo-1"].Name), err) + if !errors.Is(err, runnerErrors.ErrNotFound) { + s.FailNow(fmt.Sprintf("expected error: %v", runnerErrors.ErrNotFound)) + } } func (s *RepoTestSuite) TestUpdateRepositoryPoolMgrFailed() { diff --git a/runner/runner.go b/runner/runner.go index fd10ad78..78e298d3 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -77,7 +77,6 @@ func NewRunner(ctx context.Context, cfg config.Config, db dbCommon.Store) (*Runn store: db, poolManagerCtrl: poolManagerCtrl, providers: providers, - credentials: creds, controllerID: ctrlID.ControllerID, } @@ -355,8 +354,7 @@ type Runner struct { poolManagerCtrl PoolManagerController - providers map[string]common.Provider - credentials map[string]config.Github + providers map[string]common.Provider controllerInfo params.ControllerInfo controllerID uuid.UUID From 3e60a48ca8e8b4bad045d6a224e6d121b0811a53 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Wed, 17 Apr 2024 08:05:06 +0000 Subject: [PATCH 03/27] Preload credentials endpoint and remove extra code Signed-off-by: Gabriel Adrian Samfira --- Makefile | 2 +- database/sql/enterprise.go | 3 ++- database/sql/github.go | 14 ++++++++++++++ database/sql/organizations.go | 3 ++- database/sql/repositories.go | 3 ++- params/params.go | 9 ++++----- runner/runner.go | 31 ++++++++++--------------------- util/util.go | 10 ++++++---- 8 files changed, 41 insertions(+), 34 deletions(-) diff --git a/Makefile b/Makefile index ce21ce4b..3dcab4a9 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ export SHELLOPTS:=$(if $(SHELLOPTS),$(SHELLOPTS):)pipefail:errexit .ONESHELL: -GEN_PASSWORD=$(shell (apg -n1 -m32)) +GEN_PASSWORD=$(shell (/usr/bin/apg -n1 -m32)) IMAGE_TAG = garm-build USER_ID=$(shell ((docker --version | grep -q podman) && echo "0" || id -u)) diff --git a/database/sql/enterprise.go b/database/sql/enterprise.go index e8efcf8b..9fa3c73c 100644 --- a/database/sql/enterprise.go +++ b/database/sql/enterprise.go @@ -96,7 +96,7 @@ func (s *sqlDatabase) GetEnterpriseByID(ctx context.Context, enterpriseID string func (s *sqlDatabase) ListEnterprises(_ context.Context) ([]params.Enterprise, error) { var enterprises []Enterprise - q := s.conn.Preload("Credentials").Find(&enterprises) + q := s.conn.Preload("Credentials").Preload("Credentials.Endpoint").Find(&enterprises) if q.Error != nil { return []params.Enterprise{}, errors.Wrap(q.Error, "fetching enterprises") } @@ -183,6 +183,7 @@ func (s *sqlDatabase) getEnterprise(_ context.Context, name string) (Enterprise, q := s.conn.Where("name = ? COLLATE NOCASE", name). Preload("Credentials"). + Preload("Credentials.Endpoint"). Preload("Endpoint"). First(&enterprise) if q.Error != nil { diff --git a/database/sql/github.go b/database/sql/github.go index 300c0ef4..08e22f62 100644 --- a/database/sql/github.go +++ b/database/sql/github.go @@ -1,3 +1,17 @@ +// Copyright 2024 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 sql import ( diff --git a/database/sql/organizations.go b/database/sql/organizations.go index 0019ad39..419f81b5 100644 --- a/database/sql/organizations.go +++ b/database/sql/organizations.go @@ -87,7 +87,7 @@ func (s *sqlDatabase) GetOrganization(ctx context.Context, name string) (params. func (s *sqlDatabase) ListOrganizations(_ context.Context) ([]params.Organization, error) { var orgs []Organization - q := s.conn.Preload("Credentials").Find(&orgs) + q := s.conn.Preload("Credentials").Preload("Credentials.Endpoint").Find(&orgs) if q.Error != nil { return []params.Organization{}, errors.Wrap(q.Error, "fetching org from database") } @@ -213,6 +213,7 @@ func (s *sqlDatabase) getOrg(_ context.Context, name string) (Organization, erro q := s.conn.Where("name = ? COLLATE NOCASE", name). Preload("Credentials"). + Preload("Credentials.Endpoint"). Preload("Endpoint"). First(&org) if q.Error != nil { diff --git a/database/sql/repositories.go b/database/sql/repositories.go index 18284e1b..26e0d0bc 100644 --- a/database/sql/repositories.go +++ b/database/sql/repositories.go @@ -85,7 +85,7 @@ func (s *sqlDatabase) GetRepository(ctx context.Context, owner, name string) (pa func (s *sqlDatabase) ListRepositories(_ context.Context) ([]params.Repository, error) { var repos []Repository - q := s.conn.Preload("Credentials").Find(&repos) + q := s.conn.Preload("Credentials").Preload("Credentials.Endpoint").Find(&repos) if q.Error != nil { return []params.Repository{}, errors.Wrap(q.Error, "fetching user from database") } @@ -186,6 +186,7 @@ func (s *sqlDatabase) getRepo(_ context.Context, owner, name string) (Repository q := s.conn.Where("name = ? COLLATE NOCASE and owner = ? COLLATE NOCASE", name, owner). Preload("Credentials"). + Preload("Credentials.Endpoint"). Preload("Endpoint"). First(&repo) diff --git a/params/params.go b/params/params.go index e6bf8fb6..8409855d 100644 --- a/params/params.go +++ b/params/params.go @@ -566,8 +566,7 @@ type GithubCredentials struct { Enterprises []Enterprise `json:"enterprises,omitempty"` Endpoint string `json:"endpoint"` - CredentialsPayload []byte `json:"-"` - HTTPClient *http.Client `json:"-"` + CredentialsPayload []byte `json:"-"` } func (g GithubCredentials) GetHTTPClient(ctx context.Context) (*http.Client, error) { @@ -579,11 +578,11 @@ func (g GithubCredentials) GetHTTPClient(ctx context.Context) (*http.Client, err return nil, fmt.Errorf("failed to parse CA cert") } } - // nolint:golangci-lint,gosec,godox - // TODO: set TLS MinVersion + httpTransport := &http.Transport{ TLSClientConfig: &tls.Config{ - RootCAs: roots, + RootCAs: roots, + MinVersion: tls.VersionTLS12, }, } diff --git a/runner/runner.go b/runner/runner.go index 78e298d3..bc2c3676 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -316,32 +316,21 @@ func (p *poolManagerCtrl) GetEnterprisePoolManagers() (map[string]common.PoolMan return p.enterprises, nil } -func (p *poolManagerCtrl) getInternalConfig(ctx context.Context, creds params.GithubCredentials, poolBalancerType params.PoolBalancerType) (params.Internal, error) { +func (p *poolManagerCtrl) getInternalConfig(_ context.Context, creds params.GithubCredentials, poolBalancerType params.PoolBalancerType) (params.Internal, error) { var controllerWebhookURL string if p.config.Default.WebhookURL != "" { controllerWebhookURL = fmt.Sprintf("%s/%s", p.config.Default.WebhookURL, p.controllerID) } - httpClient, err := creds.GetHTTPClient(ctx) - if err != nil { - return params.Internal{}, fmt.Errorf("fetching http client for creds: %w", err) - } + return params.Internal{ - ControllerID: p.controllerID, - InstanceCallbackURL: p.config.Default.CallbackURL, - InstanceMetadataURL: p.config.Default.MetadataURL, - BaseWebhookURL: p.config.Default.WebhookURL, - ControllerWebhookURL: controllerWebhookURL, - JWTSecret: p.config.JWTAuth.Secret, - PoolBalancerType: poolBalancerType, - GithubCredentialsDetails: params.GithubCredentials{ - Name: creds.Name, - Description: creds.Description, - BaseURL: creds.BaseURL, - APIBaseURL: creds.APIBaseURL, - UploadBaseURL: creds.UploadBaseURL, - CABundle: creds.CABundle, - HTTPClient: httpClient, - }, + ControllerID: p.controllerID, + InstanceCallbackURL: p.config.Default.CallbackURL, + InstanceMetadataURL: p.config.Default.MetadataURL, + BaseWebhookURL: p.config.Default.WebhookURL, + ControllerWebhookURL: controllerWebhookURL, + JWTSecret: p.config.JWTAuth.Secret, + PoolBalancerType: poolBalancerType, + GithubCredentialsDetails: creds, }, nil } diff --git a/util/util.go b/util/util.go index a75be33f..56f2150a 100644 --- a/util/util.go +++ b/util/util.go @@ -435,11 +435,13 @@ func (g *githubClient) GetEntityJITConfig(ctx context.Context, instance string, return jitConfig, ret.Runner, nil } -func GithubClient(_ context.Context, entity params.GithubEntity, credsDetails params.GithubCredentials) (common.GithubClient, error) { - if credsDetails.HTTPClient == nil { - return nil, errors.New("http client is nil") +func GithubClient(ctx context.Context, entity params.GithubEntity, credsDetails params.GithubCredentials) (common.GithubClient, error) { + httpClient, err := credsDetails.GetHTTPClient(ctx) + if err != nil { + return nil, errors.Wrap(err, "fetching http client") } - ghClient, err := github.NewClient(credsDetails.HTTPClient).WithEnterpriseURLs(credsDetails.APIBaseURL, credsDetails.UploadBaseURL) + + ghClient, err := github.NewClient(httpClient).WithEnterpriseURLs(credsDetails.APIBaseURL, credsDetails.UploadBaseURL) if err != nil { return nil, errors.Wrap(err, "fetching github client") } From 9c1ffe8c20e9414176ad5f390d713a734d5de0e8 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Wed, 17 Apr 2024 12:10:00 +0000 Subject: [PATCH 04/27] Enforce same endpoint when updating credentials When updating credentials on an entity, we must ensure that the new credentials belong to the same endpoint as the entity. When an entity is created, the endpoint is determined by the credentials that were used during the create operation. From that point forward the entity is associated with an endpoint, and that cannot change. Signed-off-by: Gabriel Adrian Samfira --- database/sql/enterprise.go | 23 ++++++++++++- database/sql/enterprise_test.go | 54 ++++++++++++++++++++--------- database/sql/organizations.go | 22 +++++++++++- database/sql/organizations_test.go | 55 +++++++++++++++++++++--------- database/sql/repositories.go | 23 ++++++++++++- database/sql/repositories_test.go | 55 +++++++++++++++++++++--------- database/sql/util.go | 16 ++++++++- params/params.go | 3 ++ util/util.go | 2 +- 9 files changed, 200 insertions(+), 53 deletions(-) diff --git a/database/sql/enterprise.go b/database/sql/enterprise.go index 9fa3c73c..53be988d 100644 --- a/database/sql/enterprise.go +++ b/database/sql/enterprise.go @@ -45,7 +45,12 @@ func (s *sqlDatabase) CreateEnterprise(ctx context.Context, name, credentialsNam if err != nil { return errors.Wrap(err, "creating enterprise") } + if creds.EndpointName == nil { + return errors.Wrap(runnerErrors.ErrUnprocessable, "credentials have no endpoint") + } newEnterprise.CredentialsID = &creds.ID + newEnterprise.CredentialsName = creds.Name + newEnterprise.EndpointName = creds.EndpointName q := tx.Create(&newEnterprise) if q.Error != nil { @@ -53,6 +58,7 @@ func (s *sqlDatabase) CreateEnterprise(ctx context.Context, name, credentialsNam } newEnterprise.Credentials = creds + newEnterprise.Endpoint = creds.Endpoint return nil }) @@ -132,16 +138,27 @@ func (s *sqlDatabase) UpdateEnterprise(ctx context.Context, enterpriseID string, var creds GithubCredentials err := s.conn.Transaction(func(tx *gorm.DB) error { var err error - enterprise, err = s.getEnterpriseByID(ctx, tx, enterpriseID, "Credentials", "Endpoint") + enterprise, err = s.getEnterpriseByID(ctx, tx, enterpriseID) if err != nil { return errors.Wrap(err, "fetching enterprise") } + if enterprise.EndpointName == nil { + return errors.Wrap(runnerErrors.ErrUnprocessable, "enterprise has no endpoint") + } + if param.CredentialsName != "" { creds, err = s.getGithubCredentialsByName(ctx, tx, param.CredentialsName, false) if err != nil { return errors.Wrap(err, "fetching credentials") } + if creds.EndpointName == nil { + return errors.Wrap(runnerErrors.ErrUnprocessable, "credentials have no endpoint") + } + + if *creds.EndpointName != *enterprise.EndpointName { + return errors.Wrap(runnerErrors.ErrBadRequest, "endpoint mismatch") + } enterprise.CredentialsID = &creds.ID } if param.WebhookSecret != "" { @@ -171,6 +188,10 @@ func (s *sqlDatabase) UpdateEnterprise(ctx context.Context, enterpriseID string, return params.Enterprise{}, errors.Wrap(err, "updating enterprise") } + enterprise, err = s.getEnterpriseByID(ctx, s.conn, enterpriseID, "Endpoint", "Credentials") + if err != nil { + return params.Enterprise{}, errors.Wrap(err, "updating enterprise") + } newParams, err := s.sqlToCommonEnterprise(enterprise, true) if err != nil { return params.Enterprise{}, errors.Wrap(err, "updating enterprise") diff --git a/database/sql/enterprise_test.go b/database/sql/enterprise_test.go index 3509f946..b442ec86 100644 --- a/database/sql/enterprise_test.go +++ b/database/sql/enterprise_test.go @@ -218,7 +218,11 @@ func (s *EnterpriseTestSuite) TestCreateEnterpriseDBCreateErr() { s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). WithArgs(s.Fixtures.Enterprises[0].CredentialsName). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.testCreds.ID)) + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}).AddRow(s.testCreds.ID, s.testCreds.Endpoint)) + s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). + WithArgs(s.testCreds.Endpoint). + WillReturnRows(sqlmock.NewRows([]string{"name"}). + AddRow(s.testCreds.Endpoint)) s.Fixtures.SQLMock. ExpectExec(regexp.QuoteMeta("INSERT INTO `enterprises`")). WillReturnError(fmt.Errorf("creating enterprise mock error")) @@ -346,11 +350,17 @@ func (s *EnterpriseTestSuite) TestUpdateEnterpriseDBEncryptErr() { s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `enterprises` WHERE id = ? AND `enterprises`.`deleted_at` IS NULL ORDER BY `enterprises`.`id` LIMIT ?")). WithArgs(s.Fixtures.Enterprises[0].ID, 1). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Enterprises[0].ID)) - s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). - WithArgs(s.secondaryTestCreds.Name). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.Fixtures.Enterprises[0].ID, s.Fixtures.Enterprises[0].Endpoint.Name)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.secondaryTestCreds.Name, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). + WithArgs(s.testCreds.Endpoint). + WillReturnRows(sqlmock.NewRows([]string{"name"}). + AddRow(s.secondaryTestCreds.Endpoint)) s.Fixtures.SQLMock.ExpectRollback() _, err := s.StoreSQLMocked.UpdateEnterprise(s.adminCtx, s.Fixtures.Enterprises[0].ID, s.Fixtures.UpdateRepoParams) @@ -365,11 +375,17 @@ func (s *EnterpriseTestSuite) TestUpdateEnterpriseDBSaveErr() { s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `enterprises` WHERE id = ? AND `enterprises`.`deleted_at` IS NULL ORDER BY `enterprises`.`id` LIMIT ?")). WithArgs(s.Fixtures.Enterprises[0].ID, 1). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Enterprises[0].ID)) - s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). - WithArgs(s.secondaryTestCreds.Name). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.Fixtures.Enterprises[0].ID, s.Fixtures.Enterprises[0].Endpoint.Name)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.secondaryTestCreds.Name, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). + WithArgs(s.testCreds.Endpoint). + WillReturnRows(sqlmock.NewRows([]string{"name"}). + AddRow(s.secondaryTestCreds.Endpoint)) s.Fixtures.SQLMock. ExpectExec(("UPDATE `enterprises` SET")). WillReturnError(fmt.Errorf("saving enterprise mock error")) @@ -390,11 +406,17 @@ func (s *EnterpriseTestSuite) TestUpdateEnterpriseDBDecryptingErr() { s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `enterprises` WHERE id = ? AND `enterprises`.`deleted_at` IS NULL ORDER BY `enterprises`.`id` LIMIT ?")). WithArgs(s.Fixtures.Enterprises[0].ID, 1). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Enterprises[0].ID)) - s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). - WithArgs(s.secondaryTestCreds.Name). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.Fixtures.Enterprises[0].ID, s.Fixtures.Enterprises[0].Endpoint.Name)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.secondaryTestCreds.Name, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). + WithArgs(s.testCreds.Endpoint). + WillReturnRows(sqlmock.NewRows([]string{"name"}). + AddRow(s.secondaryTestCreds.Endpoint)) s.Fixtures.SQLMock.ExpectRollback() _, err := s.StoreSQLMocked.UpdateEnterprise(s.adminCtx, s.Fixtures.Enterprises[0].ID, s.Fixtures.UpdateRepoParams) diff --git a/database/sql/organizations.go b/database/sql/organizations.go index 419f81b5..2e76cb18 100644 --- a/database/sql/organizations.go +++ b/database/sql/organizations.go @@ -47,7 +47,12 @@ func (s *sqlDatabase) CreateOrganization(ctx context.Context, name, credentialsN if err != nil { return errors.Wrap(err, "creating org") } + if creds.EndpointName == nil { + return errors.Wrap(runnerErrors.ErrUnprocessable, "credentials have no endpoint") + } newOrg.CredentialsID = &creds.ID + newOrg.CredentialsName = creds.Name + newOrg.EndpointName = creds.EndpointName q := tx.Create(&newOrg) if q.Error != nil { @@ -55,6 +60,7 @@ func (s *sqlDatabase) CreateOrganization(ctx context.Context, name, credentialsN } newOrg.Credentials = creds + newOrg.Endpoint = creds.Endpoint return nil }) @@ -123,10 +129,13 @@ func (s *sqlDatabase) UpdateOrganization(ctx context.Context, orgID string, para var creds GithubCredentials err := s.conn.Transaction(func(tx *gorm.DB) error { var err error - org, err = s.getOrgByID(ctx, tx, orgID, "Credentials", "Endpoint") + org, err = s.getOrgByID(ctx, tx, orgID) if err != nil { return errors.Wrap(err, "fetching org") } + if org.EndpointName == nil { + return errors.Wrap(runnerErrors.ErrUnprocessable, "org has no endpoint") + } if param.CredentialsName != "" { org.CredentialsName = param.CredentialsName @@ -134,6 +143,13 @@ func (s *sqlDatabase) UpdateOrganization(ctx context.Context, orgID string, para if err != nil { return errors.Wrap(err, "fetching credentials") } + if creds.EndpointName == nil { + return errors.Wrap(runnerErrors.ErrUnprocessable, "credentials have no endpoint") + } + + if *creds.EndpointName != *org.EndpointName { + return errors.Wrap(runnerErrors.ErrBadRequest, "endpoint mismatch") + } org.CredentialsID = &creds.ID } @@ -164,6 +180,10 @@ func (s *sqlDatabase) UpdateOrganization(ctx context.Context, orgID string, para return params.Organization{}, errors.Wrap(err, "saving org") } + org, err = s.getOrgByID(ctx, s.conn, orgID, "Endpoint", "Credentials") + if err != nil { + return params.Organization{}, errors.Wrap(err, "updating enterprise") + } newParams, err := s.sqlToCommonOrganization(org, true) if err != nil { return params.Organization{}, errors.Wrap(err, "saving org") diff --git a/database/sql/organizations_test.go b/database/sql/organizations_test.go index 55e791db..c0524ff7 100644 --- a/database/sql/organizations_test.go +++ b/database/sql/organizations_test.go @@ -219,7 +219,12 @@ func (s *OrgTestSuite) TestCreateOrganizationDBCreateErr() { s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). WithArgs(s.Fixtures.Orgs[0].CredentialsName). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.testCreds.ID)) + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.testCreds.ID, s.githubEndpoint.Name)) + s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). + WithArgs(s.testCreds.Endpoint). + WillReturnRows(sqlmock.NewRows([]string{"name"}). + AddRow(s.githubEndpoint.Name)) s.Fixtures.SQLMock. ExpectExec(regexp.QuoteMeta("INSERT INTO `organizations`")). WillReturnError(fmt.Errorf("creating org mock error")) @@ -347,11 +352,17 @@ func (s *OrgTestSuite) TestUpdateOrganizationDBEncryptErr() { s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `organizations` WHERE id = ? AND `organizations`.`deleted_at` IS NULL ORDER BY `organizations`.`id` LIMIT ?")). WithArgs(s.Fixtures.Orgs[0].ID, 1). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Orgs[0].ID)) - s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). - WithArgs(s.secondaryTestCreds.Name). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.Fixtures.Orgs[0].ID, s.Fixtures.Orgs[0].Endpoint.Name)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.secondaryTestCreds.Name, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). + WithArgs(s.testCreds.Endpoint). + WillReturnRows(sqlmock.NewRows([]string{"name"}). + AddRow(s.secondaryTestCreds.Endpoint)) s.Fixtures.SQLMock.ExpectRollback() _, err := s.StoreSQLMocked.UpdateOrganization(s.adminCtx, s.Fixtures.Orgs[0].ID, s.Fixtures.UpdateRepoParams) @@ -366,11 +377,17 @@ func (s *OrgTestSuite) TestUpdateOrganizationDBSaveErr() { s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `organizations` WHERE id = ? AND `organizations`.`deleted_at` IS NULL ORDER BY `organizations`.`id` LIMIT ?")). WithArgs(s.Fixtures.Orgs[0].ID, 1). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Orgs[0].ID)) - s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). - WithArgs(s.secondaryTestCreds.Name). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.Fixtures.Orgs[0].ID, s.Fixtures.Orgs[0].Endpoint.Name)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.secondaryTestCreds.Name, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). + WithArgs(s.testCreds.Endpoint). + WillReturnRows(sqlmock.NewRows([]string{"name"}). + AddRow(s.secondaryTestCreds.Endpoint)) s.Fixtures.SQLMock. ExpectExec(("UPDATE `organizations` SET")). WillReturnError(fmt.Errorf("saving org mock error")) @@ -391,11 +408,17 @@ func (s *OrgTestSuite) TestUpdateOrganizationDBDecryptingErr() { s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `organizations` WHERE id = ? AND `organizations`.`deleted_at` IS NULL ORDER BY `organizations`.`id` LIMIT ?")). WithArgs(s.Fixtures.Orgs[0].ID, 1). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Orgs[0].ID)) - s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). - WithArgs(s.secondaryTestCreds.Name). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.Fixtures.Orgs[0].ID, s.Fixtures.Orgs[0].Endpoint.Name)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.secondaryTestCreds.Name, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). + WithArgs(s.testCreds.Endpoint). + WillReturnRows(sqlmock.NewRows([]string{"name"}). + AddRow(s.secondaryTestCreds.Endpoint)) s.Fixtures.SQLMock.ExpectRollback() _, err := s.StoreSQLMocked.UpdateOrganization(s.adminCtx, s.Fixtures.Orgs[0].ID, s.Fixtures.UpdateRepoParams) diff --git a/database/sql/repositories.go b/database/sql/repositories.go index 26e0d0bc..a938dd80 100644 --- a/database/sql/repositories.go +++ b/database/sql/repositories.go @@ -47,7 +47,12 @@ func (s *sqlDatabase) CreateRepository(ctx context.Context, owner, name, credent if err != nil { return errors.Wrap(err, "creating repository") } + if creds.EndpointName == nil { + return errors.Wrap(runnerErrors.ErrUnprocessable, "credentials have no endpoint") + } newRepo.CredentialsID = &creds.ID + newRepo.CredentialsName = creds.Name + newRepo.EndpointName = creds.EndpointName q := tx.Create(&newRepo) if q.Error != nil { @@ -55,6 +60,8 @@ func (s *sqlDatabase) CreateRepository(ctx context.Context, owner, name, credent } newRepo.Credentials = creds + newRepo.Endpoint = creds.Endpoint + return nil }) if err != nil { @@ -121,10 +128,13 @@ func (s *sqlDatabase) UpdateRepository(ctx context.Context, repoID string, param var creds GithubCredentials err := s.conn.Transaction(func(tx *gorm.DB) error { var err error - repo, err = s.getRepoByID(ctx, tx, repoID, "Credentials", "Endpoint") + repo, err = s.getRepoByID(ctx, tx, repoID) if err != nil { return errors.Wrap(err, "fetching repo") } + if repo.EndpointName == nil { + return errors.Wrap(runnerErrors.ErrUnprocessable, "repository has no endpoint") + } if param.CredentialsName != "" { repo.CredentialsName = param.CredentialsName @@ -132,6 +142,13 @@ func (s *sqlDatabase) UpdateRepository(ctx context.Context, repoID string, param if err != nil { return errors.Wrap(err, "fetching credentials") } + if creds.EndpointName == nil { + return errors.Wrap(runnerErrors.ErrUnprocessable, "credentials have no endpoint") + } + + if *creds.EndpointName != *repo.EndpointName { + return errors.Wrap(runnerErrors.ErrBadRequest, "endpoint mismatch") + } repo.CredentialsID = &creds.ID } @@ -161,6 +178,10 @@ func (s *sqlDatabase) UpdateRepository(ctx context.Context, repoID string, param return params.Repository{}, errors.Wrap(err, "saving repo") } + repo, err = s.getRepoByID(ctx, s.conn, repoID, "Endpoint", "Credentials") + if err != nil { + return params.Repository{}, errors.Wrap(err, "updating enterprise") + } newParams, err := s.sqlToCommonRepository(repo, true) if err != nil { return params.Repository{}, errors.Wrap(err, "saving repo") diff --git a/database/sql/repositories_test.go b/database/sql/repositories_test.go index 09f65a7a..7fb272e9 100644 --- a/database/sql/repositories_test.go +++ b/database/sql/repositories_test.go @@ -235,7 +235,12 @@ func (s *RepoTestSuite) TestCreateRepositoryInvalidDBCreateErr() { s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). WithArgs(s.Fixtures.Repos[0].CredentialsName). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.testCreds.ID)) + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.testCreds.ID, s.githubEndpoint.Name)) + s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). + WithArgs(s.testCreds.Endpoint). + WillReturnRows(sqlmock.NewRows([]string{"name"}). + AddRow(s.githubEndpoint.Name)) s.Fixtures.SQLMock. ExpectExec(regexp.QuoteMeta("INSERT INTO `repositories`")). WillReturnError(fmt.Errorf("creating repo mock error")) @@ -385,11 +390,17 @@ func (s *RepoTestSuite) TestUpdateRepositoryDBEncryptErr() { s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `repositories` WHERE id = ? AND `repositories`.`deleted_at` IS NULL ORDER BY `repositories`.`id` LIMIT ?")). WithArgs(s.Fixtures.Repos[0].ID, 1). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Repos[0].ID)) - s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). - WithArgs(s.secondaryTestCreds.Name). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.Fixtures.Repos[0].ID, s.githubEndpoint.Name)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.secondaryTestCreds.Name, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). + WithArgs(s.testCreds.Endpoint). + WillReturnRows(sqlmock.NewRows([]string{"name"}). + AddRow(s.secondaryTestCreds.Endpoint)) s.Fixtures.SQLMock.ExpectRollback() _, err := s.StoreSQLMocked.UpdateRepository(s.adminCtx, s.Fixtures.Repos[0].ID, s.Fixtures.UpdateRepoParams) @@ -404,11 +415,17 @@ func (s *RepoTestSuite) TestUpdateRepositoryDBSaveErr() { s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `repositories` WHERE id = ? AND `repositories`.`deleted_at` IS NULL ORDER BY `repositories`.`id` LIMIT ?")). WithArgs(s.Fixtures.Repos[0].ID, 1). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Repos[0].ID)) - s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). - WithArgs(s.secondaryTestCreds.Name). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.Fixtures.Repos[0].ID, s.githubEndpoint.Name)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.secondaryTestCreds.Name, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). + WithArgs(s.testCreds.Endpoint). + WillReturnRows(sqlmock.NewRows([]string{"name"}). + AddRow(s.secondaryTestCreds.Endpoint)) s.Fixtures.SQLMock. ExpectExec(("UPDATE `repositories` SET")). WillReturnError(fmt.Errorf("saving repo mock error")) @@ -428,11 +445,17 @@ func (s *RepoTestSuite) TestUpdateRepositoryDBDecryptingErr() { s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `repositories` WHERE id = ? AND `repositories`.`deleted_at` IS NULL ORDER BY `repositories`.`id` LIMIT ?")). WithArgs(s.Fixtures.Repos[0].ID, 1). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Repos[0].ID)) - s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). - WithArgs(s.secondaryTestCreds.Name). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.secondaryTestCreds.ID)) + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.Fixtures.Repos[0].ID, s.githubEndpoint.Name)) + s.Fixtures.SQLMock. + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.secondaryTestCreds.Name, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). + WithArgs(s.testCreds.Endpoint). + WillReturnRows(sqlmock.NewRows([]string{"name"}). + AddRow(s.secondaryTestCreds.Endpoint)) s.Fixtures.SQLMock.ExpectRollback() _, err := s.StoreSQLMocked.UpdateRepository(s.adminCtx, s.Fixtures.Repos[0].ID, s.Fixtures.UpdateRepoParams) diff --git a/database/sql/util.go b/database/sql/util.go index bc09142d..c291e5d9 100644 --- a/database/sql/util.go +++ b/database/sql/util.go @@ -114,6 +114,10 @@ func (s *sqlDatabase) sqlToCommonOrganization(org Organization, detailed bool) ( return params.Organization{}, errors.Wrap(err, "decrypting secret") } + endpoint, err := s.sqlToCommonGithubEndpoint(org.Endpoint) + if err != nil { + return params.Organization{}, errors.Wrap(err, "converting endpoint") + } ret := params.Organization{ ID: org.ID.String(), Name: org.Name, @@ -121,6 +125,7 @@ func (s *sqlDatabase) sqlToCommonOrganization(org Organization, detailed bool) ( Pools: make([]params.Pool, len(org.Pools)), WebhookSecret: string(secret), PoolBalancerType: org.PoolBalancerType, + Endpoint: endpoint, } if detailed { creds, err := s.sqlToCommonGithubCredentials(org.Credentials) @@ -153,6 +158,10 @@ func (s *sqlDatabase) sqlToCommonEnterprise(enterprise Enterprise, detailed bool return params.Enterprise{}, errors.Wrap(err, "decrypting secret") } + endpoint, err := s.sqlToCommonGithubEndpoint(enterprise.Endpoint) + if err != nil { + return params.Enterprise{}, errors.Wrap(err, "converting endpoint") + } ret := params.Enterprise{ ID: enterprise.ID.String(), Name: enterprise.Name, @@ -160,6 +169,7 @@ func (s *sqlDatabase) sqlToCommonEnterprise(enterprise Enterprise, detailed bool Pools: make([]params.Pool, len(enterprise.Pools)), WebhookSecret: string(secret), PoolBalancerType: enterprise.PoolBalancerType, + Endpoint: endpoint, } if detailed { @@ -253,7 +263,10 @@ func (s *sqlDatabase) sqlToCommonRepository(repo Repository, detailed bool) (par if err != nil { return params.Repository{}, errors.Wrap(err, "decrypting secret") } - + endpoint, err := s.sqlToCommonGithubEndpoint(repo.Endpoint) + if err != nil { + return params.Repository{}, errors.Wrap(err, "converting endpoint") + } ret := params.Repository{ ID: repo.ID.String(), Name: repo.Name, @@ -262,6 +275,7 @@ func (s *sqlDatabase) sqlToCommonRepository(repo Repository, detailed bool) (par Pools: make([]params.Pool, len(repo.Pools)), WebhookSecret: string(secret), PoolBalancerType: repo.PoolBalancerType, + Endpoint: endpoint, } if detailed { diff --git a/params/params.go b/params/params.go index 8409855d..fcaf3113 100644 --- a/params/params.go +++ b/params/params.go @@ -405,6 +405,7 @@ type Repository struct { Credentials GithubCredentials `json:"credentials"` PoolManagerStatus PoolManagerStatus `json:"pool_manager_status,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancing_type"` + Endpoint GithubEndpoint `json:"endpoint"` // Do not serialize sensitive info. WebhookSecret string `json:"-"` } @@ -447,6 +448,7 @@ type Organization struct { Credentials GithubCredentials `json:"credentials"` PoolManagerStatus PoolManagerStatus `json:"pool_manager_status,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancing_type"` + Endpoint GithubEndpoint `json:"endpoint"` // Do not serialize sensitive info. WebhookSecret string `json:"-"` } @@ -489,6 +491,7 @@ type Enterprise struct { Credentials GithubCredentials `json:"credentials"` PoolManagerStatus PoolManagerStatus `json:"pool_manager_status,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancing_type"` + Endpoint GithubEndpoint `json:"endpoint"` // Do not serialize sensitive info. WebhookSecret string `json:"-"` } diff --git a/util/util.go b/util/util.go index 56f2150a..eb390743 100644 --- a/util/util.go +++ b/util/util.go @@ -59,7 +59,7 @@ func (g *githubClient) ListEntityHooks(ctx context.Context, opts *github.ListOpt case params.GithubEntityTypeOrganization: ret, response, err = g.org.ListHooks(ctx, g.entity.Owner, opts) default: - return nil, nil, errors.New("invalid entity type") + return nil, nil, fmt.Errorf("invalid entity type: %s", g.entity.EntityType) } return ret, response, err } From 4610f832099b23418ec5bbced8aededec7a6bf3e Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Wed, 17 Apr 2024 12:45:42 +0000 Subject: [PATCH 05/27] List credentials from db Signed-off-by: Gabriel Adrian Samfira --- database/sql/enterprise.go | 6 +++++- database/sql/organizations.go | 6 +++++- database/sql/repositories.go | 6 +++++- runner/runner.go | 18 ++++++------------ 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/database/sql/enterprise.go b/database/sql/enterprise.go index 53be988d..7d20d2e8 100644 --- a/database/sql/enterprise.go +++ b/database/sql/enterprise.go @@ -102,7 +102,11 @@ func (s *sqlDatabase) GetEnterpriseByID(ctx context.Context, enterpriseID string func (s *sqlDatabase) ListEnterprises(_ context.Context) ([]params.Enterprise, error) { var enterprises []Enterprise - q := s.conn.Preload("Credentials").Preload("Credentials.Endpoint").Find(&enterprises) + q := s.conn. + Preload("Credentials"). + Preload("Credentials.Endpoint"). + Preload("Endpoint"). + Find(&enterprises) if q.Error != nil { return []params.Enterprise{}, errors.Wrap(q.Error, "fetching enterprises") } diff --git a/database/sql/organizations.go b/database/sql/organizations.go index 2e76cb18..1192c843 100644 --- a/database/sql/organizations.go +++ b/database/sql/organizations.go @@ -93,7 +93,11 @@ func (s *sqlDatabase) GetOrganization(ctx context.Context, name string) (params. func (s *sqlDatabase) ListOrganizations(_ context.Context) ([]params.Organization, error) { var orgs []Organization - q := s.conn.Preload("Credentials").Preload("Credentials.Endpoint").Find(&orgs) + q := s.conn. + Preload("Credentials"). + Preload("Credentials.Endpoint"). + Preload("Endpoint"). + Find(&orgs) if q.Error != nil { return []params.Organization{}, errors.Wrap(q.Error, "fetching org from database") } diff --git a/database/sql/repositories.go b/database/sql/repositories.go index a938dd80..8e16b97f 100644 --- a/database/sql/repositories.go +++ b/database/sql/repositories.go @@ -92,7 +92,11 @@ func (s *sqlDatabase) GetRepository(ctx context.Context, owner, name string) (pa func (s *sqlDatabase) ListRepositories(_ context.Context) ([]params.Repository, error) { var repos []Repository - q := s.conn.Preload("Credentials").Preload("Credentials.Endpoint").Find(&repos) + q := s.conn. + Preload("Credentials"). + Preload("Credentials.Endpoint"). + Preload("Endpoint"). + Find(&repos) if q.Error != nil { return []params.Repository{}, errors.Wrap(q.Error, "fetching user from database") } diff --git a/runner/runner.go b/runner/runner.go index bc2c3676..bc28a6c3 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -399,19 +399,13 @@ func (r *Runner) ListCredentials(ctx context.Context) ([]params.GithubCredential if !auth.IsAdmin(ctx) { return nil, runnerErrors.ErrUnauthorized } - ret := []params.GithubCredentials{} - - for _, val := range r.config.Github { - ret = append(ret, params.GithubCredentials{ - Name: val.Name, - Description: val.Description, - BaseURL: val.BaseEndpoint(), - APIBaseURL: val.APIEndpoint(), - UploadBaseURL: val.UploadEndpoint(), - AuthType: params.GithubAuthType(val.AuthType), - }) + + creds, err := r.store.ListGithubCredentials(ctx) + if err != nil { + return nil, errors.Wrap(err, "fetching github credentials") } - return ret, nil + + return creds, nil } func (r *Runner) ListProviders(ctx context.Context) ([]params.Provider, error) { From 257fb0b09a9493633f14e539b045cc85e0652352 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Wed, 17 Apr 2024 13:45:56 +0000 Subject: [PATCH 06/27] Take into account legacy config Signed-off-by: Gabriel Adrian Samfira --- config/config.go | 7 +++++++ database/sql/sql.go | 10 +++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/config/config.go b/config/config.go index baafcb8e..5a6fd0a2 100644 --- a/config/config.go +++ b/config/config.go @@ -299,6 +299,13 @@ type Github struct { App GithubApp `toml:"app" json:"app"` } +func (g *Github) GetAuthType() GithubAuthType { + if g.AuthType == "" { + return GithubAuthTypePAT + } + return g.AuthType +} + func (g *Github) APIEndpoint() string { if g.APIBaseURL != "" { return g.APIBaseURL diff --git a/database/sql/sql.go b/database/sql/sql.go index 1bc16e08..2436dc3d 100644 --- a/database/sql/sql.go +++ b/database/sql/sql.go @@ -297,7 +297,7 @@ func (s *sqlDatabase) migrateCredentialsToDB() (err error) { credParams := params.CreateGithubCredentialsParams{ Name: cred.Name, Description: cred.Description, - AuthType: params.GithubAuthType(cred.AuthType), + AuthType: params.GithubAuthType(cred.GetAuthType()), } switch credParams.AuthType { case params.GithubAuthTypeApp: @@ -315,11 +315,15 @@ func (s *sqlDatabase) migrateCredentialsToDB() (err error) { return errors.Wrap(err, "validating app credentials") } case params.GithubAuthTypePAT: - if cred.PAT.OAuth2Token == "" { + token := cred.PAT.OAuth2Token + if token == "" { + token = cred.OAuth2Token + } + if token == "" { return errors.New("missing OAuth2 token") } credParams.PAT = params.GithubPAT{ - OAuth2Token: cred.PAT.OAuth2Token, + OAuth2Token: token, } } From 77ecb161663d4b6ae2888f65cad64c9996db7898 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Thu, 18 Apr 2024 16:50:46 +0000 Subject: [PATCH 07/27] Add github endpoint API endpoint and CLI code Signed-off-by: Gabriel Adrian Samfira --- apiserver/controllers/controllers.go | 26 +- apiserver/controllers/credentials.go | 28 ++ apiserver/controllers/endpoints.go | 186 ++++++++++++ apiserver/routers/routers.go | 23 +- apiserver/swagger-models.yaml | 37 +++ apiserver/swagger.yaml | 140 +++++++++ .../create_github_endpoint_parameters.go | 151 ++++++++++ .../create_github_endpoint_responses.go | 179 +++++++++++ .../delete_github_endpoint_parameters.go | 151 ++++++++++ .../delete_github_endpoint_responses.go | 103 +++++++ client/endpoints/endpoints_client.go | 231 +++++++++++++++ .../get_github_endpoint_parameters.go | 151 ++++++++++ .../get_github_endpoint_responses.go | 179 +++++++++++ .../list_github_endpoints_parameters.go | 128 ++++++++ .../list_github_endpoints_responses.go | 179 +++++++++++ .../update_github_endpoint_parameters.go | 173 +++++++++++ .../update_github_endpoint_responses.go | 179 +++++++++++ client/garm_api_client.go | 5 + cmd/garm-cli/cmd/github.go | 30 ++ cmd/garm-cli/cmd/github_endpoints.go | 277 ++++++++++++++++++ database/sql/enterprise_test.go | 24 +- database/sql/github.go | 32 +- database/sql/organizations_test.go | 20 +- database/sql/repositories_test.go | 20 +- params/params.go | 9 +- params/requests.go | 137 ++++++++- runner/endpoints.go | 82 ++++++ 27 files changed, 2807 insertions(+), 73 deletions(-) create mode 100644 apiserver/controllers/credentials.go create mode 100644 apiserver/controllers/endpoints.go create mode 100644 client/endpoints/create_github_endpoint_parameters.go create mode 100644 client/endpoints/create_github_endpoint_responses.go create mode 100644 client/endpoints/delete_github_endpoint_parameters.go create mode 100644 client/endpoints/delete_github_endpoint_responses.go create mode 100644 client/endpoints/endpoints_client.go create mode 100644 client/endpoints/get_github_endpoint_parameters.go create mode 100644 client/endpoints/get_github_endpoint_responses.go create mode 100644 client/endpoints/list_github_endpoints_parameters.go create mode 100644 client/endpoints/list_github_endpoints_responses.go create mode 100644 client/endpoints/update_github_endpoint_parameters.go create mode 100644 client/endpoints/update_github_endpoint_responses.go create mode 100644 cmd/garm-cli/cmd/github.go create mode 100644 cmd/garm-cli/cmd/github_endpoints.go create mode 100644 runner/endpoints.go diff --git a/apiserver/controllers/controllers.go b/apiserver/controllers/controllers.go index 256d8efc..c1d4561e 100644 --- a/apiserver/controllers/controllers.go +++ b/apiserver/controllers/controllers.go @@ -102,7 +102,10 @@ func (a *APIController) handleWorkflowJobEvent(ctx context.Context, w http.Respo handleError(ctx, w, gErrors.NewBadRequestError("invalid post body: %s", err)) return } - + slog.Info("received webhook", "body", string(body)) + for k, v := range r.Header { + slog.InfoContext(ctx, "header", "key", k, "value", v) + } signature := r.Header.Get("X-Hub-Signature-256") hookType := r.Header.Get("X-Github-Hook-Installation-Target-Type") @@ -329,27 +332,6 @@ func (a *APIController) FirstRunHandler(w http.ResponseWriter, r *http.Request) } } -// swagger:route GET /credentials credentials ListCredentials -// -// List all credentials. -// -// Responses: -// 200: Credentials -// 400: APIErrorResponse -func (a *APIController) ListCredentials(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - creds, err := a.r.ListCredentials(ctx) - if err != nil { - handleError(ctx, w, err) - return - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(creds); err != nil { - slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") - } -} - // swagger:route GET /providers providers ListProviders // // List all providers. diff --git a/apiserver/controllers/credentials.go b/apiserver/controllers/credentials.go new file mode 100644 index 00000000..bfc3d494 --- /dev/null +++ b/apiserver/controllers/credentials.go @@ -0,0 +1,28 @@ +package controllers + +import ( + "encoding/json" + "log/slog" + "net/http" +) + +// swagger:route GET /credentials credentials ListCredentials +// +// List all credentials. +// +// Responses: +// 200: Credentials +// 400: APIErrorResponse +func (a *APIController) ListCredentials(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + creds, err := a.r.ListCredentials(ctx) + if err != nil { + handleError(ctx, w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(creds); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") + } +} diff --git a/apiserver/controllers/endpoints.go b/apiserver/controllers/endpoints.go new file mode 100644 index 00000000..81e984d4 --- /dev/null +++ b/apiserver/controllers/endpoints.go @@ -0,0 +1,186 @@ +package controllers + +import ( + "encoding/json" + "log/slog" + "net/http" + + "github.com/gorilla/mux" + + gErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/params" +) + +// swagger:route POST /github/endpoints endpoints CreateGithubEndpoint +// +// Create a GitHub Endpoint. +// +// Parameters: +// + name: Body +// description: Parameters used when creating a GitHub endpoint. +// type: CreateGithubEndpointParams +// in: body +// required: true +// +// Responses: +// 200: GithubEndpoint +// default: APIErrorResponse +func (a *APIController) CreateGithubEndpoint(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var params params.CreateGithubEndpointParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to decode request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + endpoint, err := a.r.CreateGithubEndpoint(ctx, params) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to create GitHub endpoint") + handleError(ctx, w, err) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(endpoint); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") + } +} + +// swagger:route GET /github/endpoints endpoints ListGithubEndpoints +// +// List all GitHub Endpoints. +// +// Responses: +// 200: GithubEndpoints +// default: APIErrorResponse +func (a *APIController) ListGithubEndpoints(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + endpoints, err := a.r.ListGithubEndpoints(ctx) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to list GitHub endpoints") + handleError(ctx, w, err) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(endpoints); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") + } +} + +// swagger:route GET /github/endpoints/{name} endpoints GetGithubEndpoint +// +// Get a GitHub Endpoint. +// +// Parameters: +// + name: name +// description: The name of the GitHub endpoint. +// type: string +// in: path +// required: true +// +// Responses: +// 200: GithubEndpoint +// default: APIErrorResponse +func (a *APIController) GetGithubEndpoint(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok { + slog.ErrorContext(ctx, "missing name in request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + endpoint, err := a.r.GetGithubEndpoint(ctx, name) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to get GitHub endpoint") + handleError(ctx, w, err) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(endpoint); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") + } +} + +// swagger:route DELETE /github/endpoints/{name} endpoints DeleteGithubEndpoint +// +// Delete a GitHub Endpoint. +// +// Parameters: +// + name: name +// description: The name of the GitHub endpoint. +// type: string +// in: path +// required: true +// +// Responses: +// default: APIErrorResponse +func (a *APIController) DeleteGithubEndpoint(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok { + slog.ErrorContext(ctx, "missing name in request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + if err := a.r.DeleteGithubEndpoint(ctx, name); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to delete GitHub endpoint") + handleError(ctx, w, err) + return + } + w.WriteHeader(http.StatusNoContent) +} + +// swagger:route PUT /github/endpoints/{name} endpoints UpdateGithubEndpoint +// +// Update a GitHub Endpoint. +// +// Parameters: +// + name: name +// description: The name of the GitHub endpoint. +// type: string +// in: path +// required: true +// + name: Body +// description: Parameters used when updating a GitHub endpoint. +// type: UpdateGithubEndpointParams +// in: body +// required: true +// +// Responses: +// 200: GithubEndpoint +// default: APIErrorResponse +func (a *APIController) UpdateGithubEndpoint(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok { + slog.ErrorContext(ctx, "missing name in request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + var params params.UpdateGithubEndpointParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to decode request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + endpoint, err := a.r.UpdateGithubEndpoint(ctx, name, params) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to update GitHub endpoint") + handleError(ctx, w, err) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(endpoint); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") + } +} diff --git a/apiserver/routers/routers.go b/apiserver/routers/routers.go index 5b4e69ab..fcb971d0 100644 --- a/apiserver/routers/routers.go +++ b/apiserver/routers/routers.go @@ -339,9 +339,11 @@ func NewAPIRouter(han *controllers.APIController, authMiddleware, initMiddleware apiRouter.Handle("/enterprises/", http.HandlerFunc(han.CreateEnterpriseHandler)).Methods("POST", "OPTIONS") apiRouter.Handle("/enterprises", http.HandlerFunc(han.CreateEnterpriseHandler)).Methods("POST", "OPTIONS") - // Credentials and providers + // Credentials apiRouter.Handle("/credentials/", http.HandlerFunc(han.ListCredentials)).Methods("GET", "OPTIONS") apiRouter.Handle("/credentials", http.HandlerFunc(han.ListCredentials)).Methods("GET", "OPTIONS") + + // Providers apiRouter.Handle("/providers/", http.HandlerFunc(han.ListProviders)).Methods("GET", "OPTIONS") apiRouter.Handle("/providers", http.HandlerFunc(han.ListProviders)).Methods("GET", "OPTIONS") @@ -349,6 +351,25 @@ func NewAPIRouter(han *controllers.APIController, authMiddleware, initMiddleware apiRouter.Handle("/controller-info/", http.HandlerFunc(han.ControllerInfoHandler)).Methods("GET", "OPTIONS") apiRouter.Handle("/controller-info", http.HandlerFunc(han.ControllerInfoHandler)).Methods("GET", "OPTIONS") + ////////////////////// + // Github Endpoints // + ////////////////////// + // Create Github Endpoint + apiRouter.Handle("/github/endpoints/", http.HandlerFunc(han.CreateGithubEndpoint)).Methods("POST", "OPTIONS") + apiRouter.Handle("/github/endpoints", http.HandlerFunc(han.CreateGithubEndpoint)).Methods("POST", "OPTIONS") + // List Github Endpoints + apiRouter.Handle("/github/endpoints/", http.HandlerFunc(han.ListGithubEndpoints)).Methods("GET", "OPTIONS") + apiRouter.Handle("/github/endpoints", http.HandlerFunc(han.ListGithubEndpoints)).Methods("GET", "OPTIONS") + // Get Github Endpoint + apiRouter.Handle("/github/endpoints/{name}/", http.HandlerFunc(han.GetGithubEndpoint)).Methods("GET", "OPTIONS") + apiRouter.Handle("/github/endpoints/{name}", http.HandlerFunc(han.GetGithubEndpoint)).Methods("GET", "OPTIONS") + // Delete Github Endpoint + apiRouter.Handle("/github/endpoints/{name}/", http.HandlerFunc(han.DeleteGithubEndpoint)).Methods("DELETE", "OPTIONS") + apiRouter.Handle("/github/endpoints/{name}", http.HandlerFunc(han.DeleteGithubEndpoint)).Methods("DELETE", "OPTIONS") + // Update Github Endpoint + apiRouter.Handle("/github/endpoints/{name}/", http.HandlerFunc(han.UpdateGithubEndpoint)).Methods("PUT", "OPTIONS") + apiRouter.Handle("/github/endpoints/{name}", http.HandlerFunc(han.UpdateGithubEndpoint)).Methods("PUT", "OPTIONS") + // Websocket log writer apiRouter.Handle("/{ws:ws\\/?}", http.HandlerFunc(han.WSHandler)).Methods("GET") diff --git a/apiserver/swagger-models.yaml b/apiserver/swagger-models.yaml index fa8181f7..ae9bdd0b 100644 --- a/apiserver/swagger-models.yaml +++ b/apiserver/swagger-models.yaml @@ -227,3 +227,40 @@ definitions: import: package: github.com/cloudbase/garm/apiserver/params alias: apiserver_params + CreateInstanceParams: + type: object + x-go-type: + type: CreateInstanceParams + import: + package: github.com/cloudbase/garm/params + alias: garm_params + UpdateGithubEndpointParams: + type: object + x-go-type: + type: UpdateGithubEndpointParams + import: + package: github.com/cloudbase/garm/params + alias: garm_params + GithubEndpoint: + type: object + x-go-type: + type: GithubEndpoint + import: + package: github.com/cloudbase/garm/params + alias: garm_params + GithubEndpoints: + type: array + x-go-type: + type: GithubEndpoints + import: + package: github.com/cloudbase/garm/params + alias: garm_params + items: + $ref: '#/definitions/GithubEndpoint' + CreateGithubEndpointParams: + type: object + x-go-type: + type: CreateGithubEndpointParams + import: + package: github.com/cloudbase/garm/params + alias: garm_params \ No newline at end of file diff --git a/apiserver/swagger.yaml b/apiserver/swagger.yaml index 8c15758c..53ca9309 100644 --- a/apiserver/swagger.yaml +++ b/apiserver/swagger.yaml @@ -23,6 +23,20 @@ definitions: alias: garm_params package: github.com/cloudbase/garm/params type: CreateEnterpriseParams + CreateGithubEndpointParams: + type: object + x-go-type: + import: + alias: garm_params + package: github.com/cloudbase/garm/params + type: CreateGithubEndpointParams + CreateInstanceParams: + type: object + x-go-type: + import: + alias: garm_params + package: github.com/cloudbase/garm/params + type: CreateInstanceParams CreateOrgParams: type: object x-go-type: @@ -76,6 +90,22 @@ definitions: alias: garm_params package: github.com/cloudbase/garm/params type: GithubCredentials + GithubEndpoint: + type: object + x-go-type: + import: + alias: garm_params + package: github.com/cloudbase/garm/params + type: GithubEndpoint + GithubEndpoints: + items: + $ref: '#/definitions/GithubEndpoint' + type: array + x-go-type: + import: + alias: garm_params + package: github.com/cloudbase/garm/params + type: GithubEndpoints HookInfo: type: object x-go-type: @@ -214,6 +244,13 @@ definitions: alias: garm_params package: github.com/cloudbase/garm/params type: UpdateEntityParams + UpdateGithubEndpointParams: + type: object + x-go-type: + import: + alias: garm_params + package: github.com/cloudbase/garm/params + type: UpdateGithubEndpointParams UpdatePoolParams: type: object x-go-type: @@ -573,6 +610,109 @@ paths: summary: Initialize the first run of the controller. tags: - first-run + /github/endpoints: + get: + operationId: ListGithubEndpoints + responses: + "200": + description: GithubEndpoints + schema: + $ref: '#/definitions/GithubEndpoints' + default: + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: List all GitHub Endpoints. + tags: + - endpoints + post: + operationId: CreateGithubEndpoint + parameters: + - description: Parameters used when creating a GitHub endpoint. + in: body + name: Body + required: true + schema: + $ref: '#/definitions/CreateGithubEndpointParams' + description: Parameters used when creating a GitHub endpoint. + type: object + responses: + "200": + description: GithubEndpoint + schema: + $ref: '#/definitions/GithubEndpoint' + default: + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: Create a GitHub Endpoint. + tags: + - endpoints + /github/endpoints/{name}: + delete: + operationId: DeleteGithubEndpoint + parameters: + - description: The name of the GitHub endpoint. + in: path + name: name + required: true + type: string + responses: + default: + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: Delete a GitHub Endpoint. + tags: + - endpoints + get: + operationId: GetGithubEndpoint + parameters: + - description: The name of the GitHub endpoint. + in: path + name: name + required: true + type: string + responses: + "200": + description: GithubEndpoint + schema: + $ref: '#/definitions/GithubEndpoint' + default: + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: Get a GitHub Endpoint. + tags: + - endpoints + put: + operationId: UpdateGithubEndpoint + parameters: + - description: The name of the GitHub endpoint. + in: path + name: name + required: true + type: string + - description: Parameters used when updating a GitHub endpoint. + in: body + name: Body + required: true + schema: + $ref: '#/definitions/UpdateGithubEndpointParams' + description: Parameters used when updating a GitHub endpoint. + type: object + responses: + "200": + description: GithubEndpoint + schema: + $ref: '#/definitions/GithubEndpoint' + default: + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: Update a GitHub Endpoint. + tags: + - endpoints /instances: get: operationId: ListInstances diff --git a/client/endpoints/create_github_endpoint_parameters.go b/client/endpoints/create_github_endpoint_parameters.go new file mode 100644 index 00000000..030fa167 --- /dev/null +++ b/client/endpoints/create_github_endpoint_parameters.go @@ -0,0 +1,151 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + + garm_params "github.com/cloudbase/garm/params" +) + +// NewCreateGithubEndpointParams creates a new CreateGithubEndpointParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewCreateGithubEndpointParams() *CreateGithubEndpointParams { + return &CreateGithubEndpointParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewCreateGithubEndpointParamsWithTimeout creates a new CreateGithubEndpointParams object +// with the ability to set a timeout on a request. +func NewCreateGithubEndpointParamsWithTimeout(timeout time.Duration) *CreateGithubEndpointParams { + return &CreateGithubEndpointParams{ + timeout: timeout, + } +} + +// NewCreateGithubEndpointParamsWithContext creates a new CreateGithubEndpointParams object +// with the ability to set a context for a request. +func NewCreateGithubEndpointParamsWithContext(ctx context.Context) *CreateGithubEndpointParams { + return &CreateGithubEndpointParams{ + Context: ctx, + } +} + +// NewCreateGithubEndpointParamsWithHTTPClient creates a new CreateGithubEndpointParams object +// with the ability to set a custom HTTPClient for a request. +func NewCreateGithubEndpointParamsWithHTTPClient(client *http.Client) *CreateGithubEndpointParams { + return &CreateGithubEndpointParams{ + HTTPClient: client, + } +} + +/* +CreateGithubEndpointParams contains all the parameters to send to the API endpoint + + for the create github endpoint operation. + + Typically these are written to a http.Request. +*/ +type CreateGithubEndpointParams struct { + + /* Body. + + Parameters used when creating a GitHub endpoint. + */ + Body garm_params.CreateGithubEndpointParams + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the create github endpoint params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *CreateGithubEndpointParams) WithDefaults() *CreateGithubEndpointParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the create github endpoint params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *CreateGithubEndpointParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the create github endpoint params +func (o *CreateGithubEndpointParams) WithTimeout(timeout time.Duration) *CreateGithubEndpointParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the create github endpoint params +func (o *CreateGithubEndpointParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the create github endpoint params +func (o *CreateGithubEndpointParams) WithContext(ctx context.Context) *CreateGithubEndpointParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the create github endpoint params +func (o *CreateGithubEndpointParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the create github endpoint params +func (o *CreateGithubEndpointParams) WithHTTPClient(client *http.Client) *CreateGithubEndpointParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the create github endpoint params +func (o *CreateGithubEndpointParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithBody adds the body to the create github endpoint params +func (o *CreateGithubEndpointParams) WithBody(body garm_params.CreateGithubEndpointParams) *CreateGithubEndpointParams { + o.SetBody(body) + return o +} + +// SetBody adds the body to the create github endpoint params +func (o *CreateGithubEndpointParams) SetBody(body garm_params.CreateGithubEndpointParams) { + o.Body = body +} + +// WriteToRequest writes these params to a swagger request +func (o *CreateGithubEndpointParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + if err := r.SetBodyParam(o.Body); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/endpoints/create_github_endpoint_responses.go b/client/endpoints/create_github_endpoint_responses.go new file mode 100644 index 00000000..62ea8150 --- /dev/null +++ b/client/endpoints/create_github_endpoint_responses.go @@ -0,0 +1,179 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" + garm_params "github.com/cloudbase/garm/params" +) + +// CreateGithubEndpointReader is a Reader for the CreateGithubEndpoint structure. +type CreateGithubEndpointReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *CreateGithubEndpointReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewCreateGithubEndpointOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + default: + result := NewCreateGithubEndpointDefault(response.Code()) + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + if response.Code()/100 == 2 { + return result, nil + } + return nil, result + } +} + +// NewCreateGithubEndpointOK creates a CreateGithubEndpointOK with default headers values +func NewCreateGithubEndpointOK() *CreateGithubEndpointOK { + return &CreateGithubEndpointOK{} +} + +/* +CreateGithubEndpointOK describes a response with status code 200, with default header values. + +GithubEndpoint +*/ +type CreateGithubEndpointOK struct { + Payload garm_params.GithubEndpoint +} + +// IsSuccess returns true when this create github endpoint o k response has a 2xx status code +func (o *CreateGithubEndpointOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this create github endpoint o k response has a 3xx status code +func (o *CreateGithubEndpointOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this create github endpoint o k response has a 4xx status code +func (o *CreateGithubEndpointOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this create github endpoint o k response has a 5xx status code +func (o *CreateGithubEndpointOK) IsServerError() bool { + return false +} + +// IsCode returns true when this create github endpoint o k response a status code equal to that given +func (o *CreateGithubEndpointOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the create github endpoint o k response +func (o *CreateGithubEndpointOK) Code() int { + return 200 +} + +func (o *CreateGithubEndpointOK) Error() string { + return fmt.Sprintf("[POST /github/endpoints][%d] createGithubEndpointOK %+v", 200, o.Payload) +} + +func (o *CreateGithubEndpointOK) String() string { + return fmt.Sprintf("[POST /github/endpoints][%d] createGithubEndpointOK %+v", 200, o.Payload) +} + +func (o *CreateGithubEndpointOK) GetPayload() garm_params.GithubEndpoint { + return o.Payload +} + +func (o *CreateGithubEndpointOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewCreateGithubEndpointDefault creates a CreateGithubEndpointDefault with default headers values +func NewCreateGithubEndpointDefault(code int) *CreateGithubEndpointDefault { + return &CreateGithubEndpointDefault{ + _statusCode: code, + } +} + +/* +CreateGithubEndpointDefault describes a response with status code -1, with default header values. + +APIErrorResponse +*/ +type CreateGithubEndpointDefault struct { + _statusCode int + + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this create github endpoint default response has a 2xx status code +func (o *CreateGithubEndpointDefault) IsSuccess() bool { + return o._statusCode/100 == 2 +} + +// IsRedirect returns true when this create github endpoint default response has a 3xx status code +func (o *CreateGithubEndpointDefault) IsRedirect() bool { + return o._statusCode/100 == 3 +} + +// IsClientError returns true when this create github endpoint default response has a 4xx status code +func (o *CreateGithubEndpointDefault) IsClientError() bool { + return o._statusCode/100 == 4 +} + +// IsServerError returns true when this create github endpoint default response has a 5xx status code +func (o *CreateGithubEndpointDefault) IsServerError() bool { + return o._statusCode/100 == 5 +} + +// IsCode returns true when this create github endpoint default response a status code equal to that given +func (o *CreateGithubEndpointDefault) IsCode(code int) bool { + return o._statusCode == code +} + +// Code gets the status code for the create github endpoint default response +func (o *CreateGithubEndpointDefault) Code() int { + return o._statusCode +} + +func (o *CreateGithubEndpointDefault) Error() string { + return fmt.Sprintf("[POST /github/endpoints][%d] CreateGithubEndpoint default %+v", o._statusCode, o.Payload) +} + +func (o *CreateGithubEndpointDefault) String() string { + return fmt.Sprintf("[POST /github/endpoints][%d] CreateGithubEndpoint default %+v", o._statusCode, o.Payload) +} + +func (o *CreateGithubEndpointDefault) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *CreateGithubEndpointDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/endpoints/delete_github_endpoint_parameters.go b/client/endpoints/delete_github_endpoint_parameters.go new file mode 100644 index 00000000..a02d4107 --- /dev/null +++ b/client/endpoints/delete_github_endpoint_parameters.go @@ -0,0 +1,151 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" +) + +// NewDeleteGithubEndpointParams creates a new DeleteGithubEndpointParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewDeleteGithubEndpointParams() *DeleteGithubEndpointParams { + return &DeleteGithubEndpointParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewDeleteGithubEndpointParamsWithTimeout creates a new DeleteGithubEndpointParams object +// with the ability to set a timeout on a request. +func NewDeleteGithubEndpointParamsWithTimeout(timeout time.Duration) *DeleteGithubEndpointParams { + return &DeleteGithubEndpointParams{ + timeout: timeout, + } +} + +// NewDeleteGithubEndpointParamsWithContext creates a new DeleteGithubEndpointParams object +// with the ability to set a context for a request. +func NewDeleteGithubEndpointParamsWithContext(ctx context.Context) *DeleteGithubEndpointParams { + return &DeleteGithubEndpointParams{ + Context: ctx, + } +} + +// NewDeleteGithubEndpointParamsWithHTTPClient creates a new DeleteGithubEndpointParams object +// with the ability to set a custom HTTPClient for a request. +func NewDeleteGithubEndpointParamsWithHTTPClient(client *http.Client) *DeleteGithubEndpointParams { + return &DeleteGithubEndpointParams{ + HTTPClient: client, + } +} + +/* +DeleteGithubEndpointParams contains all the parameters to send to the API endpoint + + for the delete github endpoint operation. + + Typically these are written to a http.Request. +*/ +type DeleteGithubEndpointParams struct { + + /* Name. + + The name of the GitHub endpoint. + */ + Name string + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the delete github endpoint params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *DeleteGithubEndpointParams) WithDefaults() *DeleteGithubEndpointParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the delete github endpoint params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *DeleteGithubEndpointParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the delete github endpoint params +func (o *DeleteGithubEndpointParams) WithTimeout(timeout time.Duration) *DeleteGithubEndpointParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the delete github endpoint params +func (o *DeleteGithubEndpointParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the delete github endpoint params +func (o *DeleteGithubEndpointParams) WithContext(ctx context.Context) *DeleteGithubEndpointParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the delete github endpoint params +func (o *DeleteGithubEndpointParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the delete github endpoint params +func (o *DeleteGithubEndpointParams) WithHTTPClient(client *http.Client) *DeleteGithubEndpointParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the delete github endpoint params +func (o *DeleteGithubEndpointParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithName adds the name to the delete github endpoint params +func (o *DeleteGithubEndpointParams) WithName(name string) *DeleteGithubEndpointParams { + o.SetName(name) + return o +} + +// SetName adds the name to the delete github endpoint params +func (o *DeleteGithubEndpointParams) SetName(name string) { + o.Name = name +} + +// WriteToRequest writes these params to a swagger request +func (o *DeleteGithubEndpointParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + // path param name + if err := r.SetPathParam("name", o.Name); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/endpoints/delete_github_endpoint_responses.go b/client/endpoints/delete_github_endpoint_responses.go new file mode 100644 index 00000000..a6982f1d --- /dev/null +++ b/client/endpoints/delete_github_endpoint_responses.go @@ -0,0 +1,103 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" +) + +// DeleteGithubEndpointReader is a Reader for the DeleteGithubEndpoint structure. +type DeleteGithubEndpointReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *DeleteGithubEndpointReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + result := NewDeleteGithubEndpointDefault(response.Code()) + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + if response.Code()/100 == 2 { + return result, nil + } + return nil, result +} + +// NewDeleteGithubEndpointDefault creates a DeleteGithubEndpointDefault with default headers values +func NewDeleteGithubEndpointDefault(code int) *DeleteGithubEndpointDefault { + return &DeleteGithubEndpointDefault{ + _statusCode: code, + } +} + +/* +DeleteGithubEndpointDefault describes a response with status code -1, with default header values. + +APIErrorResponse +*/ +type DeleteGithubEndpointDefault struct { + _statusCode int + + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this delete github endpoint default response has a 2xx status code +func (o *DeleteGithubEndpointDefault) IsSuccess() bool { + return o._statusCode/100 == 2 +} + +// IsRedirect returns true when this delete github endpoint default response has a 3xx status code +func (o *DeleteGithubEndpointDefault) IsRedirect() bool { + return o._statusCode/100 == 3 +} + +// IsClientError returns true when this delete github endpoint default response has a 4xx status code +func (o *DeleteGithubEndpointDefault) IsClientError() bool { + return o._statusCode/100 == 4 +} + +// IsServerError returns true when this delete github endpoint default response has a 5xx status code +func (o *DeleteGithubEndpointDefault) IsServerError() bool { + return o._statusCode/100 == 5 +} + +// IsCode returns true when this delete github endpoint default response a status code equal to that given +func (o *DeleteGithubEndpointDefault) IsCode(code int) bool { + return o._statusCode == code +} + +// Code gets the status code for the delete github endpoint default response +func (o *DeleteGithubEndpointDefault) Code() int { + return o._statusCode +} + +func (o *DeleteGithubEndpointDefault) Error() string { + return fmt.Sprintf("[DELETE /github/endpoints/{name}][%d] DeleteGithubEndpoint default %+v", o._statusCode, o.Payload) +} + +func (o *DeleteGithubEndpointDefault) String() string { + return fmt.Sprintf("[DELETE /github/endpoints/{name}][%d] DeleteGithubEndpoint default %+v", o._statusCode, o.Payload) +} + +func (o *DeleteGithubEndpointDefault) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *DeleteGithubEndpointDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/endpoints/endpoints_client.go b/client/endpoints/endpoints_client.go new file mode 100644 index 00000000..6b9300ed --- /dev/null +++ b/client/endpoints/endpoints_client.go @@ -0,0 +1,231 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" +) + +// New creates a new endpoints API client. +func New(transport runtime.ClientTransport, formats strfmt.Registry) ClientService { + return &Client{transport: transport, formats: formats} +} + +/* +Client for endpoints API +*/ +type Client struct { + transport runtime.ClientTransport + formats strfmt.Registry +} + +// ClientOption is the option for Client methods +type ClientOption func(*runtime.ClientOperation) + +// ClientService is the interface for Client methods +type ClientService interface { + CreateGithubEndpoint(params *CreateGithubEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*CreateGithubEndpointOK, error) + + DeleteGithubEndpoint(params *DeleteGithubEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) error + + GetGithubEndpoint(params *GetGithubEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetGithubEndpointOK, error) + + ListGithubEndpoints(params *ListGithubEndpointsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*ListGithubEndpointsOK, error) + + UpdateGithubEndpoint(params *UpdateGithubEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateGithubEndpointOK, error) + + SetTransport(transport runtime.ClientTransport) +} + +/* +CreateGithubEndpoint creates a git hub endpoint +*/ +func (a *Client) CreateGithubEndpoint(params *CreateGithubEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*CreateGithubEndpointOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewCreateGithubEndpointParams() + } + op := &runtime.ClientOperation{ + ID: "CreateGithubEndpoint", + Method: "POST", + PathPattern: "/github/endpoints", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &CreateGithubEndpointReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*CreateGithubEndpointOK) + if ok { + return success, nil + } + // unexpected success response + unexpectedSuccess := result.(*CreateGithubEndpointDefault) + return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) +} + +/* +DeleteGithubEndpoint deletes a git hub endpoint +*/ +func (a *Client) DeleteGithubEndpoint(params *DeleteGithubEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) error { + // TODO: Validate the params before sending + if params == nil { + params = NewDeleteGithubEndpointParams() + } + op := &runtime.ClientOperation{ + ID: "DeleteGithubEndpoint", + Method: "DELETE", + PathPattern: "/github/endpoints/{name}", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &DeleteGithubEndpointReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + _, err := a.transport.Submit(op) + if err != nil { + return err + } + return nil +} + +/* +GetGithubEndpoint gets a git hub endpoint +*/ +func (a *Client) GetGithubEndpoint(params *GetGithubEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetGithubEndpointOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewGetGithubEndpointParams() + } + op := &runtime.ClientOperation{ + ID: "GetGithubEndpoint", + Method: "GET", + PathPattern: "/github/endpoints/{name}", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &GetGithubEndpointReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*GetGithubEndpointOK) + if ok { + return success, nil + } + // unexpected success response + unexpectedSuccess := result.(*GetGithubEndpointDefault) + return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) +} + +/* +ListGithubEndpoints lists all git hub endpoints +*/ +func (a *Client) ListGithubEndpoints(params *ListGithubEndpointsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*ListGithubEndpointsOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewListGithubEndpointsParams() + } + op := &runtime.ClientOperation{ + ID: "ListGithubEndpoints", + Method: "GET", + PathPattern: "/github/endpoints", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &ListGithubEndpointsReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*ListGithubEndpointsOK) + if ok { + return success, nil + } + // unexpected success response + unexpectedSuccess := result.(*ListGithubEndpointsDefault) + return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) +} + +/* +UpdateGithubEndpoint updates a git hub endpoint +*/ +func (a *Client) UpdateGithubEndpoint(params *UpdateGithubEndpointParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateGithubEndpointOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewUpdateGithubEndpointParams() + } + op := &runtime.ClientOperation{ + ID: "UpdateGithubEndpoint", + Method: "PUT", + PathPattern: "/github/endpoints/{name}", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &UpdateGithubEndpointReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*UpdateGithubEndpointOK) + if ok { + return success, nil + } + // unexpected success response + unexpectedSuccess := result.(*UpdateGithubEndpointDefault) + return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) +} + +// SetTransport changes the transport on the client +func (a *Client) SetTransport(transport runtime.ClientTransport) { + a.transport = transport +} diff --git a/client/endpoints/get_github_endpoint_parameters.go b/client/endpoints/get_github_endpoint_parameters.go new file mode 100644 index 00000000..7bd9ca00 --- /dev/null +++ b/client/endpoints/get_github_endpoint_parameters.go @@ -0,0 +1,151 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" +) + +// NewGetGithubEndpointParams creates a new GetGithubEndpointParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewGetGithubEndpointParams() *GetGithubEndpointParams { + return &GetGithubEndpointParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewGetGithubEndpointParamsWithTimeout creates a new GetGithubEndpointParams object +// with the ability to set a timeout on a request. +func NewGetGithubEndpointParamsWithTimeout(timeout time.Duration) *GetGithubEndpointParams { + return &GetGithubEndpointParams{ + timeout: timeout, + } +} + +// NewGetGithubEndpointParamsWithContext creates a new GetGithubEndpointParams object +// with the ability to set a context for a request. +func NewGetGithubEndpointParamsWithContext(ctx context.Context) *GetGithubEndpointParams { + return &GetGithubEndpointParams{ + Context: ctx, + } +} + +// NewGetGithubEndpointParamsWithHTTPClient creates a new GetGithubEndpointParams object +// with the ability to set a custom HTTPClient for a request. +func NewGetGithubEndpointParamsWithHTTPClient(client *http.Client) *GetGithubEndpointParams { + return &GetGithubEndpointParams{ + HTTPClient: client, + } +} + +/* +GetGithubEndpointParams contains all the parameters to send to the API endpoint + + for the get github endpoint operation. + + Typically these are written to a http.Request. +*/ +type GetGithubEndpointParams struct { + + /* Name. + + The name of the GitHub endpoint. + */ + Name string + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the get github endpoint params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *GetGithubEndpointParams) WithDefaults() *GetGithubEndpointParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the get github endpoint params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *GetGithubEndpointParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the get github endpoint params +func (o *GetGithubEndpointParams) WithTimeout(timeout time.Duration) *GetGithubEndpointParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the get github endpoint params +func (o *GetGithubEndpointParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the get github endpoint params +func (o *GetGithubEndpointParams) WithContext(ctx context.Context) *GetGithubEndpointParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the get github endpoint params +func (o *GetGithubEndpointParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the get github endpoint params +func (o *GetGithubEndpointParams) WithHTTPClient(client *http.Client) *GetGithubEndpointParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the get github endpoint params +func (o *GetGithubEndpointParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithName adds the name to the get github endpoint params +func (o *GetGithubEndpointParams) WithName(name string) *GetGithubEndpointParams { + o.SetName(name) + return o +} + +// SetName adds the name to the get github endpoint params +func (o *GetGithubEndpointParams) SetName(name string) { + o.Name = name +} + +// WriteToRequest writes these params to a swagger request +func (o *GetGithubEndpointParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + // path param name + if err := r.SetPathParam("name", o.Name); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/endpoints/get_github_endpoint_responses.go b/client/endpoints/get_github_endpoint_responses.go new file mode 100644 index 00000000..7f35c89f --- /dev/null +++ b/client/endpoints/get_github_endpoint_responses.go @@ -0,0 +1,179 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" + garm_params "github.com/cloudbase/garm/params" +) + +// GetGithubEndpointReader is a Reader for the GetGithubEndpoint structure. +type GetGithubEndpointReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *GetGithubEndpointReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewGetGithubEndpointOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + default: + result := NewGetGithubEndpointDefault(response.Code()) + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + if response.Code()/100 == 2 { + return result, nil + } + return nil, result + } +} + +// NewGetGithubEndpointOK creates a GetGithubEndpointOK with default headers values +func NewGetGithubEndpointOK() *GetGithubEndpointOK { + return &GetGithubEndpointOK{} +} + +/* +GetGithubEndpointOK describes a response with status code 200, with default header values. + +GithubEndpoint +*/ +type GetGithubEndpointOK struct { + Payload garm_params.GithubEndpoint +} + +// IsSuccess returns true when this get github endpoint o k response has a 2xx status code +func (o *GetGithubEndpointOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this get github endpoint o k response has a 3xx status code +func (o *GetGithubEndpointOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this get github endpoint o k response has a 4xx status code +func (o *GetGithubEndpointOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this get github endpoint o k response has a 5xx status code +func (o *GetGithubEndpointOK) IsServerError() bool { + return false +} + +// IsCode returns true when this get github endpoint o k response a status code equal to that given +func (o *GetGithubEndpointOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the get github endpoint o k response +func (o *GetGithubEndpointOK) Code() int { + return 200 +} + +func (o *GetGithubEndpointOK) Error() string { + return fmt.Sprintf("[GET /github/endpoints/{name}][%d] getGithubEndpointOK %+v", 200, o.Payload) +} + +func (o *GetGithubEndpointOK) String() string { + return fmt.Sprintf("[GET /github/endpoints/{name}][%d] getGithubEndpointOK %+v", 200, o.Payload) +} + +func (o *GetGithubEndpointOK) GetPayload() garm_params.GithubEndpoint { + return o.Payload +} + +func (o *GetGithubEndpointOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewGetGithubEndpointDefault creates a GetGithubEndpointDefault with default headers values +func NewGetGithubEndpointDefault(code int) *GetGithubEndpointDefault { + return &GetGithubEndpointDefault{ + _statusCode: code, + } +} + +/* +GetGithubEndpointDefault describes a response with status code -1, with default header values. + +APIErrorResponse +*/ +type GetGithubEndpointDefault struct { + _statusCode int + + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this get github endpoint default response has a 2xx status code +func (o *GetGithubEndpointDefault) IsSuccess() bool { + return o._statusCode/100 == 2 +} + +// IsRedirect returns true when this get github endpoint default response has a 3xx status code +func (o *GetGithubEndpointDefault) IsRedirect() bool { + return o._statusCode/100 == 3 +} + +// IsClientError returns true when this get github endpoint default response has a 4xx status code +func (o *GetGithubEndpointDefault) IsClientError() bool { + return o._statusCode/100 == 4 +} + +// IsServerError returns true when this get github endpoint default response has a 5xx status code +func (o *GetGithubEndpointDefault) IsServerError() bool { + return o._statusCode/100 == 5 +} + +// IsCode returns true when this get github endpoint default response a status code equal to that given +func (o *GetGithubEndpointDefault) IsCode(code int) bool { + return o._statusCode == code +} + +// Code gets the status code for the get github endpoint default response +func (o *GetGithubEndpointDefault) Code() int { + return o._statusCode +} + +func (o *GetGithubEndpointDefault) Error() string { + return fmt.Sprintf("[GET /github/endpoints/{name}][%d] GetGithubEndpoint default %+v", o._statusCode, o.Payload) +} + +func (o *GetGithubEndpointDefault) String() string { + return fmt.Sprintf("[GET /github/endpoints/{name}][%d] GetGithubEndpoint default %+v", o._statusCode, o.Payload) +} + +func (o *GetGithubEndpointDefault) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *GetGithubEndpointDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/endpoints/list_github_endpoints_parameters.go b/client/endpoints/list_github_endpoints_parameters.go new file mode 100644 index 00000000..c002cfe4 --- /dev/null +++ b/client/endpoints/list_github_endpoints_parameters.go @@ -0,0 +1,128 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" +) + +// NewListGithubEndpointsParams creates a new ListGithubEndpointsParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewListGithubEndpointsParams() *ListGithubEndpointsParams { + return &ListGithubEndpointsParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewListGithubEndpointsParamsWithTimeout creates a new ListGithubEndpointsParams object +// with the ability to set a timeout on a request. +func NewListGithubEndpointsParamsWithTimeout(timeout time.Duration) *ListGithubEndpointsParams { + return &ListGithubEndpointsParams{ + timeout: timeout, + } +} + +// NewListGithubEndpointsParamsWithContext creates a new ListGithubEndpointsParams object +// with the ability to set a context for a request. +func NewListGithubEndpointsParamsWithContext(ctx context.Context) *ListGithubEndpointsParams { + return &ListGithubEndpointsParams{ + Context: ctx, + } +} + +// NewListGithubEndpointsParamsWithHTTPClient creates a new ListGithubEndpointsParams object +// with the ability to set a custom HTTPClient for a request. +func NewListGithubEndpointsParamsWithHTTPClient(client *http.Client) *ListGithubEndpointsParams { + return &ListGithubEndpointsParams{ + HTTPClient: client, + } +} + +/* +ListGithubEndpointsParams contains all the parameters to send to the API endpoint + + for the list github endpoints operation. + + Typically these are written to a http.Request. +*/ +type ListGithubEndpointsParams struct { + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the list github endpoints params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *ListGithubEndpointsParams) WithDefaults() *ListGithubEndpointsParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the list github endpoints params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *ListGithubEndpointsParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the list github endpoints params +func (o *ListGithubEndpointsParams) WithTimeout(timeout time.Duration) *ListGithubEndpointsParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the list github endpoints params +func (o *ListGithubEndpointsParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the list github endpoints params +func (o *ListGithubEndpointsParams) WithContext(ctx context.Context) *ListGithubEndpointsParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the list github endpoints params +func (o *ListGithubEndpointsParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the list github endpoints params +func (o *ListGithubEndpointsParams) WithHTTPClient(client *http.Client) *ListGithubEndpointsParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the list github endpoints params +func (o *ListGithubEndpointsParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WriteToRequest writes these params to a swagger request +func (o *ListGithubEndpointsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/endpoints/list_github_endpoints_responses.go b/client/endpoints/list_github_endpoints_responses.go new file mode 100644 index 00000000..78162e8b --- /dev/null +++ b/client/endpoints/list_github_endpoints_responses.go @@ -0,0 +1,179 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" + garm_params "github.com/cloudbase/garm/params" +) + +// ListGithubEndpointsReader is a Reader for the ListGithubEndpoints structure. +type ListGithubEndpointsReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *ListGithubEndpointsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewListGithubEndpointsOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + default: + result := NewListGithubEndpointsDefault(response.Code()) + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + if response.Code()/100 == 2 { + return result, nil + } + return nil, result + } +} + +// NewListGithubEndpointsOK creates a ListGithubEndpointsOK with default headers values +func NewListGithubEndpointsOK() *ListGithubEndpointsOK { + return &ListGithubEndpointsOK{} +} + +/* +ListGithubEndpointsOK describes a response with status code 200, with default header values. + +GithubEndpoints +*/ +type ListGithubEndpointsOK struct { + Payload garm_params.GithubEndpoints +} + +// IsSuccess returns true when this list github endpoints o k response has a 2xx status code +func (o *ListGithubEndpointsOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this list github endpoints o k response has a 3xx status code +func (o *ListGithubEndpointsOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this list github endpoints o k response has a 4xx status code +func (o *ListGithubEndpointsOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this list github endpoints o k response has a 5xx status code +func (o *ListGithubEndpointsOK) IsServerError() bool { + return false +} + +// IsCode returns true when this list github endpoints o k response a status code equal to that given +func (o *ListGithubEndpointsOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the list github endpoints o k response +func (o *ListGithubEndpointsOK) Code() int { + return 200 +} + +func (o *ListGithubEndpointsOK) Error() string { + return fmt.Sprintf("[GET /github/endpoints][%d] listGithubEndpointsOK %+v", 200, o.Payload) +} + +func (o *ListGithubEndpointsOK) String() string { + return fmt.Sprintf("[GET /github/endpoints][%d] listGithubEndpointsOK %+v", 200, o.Payload) +} + +func (o *ListGithubEndpointsOK) GetPayload() garm_params.GithubEndpoints { + return o.Payload +} + +func (o *ListGithubEndpointsOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewListGithubEndpointsDefault creates a ListGithubEndpointsDefault with default headers values +func NewListGithubEndpointsDefault(code int) *ListGithubEndpointsDefault { + return &ListGithubEndpointsDefault{ + _statusCode: code, + } +} + +/* +ListGithubEndpointsDefault describes a response with status code -1, with default header values. + +APIErrorResponse +*/ +type ListGithubEndpointsDefault struct { + _statusCode int + + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this list github endpoints default response has a 2xx status code +func (o *ListGithubEndpointsDefault) IsSuccess() bool { + return o._statusCode/100 == 2 +} + +// IsRedirect returns true when this list github endpoints default response has a 3xx status code +func (o *ListGithubEndpointsDefault) IsRedirect() bool { + return o._statusCode/100 == 3 +} + +// IsClientError returns true when this list github endpoints default response has a 4xx status code +func (o *ListGithubEndpointsDefault) IsClientError() bool { + return o._statusCode/100 == 4 +} + +// IsServerError returns true when this list github endpoints default response has a 5xx status code +func (o *ListGithubEndpointsDefault) IsServerError() bool { + return o._statusCode/100 == 5 +} + +// IsCode returns true when this list github endpoints default response a status code equal to that given +func (o *ListGithubEndpointsDefault) IsCode(code int) bool { + return o._statusCode == code +} + +// Code gets the status code for the list github endpoints default response +func (o *ListGithubEndpointsDefault) Code() int { + return o._statusCode +} + +func (o *ListGithubEndpointsDefault) Error() string { + return fmt.Sprintf("[GET /github/endpoints][%d] ListGithubEndpoints default %+v", o._statusCode, o.Payload) +} + +func (o *ListGithubEndpointsDefault) String() string { + return fmt.Sprintf("[GET /github/endpoints][%d] ListGithubEndpoints default %+v", o._statusCode, o.Payload) +} + +func (o *ListGithubEndpointsDefault) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *ListGithubEndpointsDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/endpoints/update_github_endpoint_parameters.go b/client/endpoints/update_github_endpoint_parameters.go new file mode 100644 index 00000000..35ee713a --- /dev/null +++ b/client/endpoints/update_github_endpoint_parameters.go @@ -0,0 +1,173 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + + garm_params "github.com/cloudbase/garm/params" +) + +// NewUpdateGithubEndpointParams creates a new UpdateGithubEndpointParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewUpdateGithubEndpointParams() *UpdateGithubEndpointParams { + return &UpdateGithubEndpointParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewUpdateGithubEndpointParamsWithTimeout creates a new UpdateGithubEndpointParams object +// with the ability to set a timeout on a request. +func NewUpdateGithubEndpointParamsWithTimeout(timeout time.Duration) *UpdateGithubEndpointParams { + return &UpdateGithubEndpointParams{ + timeout: timeout, + } +} + +// NewUpdateGithubEndpointParamsWithContext creates a new UpdateGithubEndpointParams object +// with the ability to set a context for a request. +func NewUpdateGithubEndpointParamsWithContext(ctx context.Context) *UpdateGithubEndpointParams { + return &UpdateGithubEndpointParams{ + Context: ctx, + } +} + +// NewUpdateGithubEndpointParamsWithHTTPClient creates a new UpdateGithubEndpointParams object +// with the ability to set a custom HTTPClient for a request. +func NewUpdateGithubEndpointParamsWithHTTPClient(client *http.Client) *UpdateGithubEndpointParams { + return &UpdateGithubEndpointParams{ + HTTPClient: client, + } +} + +/* +UpdateGithubEndpointParams contains all the parameters to send to the API endpoint + + for the update github endpoint operation. + + Typically these are written to a http.Request. +*/ +type UpdateGithubEndpointParams struct { + + /* Body. + + Parameters used when updating a GitHub endpoint. + */ + Body garm_params.UpdateGithubEndpointParams + + /* Name. + + The name of the GitHub endpoint. + */ + Name string + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the update github endpoint params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *UpdateGithubEndpointParams) WithDefaults() *UpdateGithubEndpointParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the update github endpoint params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *UpdateGithubEndpointParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the update github endpoint params +func (o *UpdateGithubEndpointParams) WithTimeout(timeout time.Duration) *UpdateGithubEndpointParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the update github endpoint params +func (o *UpdateGithubEndpointParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the update github endpoint params +func (o *UpdateGithubEndpointParams) WithContext(ctx context.Context) *UpdateGithubEndpointParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the update github endpoint params +func (o *UpdateGithubEndpointParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the update github endpoint params +func (o *UpdateGithubEndpointParams) WithHTTPClient(client *http.Client) *UpdateGithubEndpointParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the update github endpoint params +func (o *UpdateGithubEndpointParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithBody adds the body to the update github endpoint params +func (o *UpdateGithubEndpointParams) WithBody(body garm_params.UpdateGithubEndpointParams) *UpdateGithubEndpointParams { + o.SetBody(body) + return o +} + +// SetBody adds the body to the update github endpoint params +func (o *UpdateGithubEndpointParams) SetBody(body garm_params.UpdateGithubEndpointParams) { + o.Body = body +} + +// WithName adds the name to the update github endpoint params +func (o *UpdateGithubEndpointParams) WithName(name string) *UpdateGithubEndpointParams { + o.SetName(name) + return o +} + +// SetName adds the name to the update github endpoint params +func (o *UpdateGithubEndpointParams) SetName(name string) { + o.Name = name +} + +// WriteToRequest writes these params to a swagger request +func (o *UpdateGithubEndpointParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + if err := r.SetBodyParam(o.Body); err != nil { + return err + } + + // path param name + if err := r.SetPathParam("name", o.Name); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/endpoints/update_github_endpoint_responses.go b/client/endpoints/update_github_endpoint_responses.go new file mode 100644 index 00000000..33e3e069 --- /dev/null +++ b/client/endpoints/update_github_endpoint_responses.go @@ -0,0 +1,179 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package endpoints + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" + garm_params "github.com/cloudbase/garm/params" +) + +// UpdateGithubEndpointReader is a Reader for the UpdateGithubEndpoint structure. +type UpdateGithubEndpointReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *UpdateGithubEndpointReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewUpdateGithubEndpointOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + default: + result := NewUpdateGithubEndpointDefault(response.Code()) + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + if response.Code()/100 == 2 { + return result, nil + } + return nil, result + } +} + +// NewUpdateGithubEndpointOK creates a UpdateGithubEndpointOK with default headers values +func NewUpdateGithubEndpointOK() *UpdateGithubEndpointOK { + return &UpdateGithubEndpointOK{} +} + +/* +UpdateGithubEndpointOK describes a response with status code 200, with default header values. + +GithubEndpoint +*/ +type UpdateGithubEndpointOK struct { + Payload garm_params.GithubEndpoint +} + +// IsSuccess returns true when this update github endpoint o k response has a 2xx status code +func (o *UpdateGithubEndpointOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this update github endpoint o k response has a 3xx status code +func (o *UpdateGithubEndpointOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this update github endpoint o k response has a 4xx status code +func (o *UpdateGithubEndpointOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this update github endpoint o k response has a 5xx status code +func (o *UpdateGithubEndpointOK) IsServerError() bool { + return false +} + +// IsCode returns true when this update github endpoint o k response a status code equal to that given +func (o *UpdateGithubEndpointOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the update github endpoint o k response +func (o *UpdateGithubEndpointOK) Code() int { + return 200 +} + +func (o *UpdateGithubEndpointOK) Error() string { + return fmt.Sprintf("[PUT /github/endpoints/{name}][%d] updateGithubEndpointOK %+v", 200, o.Payload) +} + +func (o *UpdateGithubEndpointOK) String() string { + return fmt.Sprintf("[PUT /github/endpoints/{name}][%d] updateGithubEndpointOK %+v", 200, o.Payload) +} + +func (o *UpdateGithubEndpointOK) GetPayload() garm_params.GithubEndpoint { + return o.Payload +} + +func (o *UpdateGithubEndpointOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewUpdateGithubEndpointDefault creates a UpdateGithubEndpointDefault with default headers values +func NewUpdateGithubEndpointDefault(code int) *UpdateGithubEndpointDefault { + return &UpdateGithubEndpointDefault{ + _statusCode: code, + } +} + +/* +UpdateGithubEndpointDefault describes a response with status code -1, with default header values. + +APIErrorResponse +*/ +type UpdateGithubEndpointDefault struct { + _statusCode int + + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this update github endpoint default response has a 2xx status code +func (o *UpdateGithubEndpointDefault) IsSuccess() bool { + return o._statusCode/100 == 2 +} + +// IsRedirect returns true when this update github endpoint default response has a 3xx status code +func (o *UpdateGithubEndpointDefault) IsRedirect() bool { + return o._statusCode/100 == 3 +} + +// IsClientError returns true when this update github endpoint default response has a 4xx status code +func (o *UpdateGithubEndpointDefault) IsClientError() bool { + return o._statusCode/100 == 4 +} + +// IsServerError returns true when this update github endpoint default response has a 5xx status code +func (o *UpdateGithubEndpointDefault) IsServerError() bool { + return o._statusCode/100 == 5 +} + +// IsCode returns true when this update github endpoint default response a status code equal to that given +func (o *UpdateGithubEndpointDefault) IsCode(code int) bool { + return o._statusCode == code +} + +// Code gets the status code for the update github endpoint default response +func (o *UpdateGithubEndpointDefault) Code() int { + return o._statusCode +} + +func (o *UpdateGithubEndpointDefault) Error() string { + return fmt.Sprintf("[PUT /github/endpoints/{name}][%d] UpdateGithubEndpoint default %+v", o._statusCode, o.Payload) +} + +func (o *UpdateGithubEndpointDefault) String() string { + return fmt.Sprintf("[PUT /github/endpoints/{name}][%d] UpdateGithubEndpoint default %+v", o._statusCode, o.Payload) +} + +func (o *UpdateGithubEndpointDefault) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *UpdateGithubEndpointDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/garm_api_client.go b/client/garm_api_client.go index 143953b4..597eab26 100644 --- a/client/garm_api_client.go +++ b/client/garm_api_client.go @@ -12,6 +12,7 @@ import ( "github.com/cloudbase/garm/client/controller_info" "github.com/cloudbase/garm/client/credentials" + "github.com/cloudbase/garm/client/endpoints" "github.com/cloudbase/garm/client/enterprises" "github.com/cloudbase/garm/client/first_run" "github.com/cloudbase/garm/client/instances" @@ -68,6 +69,7 @@ func New(transport runtime.ClientTransport, formats strfmt.Registry) *GarmAPI { cli.Transport = transport cli.ControllerInfo = controller_info.New(transport, formats) cli.Credentials = credentials.New(transport, formats) + cli.Endpoints = endpoints.New(transport, formats) cli.Enterprises = enterprises.New(transport, formats) cli.FirstRun = first_run.New(transport, formats) cli.Instances = instances.New(transport, formats) @@ -126,6 +128,8 @@ type GarmAPI struct { Credentials credentials.ClientService + Endpoints endpoints.ClientService + Enterprises enterprises.ClientService FirstRun first_run.ClientService @@ -154,6 +158,7 @@ func (c *GarmAPI) SetTransport(transport runtime.ClientTransport) { c.Transport = transport c.ControllerInfo.SetTransport(transport) c.Credentials.SetTransport(transport) + c.Endpoints.SetTransport(transport) c.Enterprises.SetTransport(transport) c.FirstRun.SetTransport(transport) c.Instances.SetTransport(transport) diff --git a/cmd/garm-cli/cmd/github.go b/cmd/garm-cli/cmd/github.go new file mode 100644 index 00000000..8b79a381 --- /dev/null +++ b/cmd/garm-cli/cmd/github.go @@ -0,0 +1,30 @@ +package cmd + +import "github.com/spf13/cobra" + +var ( + endpointName string + endpointBaseURL string + endpointUploadURL string + endpointAPIBaseURL string + endpointCACertPath string + endpointDescription string +) + +// githubCmd represents the the github command. This command has a set +// of subcommands that allow configuring and managing GitHub endpoints +// and credentials. +var githubCmd = &cobra.Command{ + Use: "github", + Aliases: []string{"gh"}, + SilenceUsage: true, + Short: "Manage GitHub resources", + Long: `Manage GitHub related resources. + +This command allows you to configure and manage GitHub endpoints and credentials`, + Run: nil, +} + +func init() { + rootCmd.AddCommand(githubCmd) +} diff --git a/cmd/garm-cli/cmd/github_endpoints.go b/cmd/garm-cli/cmd/github_endpoints.go new file mode 100644 index 00000000..a841d3dc --- /dev/null +++ b/cmd/garm-cli/cmd/github_endpoints.go @@ -0,0 +1,277 @@ +package cmd + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "os" + + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" + + apiClientEndpoints "github.com/cloudbase/garm/client/endpoints" + "github.com/cloudbase/garm/params" +) + +var githubEndpointCmd = &cobra.Command{ + Use: "endpoint", + SilenceUsage: true, + Short: "Manage GitHub endpoints", + Long: `Manage GitHub endpoints. + +This command allows you to configure and manage GitHub endpoints`, + Run: nil, +} + +var githubEndpointListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + SilenceUsage: true, + Short: "List GitHub endpoints", + Long: `List all configured GitHub endpoints.`, + RunE: func(_ *cobra.Command, _ []string) error { + if needsInit { + return errNeedsInitError + } + + newGHListReq := apiClientEndpoints.NewListGithubEndpointsParams() + response, err := apiCli.Endpoints.ListGithubEndpoints(newGHListReq, authToken) + if err != nil { + return err + } + formatEndpoints(response.Payload) + return nil + }, +} + +var githubEndpointShowCmd = &cobra.Command{ + Use: "show", + Aliases: []string{"get"}, + SilenceUsage: true, + Short: "Show GitHub endpoint", + Long: `Show details of a GitHub endpoint.`, + RunE: func(_ *cobra.Command, args []string) error { + if needsInit { + return errNeedsInitError + } + if len(args) == 0 { + return fmt.Errorf("requires an endpoint name") + } + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + + newGHShowReq := apiClientEndpoints.NewGetGithubEndpointParams() + newGHShowReq.Name = args[0] + response, err := apiCli.Endpoints.GetGithubEndpoint(newGHShowReq, authToken) + if err != nil { + return err + } + formatOneEndpoint(response.Payload) + return nil + }, +} + +var githubEndpointCreateCmd = &cobra.Command{ + Use: "create", + SilenceUsage: true, + Short: "Create GitHub endpoint", + Long: `Create a new GitHub endpoint.`, + RunE: func(_ *cobra.Command, _ []string) error { + if needsInit { + return errNeedsInitError + } + + createParams, err := parseCreateParams() + if err != nil { + return err + } + + newGHCreateReq := apiClientEndpoints.NewCreateGithubEndpointParams() + newGHCreateReq.Body = createParams + + response, err := apiCli.Endpoints.CreateGithubEndpoint(newGHCreateReq, authToken) + if err != nil { + return err + } + formatOneEndpoint(response.Payload) + return nil + }, +} + +var githubEndpointDeleteCmd = &cobra.Command{ + Use: "delete", + Aliases: []string{"remove", "rm"}, + SilenceUsage: true, + Short: "Delete GitHub endpoint", + Long: "Delete a GitHub endpoint", + RunE: func(_ *cobra.Command, args []string) error { + if needsInit { + return errNeedsInitError + } + if len(args) == 0 { + return fmt.Errorf("requires an endpoint name") + } + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + + newGHDeleteReq := apiClientEndpoints.NewDeleteGithubEndpointParams() + newGHDeleteReq.Name = args[0] + if err := apiCli.Endpoints.DeleteGithubEndpoint(newGHDeleteReq, authToken); err != nil { + return err + } + return nil + }, +} + +var githubEndpointUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update GitHub endpoint", + Long: "Update a GitHub endpoint", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return errNeedsInitError + } + if len(args) == 0 { + return fmt.Errorf("requires an endpoint name") + } + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + + updateParams := params.UpdateGithubEndpointParams{} + + if cmd.Flags().Changed("ca-cert-path") { + cert, err := parseReadAndParsCABundle() + if err != nil { + return err + } + updateParams.CACertBundle = cert + } + + if cmd.Flags().Changed("description") { + updateParams.Description = &endpointDescription + } + + if cmd.Flags().Changed("base-url") { + updateParams.BaseURL = &endpointBaseURL + } + + if cmd.Flags().Changed("upload-url") { + updateParams.UploadBaseURL = &endpointUploadURL + } + + if cmd.Flags().Changed("api-base-url") { + updateParams.APIBaseURL = &endpointAPIBaseURL + } + + newGHEndpointUpdateReq := apiClientEndpoints.NewUpdateGithubEndpointParams() + newGHEndpointUpdateReq.Name = args[0] + newGHEndpointUpdateReq.Body = updateParams + + response, err := apiCli.Endpoints.UpdateGithubEndpoint(newGHEndpointUpdateReq, authToken) + if err != nil { + return err + } + formatOneEndpoint(response.Payload) + return nil + }, +} + +func init() { + githubEndpointCreateCmd.Flags().StringVar(&endpointName, "name", "", "Name of the GitHub endpoint") + githubEndpointCreateCmd.Flags().StringVar(&endpointDescription, "description", "", "Description for the github endpoint") + githubEndpointCreateCmd.Flags().StringVar(&endpointBaseURL, "base-url", "", "Base URL of the GitHub endpoint") + githubEndpointCreateCmd.Flags().StringVar(&endpointUploadURL, "upload-url", "", "Upload URL of the GitHub endpoint") + githubEndpointCreateCmd.Flags().StringVar(&endpointAPIBaseURL, "api-base-url", "", "API Base URL of the GitHub endpoint") + githubEndpointCreateCmd.Flags().StringVar(&endpointCACertPath, "ca-cert-path", "", "CA Cert Path of the GitHub endpoint") + + githubEndpointCreateCmd.MarkFlagRequired("name") + githubEndpointCreateCmd.MarkFlagRequired("base-url") + githubEndpointCreateCmd.MarkFlagRequired("api-base-url") + githubEndpointCreateCmd.MarkFlagRequired("upload-url") + + githubEndpointUpdateCmd.Flags().StringVar(&endpointDescription, "description", "", "Description for the github endpoint") + githubEndpointUpdateCmd.Flags().StringVar(&endpointBaseURL, "base-url", "", "Base URL of the GitHub endpoint") + githubEndpointUpdateCmd.Flags().StringVar(&endpointUploadURL, "upload-url", "", "Upload URL of the GitHub endpoint") + githubEndpointUpdateCmd.Flags().StringVar(&endpointAPIBaseURL, "api-base-url", "", "API Base URL of the GitHub endpoint") + githubEndpointUpdateCmd.Flags().StringVar(&endpointCACertPath, "ca-cert-path", "", "CA Cert Path of the GitHub endpoint") + + githubEndpointCmd.AddCommand(githubEndpointListCmd) + githubEndpointCmd.AddCommand(githubEndpointShowCmd) + githubEndpointCmd.AddCommand(githubEndpointCreateCmd) + githubEndpointCmd.AddCommand(githubEndpointDeleteCmd) + githubEndpointCmd.AddCommand(githubEndpointUpdateCmd) + + githubCmd.AddCommand(githubEndpointCmd) +} + +func parseReadAndParsCABundle() ([]byte, error) { + if endpointCACertPath == "" { + return nil, nil + } + + if _, err := os.Stat(endpointCACertPath); os.IsNotExist(err) { + return nil, fmt.Errorf("CA cert file not found: %s", endpointCACertPath) + } + contents, err := os.ReadFile(endpointCACertPath) + if err != nil { + return nil, err + } + pemBlock, _ := pem.Decode(contents) + if pemBlock == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + if _, err := x509.ParseCertificates(pemBlock.Bytes); err != nil { + return nil, fmt.Errorf("failed to parse CA cert bundle: %w", err) + } + return contents, nil +} + +func parseCreateParams() (params.CreateGithubEndpointParams, error) { + certBundleBytes, err := parseReadAndParsCABundle() + if err != nil { + return params.CreateGithubEndpointParams{}, err + } + + ret := params.CreateGithubEndpointParams{ + Name: endpointName, + BaseURL: endpointBaseURL, + UploadBaseURL: endpointUploadURL, + APIBaseURL: endpointAPIBaseURL, + Description: endpointDescription, + CACertBundle: certBundleBytes, + } + return ret, nil +} + +func formatEndpoints(endpoints params.GithubEndpoints) { + t := table.NewWriter() + header := table.Row{"Name", "Base URL", "Description"} + t.AppendHeader(header) + for _, val := range endpoints { + t.AppendRow([]interface{}{val.Name, val.BaseURL, val.Description}) + t.AppendSeparator() + } + fmt.Println(t.Render()) +} + +func formatOneEndpoint(endpoint params.GithubEndpoint) { + t := table.NewWriter() + header := table.Row{"Field", "Value"} + t.AppendHeader(header) + t.AppendRow([]interface{}{"Name", endpoint.Name}) + t.AppendRow([]interface{}{"Base URL", endpoint.BaseURL}) + t.AppendRow([]interface{}{"Upload URL", endpoint.UploadBaseURL}) + t.AppendRow([]interface{}{"API Base URL", endpoint.APIBaseURL}) + if len(endpoint.CACertBundle) > 0 { + t.AppendRow([]interface{}{"CA Cert Bundle", string(endpoint.CACertBundle)}) + } + t.SetColumnConfigs([]table.ColumnConfig{ + {Number: 1, AutoMerge: true}, + {Number: 2, AutoMerge: false, WidthMax: 100}, + }) + fmt.Println(t.Render()) +} diff --git a/database/sql/enterprise_test.go b/database/sql/enterprise_test.go index b442ec86..d49b25bd 100644 --- a/database/sql/enterprise_test.go +++ b/database/sql/enterprise_test.go @@ -218,11 +218,11 @@ func (s *EnterpriseTestSuite) TestCreateEnterpriseDBCreateErr() { s.Fixtures.SQLMock. ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). WithArgs(s.Fixtures.Enterprises[0].CredentialsName). - WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}).AddRow(s.testCreds.ID, s.testCreds.Endpoint)) + WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}).AddRow(s.testCreds.ID, s.testCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). - WithArgs(s.testCreds.Endpoint). + WithArgs(s.testCreds.Endpoint.Name). WillReturnRows(sqlmock.NewRows([]string{"name"}). - AddRow(s.testCreds.Endpoint)) + AddRow(s.testCreds.Endpoint.Name)) s.Fixtures.SQLMock. ExpectExec(regexp.QuoteMeta("INSERT INTO `enterprises`")). WillReturnError(fmt.Errorf("creating enterprise mock error")) @@ -356,11 +356,11 @@ func (s *EnterpriseTestSuite) TestUpdateEnterpriseDBEncryptErr() { ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). WithArgs(s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). - AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). - WithArgs(s.testCreds.Endpoint). + WithArgs(s.testCreds.Endpoint.Name). WillReturnRows(sqlmock.NewRows([]string{"name"}). - AddRow(s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectRollback() _, err := s.StoreSQLMocked.UpdateEnterprise(s.adminCtx, s.Fixtures.Enterprises[0].ID, s.Fixtures.UpdateRepoParams) @@ -381,11 +381,11 @@ func (s *EnterpriseTestSuite) TestUpdateEnterpriseDBSaveErr() { ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). WithArgs(s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). - AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). - WithArgs(s.testCreds.Endpoint). + WithArgs(s.testCreds.Endpoint.Name). WillReturnRows(sqlmock.NewRows([]string{"name"}). - AddRow(s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock. ExpectExec(("UPDATE `enterprises` SET")). WillReturnError(fmt.Errorf("saving enterprise mock error")) @@ -412,11 +412,11 @@ func (s *EnterpriseTestSuite) TestUpdateEnterpriseDBDecryptingErr() { ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). WithArgs(s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). - AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). - WithArgs(s.testCreds.Endpoint). + WithArgs(s.testCreds.Endpoint.Name). WillReturnRows(sqlmock.NewRows([]string{"name"}). - AddRow(s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectRollback() _, err := s.StoreSQLMocked.UpdateEnterprise(s.adminCtx, s.Fixtures.Enterprises[0].ID, s.Fixtures.UpdateRepoParams) diff --git a/database/sql/github.go b/database/sql/github.go index 08e22f62..8a3bb50c 100644 --- a/database/sql/github.go +++ b/database/sql/github.go @@ -36,6 +36,11 @@ func (s *sqlDatabase) sqlToCommonGithubCredentials(creds GithubCredentials) (par return params.GithubCredentials{}, errors.Wrap(err, "unsealing credentials") } + ep, err := s.sqlToCommonGithubEndpoint(creds.Endpoint) + if err != nil { + return params.GithubCredentials{}, errors.Wrap(err, "converting github endpoint") + } + commonCreds := params.GithubCredentials{ ID: creds.ID, Name: creds.Name, @@ -45,7 +50,7 @@ func (s *sqlDatabase) sqlToCommonGithubCredentials(creds GithubCredentials) (par UploadBaseURL: creds.Endpoint.UploadBaseURL, CABundle: creds.Endpoint.CACertBundle, AuthType: creds.AuthType, - Endpoint: creds.Endpoint.Name, + Endpoint: ep, CredentialsPayload: data, } @@ -216,8 +221,29 @@ func (s *sqlDatabase) DeleteGithubEndpoint(_ context.Context, name string) error } } - if credsCount > 0 { - return errors.New("cannot delete endpoint with credentials") + var repoCnt int64 + if err := tx.Model(&Repository{}).Where("endpoint_name = ?", endpoint.Name).Count(&repoCnt).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrap(err, "fetching github repositories") + } + } + + var orgCnt int64 + if err := tx.Model(&Organization{}).Where("endpoint_name = ?", endpoint.Name).Count(&orgCnt).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrap(err, "fetching github organizations") + } + } + + var entCnt int64 + if err := tx.Model(&Enterprise{}).Where("endpoint_name = ?", endpoint.Name).Count(&entCnt).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrap(err, "fetching github enterprises") + } + } + + if credsCount > 0 || repoCnt > 0 || orgCnt > 0 || entCnt > 0 { + return errors.New("cannot delete endpoint with associated entities") } if err := tx.Unscoped().Delete(&endpoint).Error; err != nil { diff --git a/database/sql/organizations_test.go b/database/sql/organizations_test.go index c0524ff7..9954b978 100644 --- a/database/sql/organizations_test.go +++ b/database/sql/organizations_test.go @@ -222,7 +222,7 @@ func (s *OrgTestSuite) TestCreateOrganizationDBCreateErr() { WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.testCreds.ID, s.githubEndpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). - WithArgs(s.testCreds.Endpoint). + WithArgs(s.testCreds.Endpoint.Name). WillReturnRows(sqlmock.NewRows([]string{"name"}). AddRow(s.githubEndpoint.Name)) s.Fixtures.SQLMock. @@ -358,11 +358,11 @@ func (s *OrgTestSuite) TestUpdateOrganizationDBEncryptErr() { ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). WithArgs(s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). - AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). - WithArgs(s.testCreds.Endpoint). + WithArgs(s.testCreds.Endpoint.Name). WillReturnRows(sqlmock.NewRows([]string{"name"}). - AddRow(s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectRollback() _, err := s.StoreSQLMocked.UpdateOrganization(s.adminCtx, s.Fixtures.Orgs[0].ID, s.Fixtures.UpdateRepoParams) @@ -383,11 +383,11 @@ func (s *OrgTestSuite) TestUpdateOrganizationDBSaveErr() { ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). WithArgs(s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). - AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). - WithArgs(s.testCreds.Endpoint). + WithArgs(s.testCreds.Endpoint.Name). WillReturnRows(sqlmock.NewRows([]string{"name"}). - AddRow(s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock. ExpectExec(("UPDATE `organizations` SET")). WillReturnError(fmt.Errorf("saving org mock error")) @@ -414,11 +414,11 @@ func (s *OrgTestSuite) TestUpdateOrganizationDBDecryptingErr() { ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). WithArgs(s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). - AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). - WithArgs(s.testCreds.Endpoint). + WithArgs(s.testCreds.Endpoint.Name). WillReturnRows(sqlmock.NewRows([]string{"name"}). - AddRow(s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectRollback() _, err := s.StoreSQLMocked.UpdateOrganization(s.adminCtx, s.Fixtures.Orgs[0].ID, s.Fixtures.UpdateRepoParams) diff --git a/database/sql/repositories_test.go b/database/sql/repositories_test.go index 7fb272e9..3ff780a4 100644 --- a/database/sql/repositories_test.go +++ b/database/sql/repositories_test.go @@ -238,7 +238,7 @@ func (s *RepoTestSuite) TestCreateRepositoryInvalidDBCreateErr() { WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.testCreds.ID, s.githubEndpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). - WithArgs(s.testCreds.Endpoint). + WithArgs(s.testCreds.Endpoint.Name). WillReturnRows(sqlmock.NewRows([]string{"name"}). AddRow(s.githubEndpoint.Name)) s.Fixtures.SQLMock. @@ -396,11 +396,11 @@ func (s *RepoTestSuite) TestUpdateRepositoryDBEncryptErr() { ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). WithArgs(s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). - AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). - WithArgs(s.testCreds.Endpoint). + WithArgs(s.testCreds.Endpoint.Name). WillReturnRows(sqlmock.NewRows([]string{"name"}). - AddRow(s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectRollback() _, err := s.StoreSQLMocked.UpdateRepository(s.adminCtx, s.Fixtures.Repos[0].ID, s.Fixtures.UpdateRepoParams) @@ -421,11 +421,11 @@ func (s *RepoTestSuite) TestUpdateRepositoryDBSaveErr() { ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). WithArgs(s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). - AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). - WithArgs(s.testCreds.Endpoint). + WithArgs(s.testCreds.Endpoint.Name). WillReturnRows(sqlmock.NewRows([]string{"name"}). - AddRow(s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock. ExpectExec(("UPDATE `repositories` SET")). WillReturnError(fmt.Errorf("saving repo mock error")) @@ -451,11 +451,11 @@ func (s *RepoTestSuite) TestUpdateRepositoryDBDecryptingErr() { ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). WithArgs(s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). - AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). - WithArgs(s.testCreds.Endpoint). + WithArgs(s.testCreds.Endpoint.Name). WillReturnRows(sqlmock.NewRows([]string{"name"}). - AddRow(s.secondaryTestCreds.Endpoint)) + AddRow(s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectRollback() _, err := s.StoreSQLMocked.UpdateRepository(s.adminCtx, s.Fixtures.Repos[0].ID, s.Fixtures.UpdateRepoParams) diff --git a/params/params.go b/params/params.go index fcaf3113..c28990d7 100644 --- a/params/params.go +++ b/params/params.go @@ -567,7 +567,7 @@ type GithubCredentials struct { Repositories []Repository `json:"repositories,omitempty"` Organizations []Organization `json:"organizations,omitempty"` Enterprises []Enterprise `json:"enterprises,omitempty"` - Endpoint string `json:"endpoint"` + Endpoint GithubEndpoint `json:"endpoint"` CredentialsPayload []byte `json:"-"` } @@ -802,13 +802,16 @@ func (g GithubEntity) String() string { return "" } +// used by swagger client generated code +type GithubEndpoints []GithubEndpoint + type GithubEndpoint struct { Name string `json:"name"` Description string `json:"description"` APIBaseURL string `json:"api_base_url"` UploadBaseURL string `json:"upload_base_url"` BaseURL string `json:"base_url"` - CACertBundle []byte `json:"ca_cert_bundle"` + CACertBundle []byte `json:"ca_cert_bundle,omitempty"` - Credentials []GithubCredentials `json:"credentials"` + Credentials []GithubCredentials `json:"credentials,omitempty"` } diff --git a/params/requests.go b/params/requests.go index 5da66d53..ff7a3824 100644 --- a/params/requests.go +++ b/params/requests.go @@ -19,12 +19,17 @@ import ( "encoding/json" "encoding/pem" "fmt" + "net/url" "github.com/cloudbase/garm-provider-common/errors" commonParams "github.com/cloudbase/garm-provider-common/params" ) -const DefaultRunnerPrefix = "garm" +const ( + DefaultRunnerPrefix string = "garm" + httpsScheme string = "https" + httpScheme string = "http" +) type InstanceRequest struct { Name string `json:"name"` @@ -269,20 +274,128 @@ type InstanceUpdateMessage struct { } type CreateGithubEndpointParams struct { - Name string `json:"name"` - Description string `json:"description"` - APIBaseURL string `json:"api_base_url"` - UploadBaseURL string `json:"upload_base_url"` - BaseURL string `json:"base_url"` - CACertBundle []byte `json:"ca_cert_bundle"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + APIBaseURL string `json:"api_base_url,omitempty"` + UploadBaseURL string `json:"upload_base_url,omitempty"` + BaseURL string `json:"base_url,omitempty"` + CACertBundle []byte `json:"ca_cert_bundle,omitempty"` +} + +func (c CreateGithubEndpointParams) Validate() error { + if c.APIBaseURL == "" { + return errors.NewBadRequestError("missing api_base_url") + } + + url, err := url.Parse(c.APIBaseURL) + if err != nil || url.Scheme == "" || url.Host == "" { + return errors.NewBadRequestError("invalid api_base_url") + } + switch url.Scheme { + case httpsScheme, httpScheme: + default: + return errors.NewBadRequestError("invalid api_base_url") + } + + if c.UploadBaseURL == "" { + return errors.NewBadRequestError("missing upload_base_url") + } + + url, err = url.Parse(c.UploadBaseURL) + if err != nil || url.Scheme == "" || url.Host == "" { + return errors.NewBadRequestError("invalid upload_base_url") + } + + switch url.Scheme { + case httpsScheme, httpScheme: + default: + return errors.NewBadRequestError("invalid api_base_url") + } + + if c.BaseURL == "" { + return errors.NewBadRequestError("missing base_url") + } + + url, err = url.Parse(c.BaseURL) + if err != nil || url.Scheme == "" || url.Host == "" { + return errors.NewBadRequestError("invalid base_url") + } + + switch url.Scheme { + case httpsScheme, httpScheme: + default: + return errors.NewBadRequestError("invalid api_base_url") + } + + if c.CACertBundle != nil { + block, _ := pem.Decode(c.CACertBundle) + if block == nil { + return errors.NewBadRequestError("invalid ca_cert_bundle") + } + if _, err := x509.ParseCertificates(block.Bytes); err != nil { + return errors.NewBadRequestError("invalid ca_cert_bundle") + } + } + + return nil } type UpdateGithubEndpointParams struct { - Description *string `json:"description"` - APIBaseURL *string `json:"api_base_url"` - UploadBaseURL *string `json:"upload_base_url"` - BaseURL *string `json:"base_url"` - CACertBundle []byte `json:"ca_cert_bundle"` + Description *string `json:"description,omitempty"` + APIBaseURL *string `json:"api_base_url,omitempty"` + UploadBaseURL *string `json:"upload_base_url,omitempty"` + BaseURL *string `json:"base_url,omitempty"` + CACertBundle []byte `json:"ca_cert_bundle,omitempty"` +} + +func (u UpdateGithubEndpointParams) Validate() error { + if u.APIBaseURL != nil { + url, err := url.Parse(*u.APIBaseURL) + if err != nil || url.Scheme == "" || url.Host == "" { + return errors.NewBadRequestError("invalid api_base_url") + } + switch url.Scheme { + case httpsScheme, httpScheme: + default: + return errors.NewBadRequestError("invalid api_base_url") + } + } + + if u.UploadBaseURL != nil { + url, err := url.Parse(*u.UploadBaseURL) + if err != nil || url.Scheme == "" || url.Host == "" { + return errors.NewBadRequestError("invalid upload_base_url") + } + switch url.Scheme { + case httpsScheme, httpScheme: + default: + return errors.NewBadRequestError("invalid api_base_url") + } + } + + if u.BaseURL != nil { + url, err := url.Parse(*u.BaseURL) + if err != nil || url.Scheme == "" || url.Host == "" { + return errors.NewBadRequestError("invalid base_url") + } + switch url.Scheme { + case httpsScheme, httpScheme: + default: + return errors.NewBadRequestError("invalid api_base_url") + } + } + + if u.CACertBundle != nil { + block, _ := pem.Decode(u.CACertBundle) + if block == nil { + return errors.NewBadRequestError("invalid ca_cert_bundle") + } + if _, err := x509.ParseCertificates(block.Bytes); err != nil { + return errors.NewBadRequestError("invalid ca_cert_bundle") + } + } + + return nil } type GithubPAT struct { diff --git a/runner/endpoints.go b/runner/endpoints.go new file mode 100644 index 00000000..1f6431ea --- /dev/null +++ b/runner/endpoints.go @@ -0,0 +1,82 @@ +package runner + +import ( + "context" + + "github.com/pkg/errors" + + runnerErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/auth" + "github.com/cloudbase/garm/params" +) + +func (r *Runner) CreateGithubEndpoint(ctx context.Context, param params.CreateGithubEndpointParams) (params.GithubEndpoint, error) { + if !auth.IsAdmin(ctx) { + return params.GithubEndpoint{}, runnerErrors.ErrUnauthorized + } + + if err := param.Validate(); err != nil { + return params.GithubEndpoint{}, errors.Wrap(err, "failed to validate github endpoint params") + } + + ep, err := r.store.CreateGithubEndpoint(ctx, param) + if err != nil { + return params.GithubEndpoint{}, errors.Wrap(err, "failed to create github endpoint") + } + + return ep, nil +} + +func (r *Runner) GetGithubEndpoint(ctx context.Context, name string) (params.GithubEndpoint, error) { + if !auth.IsAdmin(ctx) { + return params.GithubEndpoint{}, runnerErrors.ErrUnauthorized + } + endpoint, err := r.store.GetGithubEndpoint(ctx, name) + if err != nil { + return params.GithubEndpoint{}, errors.Wrap(err, "failed to get github endpoint") + } + + return endpoint, nil +} + +func (r *Runner) DeleteGithubEndpoint(ctx context.Context, name string) error { + if !auth.IsAdmin(ctx) { + return runnerErrors.ErrUnauthorized + } + + err := r.store.DeleteGithubEndpoint(ctx, name) + if err != nil { + return errors.Wrap(err, "failed to delete github endpoint") + } + + return nil +} + +func (r *Runner) UpdateGithubEndpoint(ctx context.Context, name string, param params.UpdateGithubEndpointParams) (params.GithubEndpoint, error) { + if !auth.IsAdmin(ctx) { + return params.GithubEndpoint{}, runnerErrors.ErrUnauthorized + } + + if err := param.Validate(); err != nil { + return params.GithubEndpoint{}, errors.Wrap(err, "failed to validate github endpoint params") + } + + newEp, err := r.store.UpdateGithubEndpoint(ctx, name, param) + if err != nil { + return params.GithubEndpoint{}, errors.Wrap(err, "failed to update github endpoint") + } + return newEp, nil +} + +func (r *Runner) ListGithubEndpoints(ctx context.Context) ([]params.GithubEndpoint, error) { + if !auth.IsAdmin(ctx) { + return nil, runnerErrors.ErrUnauthorized + } + + endpoints, err := r.store.ListGithubEndpoints(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to list github endpoints") + } + + return endpoints, nil +} From eadbe784b92e02ec729b93386007f9376cd941b4 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Fri, 19 Apr 2024 08:47:44 +0000 Subject: [PATCH 08/27] Add github credentials API and cli code Signed-off-by: Gabriel Adrian Samfira --- apiserver/controllers/controllers.go | 5 +- apiserver/controllers/credentials.go | 201 +++++++++ apiserver/routers/routers.go | 26 +- apiserver/swagger-models.yaml | 16 +- apiserver/swagger.yaml | 132 +++++- .../create_credentials_parameters.go | 151 +++++++ .../create_credentials_responses.go | 174 ++++++++ client/credentials/credentials_client.go | 159 ++++++- .../delete_credentials_parameters.go | 152 +++++++ .../delete_credentials_responses.go | 103 +++++ .../credentials/get_credentials_parameters.go | 152 +++++++ .../credentials/get_credentials_responses.go | 174 ++++++++ .../credentials/list_credentials_responses.go | 10 +- .../update_credentials_parameters.go | 174 ++++++++ .../update_credentials_responses.go | 174 ++++++++ cmd/garm-cli/cmd/credentials.go | 76 ---- cmd/garm-cli/cmd/github_credentials.go | 388 ++++++++++++++++++ cmd/garm-cli/cmd/github_endpoints.go | 12 +- database/common/common.go | 2 +- database/common/mocks/Store.go | 18 +- database/sql/github.go | 7 +- database/sql/sql.go | 3 +- internal/testing/testing.go | 3 +- params/params.go | 4 + params/requests.go | 127 ++++-- runner/github_credentials.go | 83 ++++ runner/{endpoints.go => github_endpoints.go} | 0 runner/runner.go | 13 - 28 files changed, 2364 insertions(+), 175 deletions(-) create mode 100644 client/credentials/create_credentials_parameters.go create mode 100644 client/credentials/create_credentials_responses.go create mode 100644 client/credentials/delete_credentials_parameters.go create mode 100644 client/credentials/delete_credentials_responses.go create mode 100644 client/credentials/get_credentials_parameters.go create mode 100644 client/credentials/get_credentials_responses.go create mode 100644 client/credentials/update_credentials_parameters.go create mode 100644 client/credentials/update_credentials_responses.go delete mode 100644 cmd/garm-cli/cmd/credentials.go create mode 100644 cmd/garm-cli/cmd/github_credentials.go create mode 100644 runner/github_credentials.go rename runner/{endpoints.go => github_endpoints.go} (100%) diff --git a/apiserver/controllers/controllers.go b/apiserver/controllers/controllers.go index c1d4561e..56745b82 100644 --- a/apiserver/controllers/controllers.go +++ b/apiserver/controllers/controllers.go @@ -102,10 +102,7 @@ func (a *APIController) handleWorkflowJobEvent(ctx context.Context, w http.Respo handleError(ctx, w, gErrors.NewBadRequestError("invalid post body: %s", err)) return } - slog.Info("received webhook", "body", string(body)) - for k, v := range r.Header { - slog.InfoContext(ctx, "header", "key", k, "value", v) - } + signature := r.Header.Get("X-Hub-Signature-256") hookType := r.Header.Get("X-Github-Hook-Installation-Target-Type") diff --git a/apiserver/controllers/credentials.go b/apiserver/controllers/credentials.go index bfc3d494..70869b54 100644 --- a/apiserver/controllers/credentials.go +++ b/apiserver/controllers/credentials.go @@ -3,10 +3,18 @@ package controllers import ( "encoding/json" "log/slog" + "math" "net/http" + "strconv" + + "github.com/gorilla/mux" + + gErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/params" ) // swagger:route GET /credentials credentials ListCredentials +// swagger:route GET /github/credentials credentials ListCredentials // // List all credentials. // @@ -26,3 +34,196 @@ func (a *APIController) ListCredentials(w http.ResponseWriter, r *http.Request) slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") } } + +// swagger:route POST /github/credentials credentials CreateCredentials +// +// Create a GitHub credential. +// +// Parameters: +// + name: Body +// description: Parameters used when creating a GitHub credential. +// type: CreateGithubCredentialsParams +// in: body +// required: true +// +// Responses: +// 200: GithubCredentials +// 400: APIErrorResponse +func (a *APIController) CreateGithubCredential(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var params params.CreateGithubCredentialsParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to decode request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + cred, err := a.r.CreateGithubCredentials(ctx, params) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to create GitHub credential") + handleError(ctx, w, err) + return + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(cred); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") + } +} + +// swagger:route GET /github/credentials/{id} credentials GetCredentials +// +// Get a GitHub credential. +// +// Parameters: +// + name: id +// description: ID of the GitHub credential. +// type: integer +// in: path +// required: true +// +// Responses: +// 200: GithubCredentials +// 400: APIErrorResponse +func (a *APIController) GetGithubCredential(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + idParam, ok := vars["id"] + if !ok { + slog.ErrorContext(ctx, "missing id in request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + id, err := strconv.ParseUint(idParam, 10, 64) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to parse id") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + if id > math.MaxUint { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "id is too large") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + cred, err := a.r.GetGithubCredentials(ctx, uint(id)) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to get GitHub credential") + handleError(ctx, w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(cred); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") + } +} + +// swagger:route DELETE /github/credentials/{id} credentials DeleteCredentials +// +// Delete a GitHub credential. +// +// Parameters: +// + name: id +// description: ID of the GitHub credential. +// type: integer +// in: path +// required: true +// +// Responses: +// default: APIErrorResponse +func (a *APIController) DeleteGithubCredential(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + idParam, ok := vars["id"] + if !ok { + slog.ErrorContext(ctx, "missing id in request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + id, err := strconv.ParseUint(idParam, 10, 64) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to parse id") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + if id > math.MaxUint { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "id is too large") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + if err := a.r.DeleteGithubCredentials(ctx, uint(id)); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to delete GitHub credential") + handleError(ctx, w, err) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// swagger:route PUT /github/credentials/{id} credentials UpdateCredentials +// +// Update a GitHub credential. +// +// Parameters: +// + name: id +// description: ID of the GitHub credential. +// type: integer +// in: path +// required: true +// + name: Body +// description: Parameters used when updating a GitHub credential. +// type: UpdateGithubCredentialsParams +// in: body +// required: true +// +// Responses: +// 200: GithubCredentials +// 400: APIErrorResponse +func (a *APIController) UpdateGithubCredential(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + idParam, ok := vars["id"] + if !ok { + slog.ErrorContext(ctx, "missing id in request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + id, err := strconv.ParseUint(idParam, 10, 64) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to parse id") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + if id > math.MaxUint { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "id is too large") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + var params params.UpdateGithubCredentialsParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to decode request") + handleError(ctx, w, gErrors.ErrBadRequest) + return + } + + cred, err := a.r.UpdateGithubCredentials(ctx, uint(id), params) + if err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to update GitHub credential") + handleError(ctx, w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(cred); err != nil { + slog.With(slog.Any("error", err)).ErrorContext(ctx, "failed to encode response") + } +} diff --git a/apiserver/routers/routers.go b/apiserver/routers/routers.go index fcb971d0..583fb8ac 100644 --- a/apiserver/routers/routers.go +++ b/apiserver/routers/routers.go @@ -339,10 +339,6 @@ func NewAPIRouter(han *controllers.APIController, authMiddleware, initMiddleware apiRouter.Handle("/enterprises/", http.HandlerFunc(han.CreateEnterpriseHandler)).Methods("POST", "OPTIONS") apiRouter.Handle("/enterprises", http.HandlerFunc(han.CreateEnterpriseHandler)).Methods("POST", "OPTIONS") - // Credentials - apiRouter.Handle("/credentials/", http.HandlerFunc(han.ListCredentials)).Methods("GET", "OPTIONS") - apiRouter.Handle("/credentials", http.HandlerFunc(han.ListCredentials)).Methods("GET", "OPTIONS") - // Providers apiRouter.Handle("/providers/", http.HandlerFunc(han.ListProviders)).Methods("GET", "OPTIONS") apiRouter.Handle("/providers", http.HandlerFunc(han.ListProviders)).Methods("GET", "OPTIONS") @@ -370,6 +366,28 @@ func NewAPIRouter(han *controllers.APIController, authMiddleware, initMiddleware apiRouter.Handle("/github/endpoints/{name}/", http.HandlerFunc(han.UpdateGithubEndpoint)).Methods("PUT", "OPTIONS") apiRouter.Handle("/github/endpoints/{name}", http.HandlerFunc(han.UpdateGithubEndpoint)).Methods("PUT", "OPTIONS") + //////////////////////// + // Github credentials // + //////////////////////// + // Legacy credentials path + apiRouter.Handle("/credentials/", http.HandlerFunc(han.ListCredentials)).Methods("GET", "OPTIONS") + apiRouter.Handle("/credentials", http.HandlerFunc(han.ListCredentials)).Methods("GET", "OPTIONS") + // List Github Credentials + apiRouter.Handle("/github/credentials/", http.HandlerFunc(han.ListCredentials)).Methods("GET", "OPTIONS") + apiRouter.Handle("/github/credentials", http.HandlerFunc(han.ListCredentials)).Methods("GET", "OPTIONS") + // Create Github Credentials + apiRouter.Handle("/github/credentials/", http.HandlerFunc(han.CreateGithubCredential)).Methods("POST", "OPTIONS") + apiRouter.Handle("/github/credentials", http.HandlerFunc(han.CreateGithubCredential)).Methods("POST", "OPTIONS") + // Get Github Credential + apiRouter.Handle("/github/credentials/{id}/", http.HandlerFunc(han.GetGithubCredential)).Methods("GET", "OPTIONS") + apiRouter.Handle("/github/credentials/{id}", http.HandlerFunc(han.GetGithubCredential)).Methods("GET", "OPTIONS") + // Delete Github Credential + apiRouter.Handle("/github/credentials/{id}/", http.HandlerFunc(han.DeleteGithubCredential)).Methods("DELETE", "OPTIONS") + apiRouter.Handle("/github/credentials/{id}", http.HandlerFunc(han.DeleteGithubCredential)).Methods("DELETE", "OPTIONS") + // Update Github Credential + apiRouter.Handle("/github/credentials/{id}/", http.HandlerFunc(han.UpdateGithubCredential)).Methods("PUT", "OPTIONS") + apiRouter.Handle("/github/credentials/{id}", http.HandlerFunc(han.UpdateGithubCredential)).Methods("PUT", "OPTIONS") + // Websocket log writer apiRouter.Handle("/{ws:ws\\/?}", http.HandlerFunc(han.WSHandler)).Methods("GET") diff --git a/apiserver/swagger-models.yaml b/apiserver/swagger-models.yaml index ae9bdd0b..b9ab5670 100644 --- a/apiserver/swagger-models.yaml +++ b/apiserver/swagger-models.yaml @@ -263,4 +263,18 @@ definitions: type: CreateGithubEndpointParams import: package: github.com/cloudbase/garm/params - alias: garm_params \ No newline at end of file + alias: garm_params + CreateGithubCredentialsParams: + type: object + x-go-type: + type: CreateGithubCredentialsParams + import: + package: github.com/cloudbase/garm/params + alias: garm_params + UpdateGithubCredentialsParams: + type: object + x-go-type: + type: UpdateGithubCredentialsParams + import: + package: github.com/cloudbase/garm/params + alias: garm_params diff --git a/apiserver/swagger.yaml b/apiserver/swagger.yaml index 53ca9309..afed3747 100644 --- a/apiserver/swagger.yaml +++ b/apiserver/swagger.yaml @@ -23,6 +23,13 @@ definitions: alias: garm_params package: github.com/cloudbase/garm/params type: CreateEnterpriseParams + CreateGithubCredentialsParams: + type: object + x-go-type: + import: + alias: garm_params + package: github.com/cloudbase/garm/params + type: CreateGithubCredentialsParams CreateGithubEndpointParams: type: object x-go-type: @@ -244,6 +251,13 @@ definitions: alias: garm_params package: github.com/cloudbase/garm/params type: UpdateEntityParams + UpdateGithubCredentialsParams: + type: object + x-go-type: + import: + alias: garm_params + package: github.com/cloudbase/garm/params + type: UpdateGithubCredentialsParams UpdateGithubEndpointParams: type: object x-go-type: @@ -312,21 +326,6 @@ paths: summary: Get controller info. tags: - controllerInfo - /credentials: - get: - operationId: ListCredentials - responses: - "200": - description: Credentials - schema: - $ref: '#/definitions/Credentials' - "400": - description: APIErrorResponse - schema: - $ref: '#/definitions/APIErrorResponse' - summary: List all credentials. - tags: - - credentials /enterprises: get: operationId: ListEnterprises @@ -610,6 +609,109 @@ paths: summary: Initialize the first run of the controller. tags: - first-run + /github/credentials: + get: + operationId: ListCredentials + responses: + "200": + description: Credentials + schema: + $ref: '#/definitions/Credentials' + "400": + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: List all credentials. + tags: + - credentials + post: + operationId: CreateCredentials + parameters: + - description: Parameters used when creating a GitHub credential. + in: body + name: Body + required: true + schema: + $ref: '#/definitions/CreateGithubCredentialsParams' + description: Parameters used when creating a GitHub credential. + type: object + responses: + "200": + description: GithubCredentials + schema: + $ref: '#/definitions/GithubCredentials' + "400": + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: Create a GitHub credential. + tags: + - credentials + /github/credentials/{id}: + delete: + operationId: DeleteCredentials + parameters: + - description: ID of the GitHub credential. + in: path + name: id + required: true + type: integer + responses: + default: + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: Delete a GitHub credential. + tags: + - credentials + get: + operationId: GetCredentials + parameters: + - description: ID of the GitHub credential. + in: path + name: id + required: true + type: integer + responses: + "200": + description: GithubCredentials + schema: + $ref: '#/definitions/GithubCredentials' + "400": + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: Get a GitHub credential. + tags: + - credentials + put: + operationId: UpdateCredentials + parameters: + - description: ID of the GitHub credential. + in: path + name: id + required: true + type: integer + - description: Parameters used when updating a GitHub credential. + in: body + name: Body + required: true + schema: + $ref: '#/definitions/UpdateGithubCredentialsParams' + description: Parameters used when updating a GitHub credential. + type: object + responses: + "200": + description: GithubCredentials + schema: + $ref: '#/definitions/GithubCredentials' + "400": + description: APIErrorResponse + schema: + $ref: '#/definitions/APIErrorResponse' + summary: Update a GitHub credential. + tags: + - credentials /github/endpoints: get: operationId: ListGithubEndpoints diff --git a/client/credentials/create_credentials_parameters.go b/client/credentials/create_credentials_parameters.go new file mode 100644 index 00000000..4288808f --- /dev/null +++ b/client/credentials/create_credentials_parameters.go @@ -0,0 +1,151 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + + garm_params "github.com/cloudbase/garm/params" +) + +// NewCreateCredentialsParams creates a new CreateCredentialsParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewCreateCredentialsParams() *CreateCredentialsParams { + return &CreateCredentialsParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewCreateCredentialsParamsWithTimeout creates a new CreateCredentialsParams object +// with the ability to set a timeout on a request. +func NewCreateCredentialsParamsWithTimeout(timeout time.Duration) *CreateCredentialsParams { + return &CreateCredentialsParams{ + timeout: timeout, + } +} + +// NewCreateCredentialsParamsWithContext creates a new CreateCredentialsParams object +// with the ability to set a context for a request. +func NewCreateCredentialsParamsWithContext(ctx context.Context) *CreateCredentialsParams { + return &CreateCredentialsParams{ + Context: ctx, + } +} + +// NewCreateCredentialsParamsWithHTTPClient creates a new CreateCredentialsParams object +// with the ability to set a custom HTTPClient for a request. +func NewCreateCredentialsParamsWithHTTPClient(client *http.Client) *CreateCredentialsParams { + return &CreateCredentialsParams{ + HTTPClient: client, + } +} + +/* +CreateCredentialsParams contains all the parameters to send to the API endpoint + + for the create credentials operation. + + Typically these are written to a http.Request. +*/ +type CreateCredentialsParams struct { + + /* Body. + + Parameters used when creating a GitHub credential. + */ + Body garm_params.CreateGithubCredentialsParams + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the create credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *CreateCredentialsParams) WithDefaults() *CreateCredentialsParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the create credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *CreateCredentialsParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the create credentials params +func (o *CreateCredentialsParams) WithTimeout(timeout time.Duration) *CreateCredentialsParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the create credentials params +func (o *CreateCredentialsParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the create credentials params +func (o *CreateCredentialsParams) WithContext(ctx context.Context) *CreateCredentialsParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the create credentials params +func (o *CreateCredentialsParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the create credentials params +func (o *CreateCredentialsParams) WithHTTPClient(client *http.Client) *CreateCredentialsParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the create credentials params +func (o *CreateCredentialsParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithBody adds the body to the create credentials params +func (o *CreateCredentialsParams) WithBody(body garm_params.CreateGithubCredentialsParams) *CreateCredentialsParams { + o.SetBody(body) + return o +} + +// SetBody adds the body to the create credentials params +func (o *CreateCredentialsParams) SetBody(body garm_params.CreateGithubCredentialsParams) { + o.Body = body +} + +// WriteToRequest writes these params to a swagger request +func (o *CreateCredentialsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + if err := r.SetBodyParam(o.Body); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/credentials/create_credentials_responses.go b/client/credentials/create_credentials_responses.go new file mode 100644 index 00000000..1b6dbe94 --- /dev/null +++ b/client/credentials/create_credentials_responses.go @@ -0,0 +1,174 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" + garm_params "github.com/cloudbase/garm/params" +) + +// CreateCredentialsReader is a Reader for the CreateCredentials structure. +type CreateCredentialsReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *CreateCredentialsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewCreateCredentialsOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewCreateCredentialsBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("[POST /github/credentials] CreateCredentials", response, response.Code()) + } +} + +// NewCreateCredentialsOK creates a CreateCredentialsOK with default headers values +func NewCreateCredentialsOK() *CreateCredentialsOK { + return &CreateCredentialsOK{} +} + +/* +CreateCredentialsOK describes a response with status code 200, with default header values. + +GithubCredentials +*/ +type CreateCredentialsOK struct { + Payload garm_params.GithubCredentials +} + +// IsSuccess returns true when this create credentials o k response has a 2xx status code +func (o *CreateCredentialsOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this create credentials o k response has a 3xx status code +func (o *CreateCredentialsOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this create credentials o k response has a 4xx status code +func (o *CreateCredentialsOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this create credentials o k response has a 5xx status code +func (o *CreateCredentialsOK) IsServerError() bool { + return false +} + +// IsCode returns true when this create credentials o k response a status code equal to that given +func (o *CreateCredentialsOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the create credentials o k response +func (o *CreateCredentialsOK) Code() int { + return 200 +} + +func (o *CreateCredentialsOK) Error() string { + return fmt.Sprintf("[POST /github/credentials][%d] createCredentialsOK %+v", 200, o.Payload) +} + +func (o *CreateCredentialsOK) String() string { + return fmt.Sprintf("[POST /github/credentials][%d] createCredentialsOK %+v", 200, o.Payload) +} + +func (o *CreateCredentialsOK) GetPayload() garm_params.GithubCredentials { + return o.Payload +} + +func (o *CreateCredentialsOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewCreateCredentialsBadRequest creates a CreateCredentialsBadRequest with default headers values +func NewCreateCredentialsBadRequest() *CreateCredentialsBadRequest { + return &CreateCredentialsBadRequest{} +} + +/* +CreateCredentialsBadRequest describes a response with status code 400, with default header values. + +APIErrorResponse +*/ +type CreateCredentialsBadRequest struct { + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this create credentials bad request response has a 2xx status code +func (o *CreateCredentialsBadRequest) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this create credentials bad request response has a 3xx status code +func (o *CreateCredentialsBadRequest) IsRedirect() bool { + return false +} + +// IsClientError returns true when this create credentials bad request response has a 4xx status code +func (o *CreateCredentialsBadRequest) IsClientError() bool { + return true +} + +// IsServerError returns true when this create credentials bad request response has a 5xx status code +func (o *CreateCredentialsBadRequest) IsServerError() bool { + return false +} + +// IsCode returns true when this create credentials bad request response a status code equal to that given +func (o *CreateCredentialsBadRequest) IsCode(code int) bool { + return code == 400 +} + +// Code gets the status code for the create credentials bad request response +func (o *CreateCredentialsBadRequest) Code() int { + return 400 +} + +func (o *CreateCredentialsBadRequest) Error() string { + return fmt.Sprintf("[POST /github/credentials][%d] createCredentialsBadRequest %+v", 400, o.Payload) +} + +func (o *CreateCredentialsBadRequest) String() string { + return fmt.Sprintf("[POST /github/credentials][%d] createCredentialsBadRequest %+v", 400, o.Payload) +} + +func (o *CreateCredentialsBadRequest) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *CreateCredentialsBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/credentials/credentials_client.go b/client/credentials/credentials_client.go index 226b702d..9e19f1f3 100644 --- a/client/credentials/credentials_client.go +++ b/client/credentials/credentials_client.go @@ -30,11 +30,129 @@ type ClientOption func(*runtime.ClientOperation) // ClientService is the interface for Client methods type ClientService interface { + CreateCredentials(params *CreateCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*CreateCredentialsOK, error) + + DeleteCredentials(params *DeleteCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) error + + GetCredentials(params *GetCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetCredentialsOK, error) + ListCredentials(params *ListCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*ListCredentialsOK, error) + UpdateCredentials(params *UpdateCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateCredentialsOK, error) + SetTransport(transport runtime.ClientTransport) } +/* +CreateCredentials creates a git hub credential +*/ +func (a *Client) CreateCredentials(params *CreateCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*CreateCredentialsOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewCreateCredentialsParams() + } + op := &runtime.ClientOperation{ + ID: "CreateCredentials", + Method: "POST", + PathPattern: "/github/credentials", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &CreateCredentialsReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*CreateCredentialsOK) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for CreateCredentials: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + +/* +DeleteCredentials deletes a git hub credential +*/ +func (a *Client) DeleteCredentials(params *DeleteCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) error { + // TODO: Validate the params before sending + if params == nil { + params = NewDeleteCredentialsParams() + } + op := &runtime.ClientOperation{ + ID: "DeleteCredentials", + Method: "DELETE", + PathPattern: "/github/credentials/{id}", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &DeleteCredentialsReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + _, err := a.transport.Submit(op) + if err != nil { + return err + } + return nil +} + +/* +GetCredentials gets a git hub credential +*/ +func (a *Client) GetCredentials(params *GetCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetCredentialsOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewGetCredentialsParams() + } + op := &runtime.ClientOperation{ + ID: "GetCredentials", + Method: "GET", + PathPattern: "/github/credentials/{id}", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &GetCredentialsReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*GetCredentialsOK) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for GetCredentials: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + /* ListCredentials lists all credentials */ @@ -46,7 +164,7 @@ func (a *Client) ListCredentials(params *ListCredentialsParams, authInfo runtime op := &runtime.ClientOperation{ ID: "ListCredentials", Method: "GET", - PathPattern: "/credentials", + PathPattern: "/github/credentials", ProducesMediaTypes: []string{"application/json"}, ConsumesMediaTypes: []string{"application/json"}, Schemes: []string{"http"}, @@ -74,6 +192,45 @@ func (a *Client) ListCredentials(params *ListCredentialsParams, authInfo runtime panic(msg) } +/* +UpdateCredentials updates a git hub credential +*/ +func (a *Client) UpdateCredentials(params *UpdateCredentialsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateCredentialsOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewUpdateCredentialsParams() + } + op := &runtime.ClientOperation{ + ID: "UpdateCredentials", + Method: "PUT", + PathPattern: "/github/credentials/{id}", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &UpdateCredentialsReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*UpdateCredentialsOK) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for UpdateCredentials: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + // SetTransport changes the transport on the client func (a *Client) SetTransport(transport runtime.ClientTransport) { a.transport = transport diff --git a/client/credentials/delete_credentials_parameters.go b/client/credentials/delete_credentials_parameters.go new file mode 100644 index 00000000..f36f8725 --- /dev/null +++ b/client/credentials/delete_credentials_parameters.go @@ -0,0 +1,152 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// NewDeleteCredentialsParams creates a new DeleteCredentialsParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewDeleteCredentialsParams() *DeleteCredentialsParams { + return &DeleteCredentialsParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewDeleteCredentialsParamsWithTimeout creates a new DeleteCredentialsParams object +// with the ability to set a timeout on a request. +func NewDeleteCredentialsParamsWithTimeout(timeout time.Duration) *DeleteCredentialsParams { + return &DeleteCredentialsParams{ + timeout: timeout, + } +} + +// NewDeleteCredentialsParamsWithContext creates a new DeleteCredentialsParams object +// with the ability to set a context for a request. +func NewDeleteCredentialsParamsWithContext(ctx context.Context) *DeleteCredentialsParams { + return &DeleteCredentialsParams{ + Context: ctx, + } +} + +// NewDeleteCredentialsParamsWithHTTPClient creates a new DeleteCredentialsParams object +// with the ability to set a custom HTTPClient for a request. +func NewDeleteCredentialsParamsWithHTTPClient(client *http.Client) *DeleteCredentialsParams { + return &DeleteCredentialsParams{ + HTTPClient: client, + } +} + +/* +DeleteCredentialsParams contains all the parameters to send to the API endpoint + + for the delete credentials operation. + + Typically these are written to a http.Request. +*/ +type DeleteCredentialsParams struct { + + /* ID. + + ID of the GitHub credential. + */ + ID int64 + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the delete credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *DeleteCredentialsParams) WithDefaults() *DeleteCredentialsParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the delete credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *DeleteCredentialsParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the delete credentials params +func (o *DeleteCredentialsParams) WithTimeout(timeout time.Duration) *DeleteCredentialsParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the delete credentials params +func (o *DeleteCredentialsParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the delete credentials params +func (o *DeleteCredentialsParams) WithContext(ctx context.Context) *DeleteCredentialsParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the delete credentials params +func (o *DeleteCredentialsParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the delete credentials params +func (o *DeleteCredentialsParams) WithHTTPClient(client *http.Client) *DeleteCredentialsParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the delete credentials params +func (o *DeleteCredentialsParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithID adds the id to the delete credentials params +func (o *DeleteCredentialsParams) WithID(id int64) *DeleteCredentialsParams { + o.SetID(id) + return o +} + +// SetID adds the id to the delete credentials params +func (o *DeleteCredentialsParams) SetID(id int64) { + o.ID = id +} + +// WriteToRequest writes these params to a swagger request +func (o *DeleteCredentialsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + // path param id + if err := r.SetPathParam("id", swag.FormatInt64(o.ID)); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/credentials/delete_credentials_responses.go b/client/credentials/delete_credentials_responses.go new file mode 100644 index 00000000..a174c172 --- /dev/null +++ b/client/credentials/delete_credentials_responses.go @@ -0,0 +1,103 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" +) + +// DeleteCredentialsReader is a Reader for the DeleteCredentials structure. +type DeleteCredentialsReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *DeleteCredentialsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + result := NewDeleteCredentialsDefault(response.Code()) + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + if response.Code()/100 == 2 { + return result, nil + } + return nil, result +} + +// NewDeleteCredentialsDefault creates a DeleteCredentialsDefault with default headers values +func NewDeleteCredentialsDefault(code int) *DeleteCredentialsDefault { + return &DeleteCredentialsDefault{ + _statusCode: code, + } +} + +/* +DeleteCredentialsDefault describes a response with status code -1, with default header values. + +APIErrorResponse +*/ +type DeleteCredentialsDefault struct { + _statusCode int + + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this delete credentials default response has a 2xx status code +func (o *DeleteCredentialsDefault) IsSuccess() bool { + return o._statusCode/100 == 2 +} + +// IsRedirect returns true when this delete credentials default response has a 3xx status code +func (o *DeleteCredentialsDefault) IsRedirect() bool { + return o._statusCode/100 == 3 +} + +// IsClientError returns true when this delete credentials default response has a 4xx status code +func (o *DeleteCredentialsDefault) IsClientError() bool { + return o._statusCode/100 == 4 +} + +// IsServerError returns true when this delete credentials default response has a 5xx status code +func (o *DeleteCredentialsDefault) IsServerError() bool { + return o._statusCode/100 == 5 +} + +// IsCode returns true when this delete credentials default response a status code equal to that given +func (o *DeleteCredentialsDefault) IsCode(code int) bool { + return o._statusCode == code +} + +// Code gets the status code for the delete credentials default response +func (o *DeleteCredentialsDefault) Code() int { + return o._statusCode +} + +func (o *DeleteCredentialsDefault) Error() string { + return fmt.Sprintf("[DELETE /github/credentials/{id}][%d] DeleteCredentials default %+v", o._statusCode, o.Payload) +} + +func (o *DeleteCredentialsDefault) String() string { + return fmt.Sprintf("[DELETE /github/credentials/{id}][%d] DeleteCredentials default %+v", o._statusCode, o.Payload) +} + +func (o *DeleteCredentialsDefault) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *DeleteCredentialsDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/credentials/get_credentials_parameters.go b/client/credentials/get_credentials_parameters.go new file mode 100644 index 00000000..ff8305e8 --- /dev/null +++ b/client/credentials/get_credentials_parameters.go @@ -0,0 +1,152 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// NewGetCredentialsParams creates a new GetCredentialsParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewGetCredentialsParams() *GetCredentialsParams { + return &GetCredentialsParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewGetCredentialsParamsWithTimeout creates a new GetCredentialsParams object +// with the ability to set a timeout on a request. +func NewGetCredentialsParamsWithTimeout(timeout time.Duration) *GetCredentialsParams { + return &GetCredentialsParams{ + timeout: timeout, + } +} + +// NewGetCredentialsParamsWithContext creates a new GetCredentialsParams object +// with the ability to set a context for a request. +func NewGetCredentialsParamsWithContext(ctx context.Context) *GetCredentialsParams { + return &GetCredentialsParams{ + Context: ctx, + } +} + +// NewGetCredentialsParamsWithHTTPClient creates a new GetCredentialsParams object +// with the ability to set a custom HTTPClient for a request. +func NewGetCredentialsParamsWithHTTPClient(client *http.Client) *GetCredentialsParams { + return &GetCredentialsParams{ + HTTPClient: client, + } +} + +/* +GetCredentialsParams contains all the parameters to send to the API endpoint + + for the get credentials operation. + + Typically these are written to a http.Request. +*/ +type GetCredentialsParams struct { + + /* ID. + + ID of the GitHub credential. + */ + ID int64 + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the get credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *GetCredentialsParams) WithDefaults() *GetCredentialsParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the get credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *GetCredentialsParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the get credentials params +func (o *GetCredentialsParams) WithTimeout(timeout time.Duration) *GetCredentialsParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the get credentials params +func (o *GetCredentialsParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the get credentials params +func (o *GetCredentialsParams) WithContext(ctx context.Context) *GetCredentialsParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the get credentials params +func (o *GetCredentialsParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the get credentials params +func (o *GetCredentialsParams) WithHTTPClient(client *http.Client) *GetCredentialsParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the get credentials params +func (o *GetCredentialsParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithID adds the id to the get credentials params +func (o *GetCredentialsParams) WithID(id int64) *GetCredentialsParams { + o.SetID(id) + return o +} + +// SetID adds the id to the get credentials params +func (o *GetCredentialsParams) SetID(id int64) { + o.ID = id +} + +// WriteToRequest writes these params to a swagger request +func (o *GetCredentialsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + // path param id + if err := r.SetPathParam("id", swag.FormatInt64(o.ID)); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/credentials/get_credentials_responses.go b/client/credentials/get_credentials_responses.go new file mode 100644 index 00000000..ef2d041d --- /dev/null +++ b/client/credentials/get_credentials_responses.go @@ -0,0 +1,174 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" + garm_params "github.com/cloudbase/garm/params" +) + +// GetCredentialsReader is a Reader for the GetCredentials structure. +type GetCredentialsReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *GetCredentialsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewGetCredentialsOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewGetCredentialsBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("[GET /github/credentials/{id}] GetCredentials", response, response.Code()) + } +} + +// NewGetCredentialsOK creates a GetCredentialsOK with default headers values +func NewGetCredentialsOK() *GetCredentialsOK { + return &GetCredentialsOK{} +} + +/* +GetCredentialsOK describes a response with status code 200, with default header values. + +GithubCredentials +*/ +type GetCredentialsOK struct { + Payload garm_params.GithubCredentials +} + +// IsSuccess returns true when this get credentials o k response has a 2xx status code +func (o *GetCredentialsOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this get credentials o k response has a 3xx status code +func (o *GetCredentialsOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this get credentials o k response has a 4xx status code +func (o *GetCredentialsOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this get credentials o k response has a 5xx status code +func (o *GetCredentialsOK) IsServerError() bool { + return false +} + +// IsCode returns true when this get credentials o k response a status code equal to that given +func (o *GetCredentialsOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the get credentials o k response +func (o *GetCredentialsOK) Code() int { + return 200 +} + +func (o *GetCredentialsOK) Error() string { + return fmt.Sprintf("[GET /github/credentials/{id}][%d] getCredentialsOK %+v", 200, o.Payload) +} + +func (o *GetCredentialsOK) String() string { + return fmt.Sprintf("[GET /github/credentials/{id}][%d] getCredentialsOK %+v", 200, o.Payload) +} + +func (o *GetCredentialsOK) GetPayload() garm_params.GithubCredentials { + return o.Payload +} + +func (o *GetCredentialsOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewGetCredentialsBadRequest creates a GetCredentialsBadRequest with default headers values +func NewGetCredentialsBadRequest() *GetCredentialsBadRequest { + return &GetCredentialsBadRequest{} +} + +/* +GetCredentialsBadRequest describes a response with status code 400, with default header values. + +APIErrorResponse +*/ +type GetCredentialsBadRequest struct { + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this get credentials bad request response has a 2xx status code +func (o *GetCredentialsBadRequest) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this get credentials bad request response has a 3xx status code +func (o *GetCredentialsBadRequest) IsRedirect() bool { + return false +} + +// IsClientError returns true when this get credentials bad request response has a 4xx status code +func (o *GetCredentialsBadRequest) IsClientError() bool { + return true +} + +// IsServerError returns true when this get credentials bad request response has a 5xx status code +func (o *GetCredentialsBadRequest) IsServerError() bool { + return false +} + +// IsCode returns true when this get credentials bad request response a status code equal to that given +func (o *GetCredentialsBadRequest) IsCode(code int) bool { + return code == 400 +} + +// Code gets the status code for the get credentials bad request response +func (o *GetCredentialsBadRequest) Code() int { + return 400 +} + +func (o *GetCredentialsBadRequest) Error() string { + return fmt.Sprintf("[GET /github/credentials/{id}][%d] getCredentialsBadRequest %+v", 400, o.Payload) +} + +func (o *GetCredentialsBadRequest) String() string { + return fmt.Sprintf("[GET /github/credentials/{id}][%d] getCredentialsBadRequest %+v", 400, o.Payload) +} + +func (o *GetCredentialsBadRequest) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *GetCredentialsBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/client/credentials/list_credentials_responses.go b/client/credentials/list_credentials_responses.go index 2d4c2f9a..ac763e44 100644 --- a/client/credentials/list_credentials_responses.go +++ b/client/credentials/list_credentials_responses.go @@ -37,7 +37,7 @@ func (o *ListCredentialsReader) ReadResponse(response runtime.ClientResponse, co } return nil, result default: - return nil, runtime.NewAPIError("[GET /credentials] ListCredentials", response, response.Code()) + return nil, runtime.NewAPIError("[GET /github/credentials] ListCredentials", response, response.Code()) } } @@ -86,11 +86,11 @@ func (o *ListCredentialsOK) Code() int { } func (o *ListCredentialsOK) Error() string { - return fmt.Sprintf("[GET /credentials][%d] listCredentialsOK %+v", 200, o.Payload) + return fmt.Sprintf("[GET /github/credentials][%d] listCredentialsOK %+v", 200, o.Payload) } func (o *ListCredentialsOK) String() string { - return fmt.Sprintf("[GET /credentials][%d] listCredentialsOK %+v", 200, o.Payload) + return fmt.Sprintf("[GET /github/credentials][%d] listCredentialsOK %+v", 200, o.Payload) } func (o *ListCredentialsOK) GetPayload() garm_params.Credentials { @@ -152,11 +152,11 @@ func (o *ListCredentialsBadRequest) Code() int { } func (o *ListCredentialsBadRequest) Error() string { - return fmt.Sprintf("[GET /credentials][%d] listCredentialsBadRequest %+v", 400, o.Payload) + return fmt.Sprintf("[GET /github/credentials][%d] listCredentialsBadRequest %+v", 400, o.Payload) } func (o *ListCredentialsBadRequest) String() string { - return fmt.Sprintf("[GET /credentials][%d] listCredentialsBadRequest %+v", 400, o.Payload) + return fmt.Sprintf("[GET /github/credentials][%d] listCredentialsBadRequest %+v", 400, o.Payload) } func (o *ListCredentialsBadRequest) GetPayload() apiserver_params.APIErrorResponse { diff --git a/client/credentials/update_credentials_parameters.go b/client/credentials/update_credentials_parameters.go new file mode 100644 index 00000000..bba26c95 --- /dev/null +++ b/client/credentials/update_credentials_parameters.go @@ -0,0 +1,174 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + + garm_params "github.com/cloudbase/garm/params" +) + +// NewUpdateCredentialsParams creates a new UpdateCredentialsParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewUpdateCredentialsParams() *UpdateCredentialsParams { + return &UpdateCredentialsParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewUpdateCredentialsParamsWithTimeout creates a new UpdateCredentialsParams object +// with the ability to set a timeout on a request. +func NewUpdateCredentialsParamsWithTimeout(timeout time.Duration) *UpdateCredentialsParams { + return &UpdateCredentialsParams{ + timeout: timeout, + } +} + +// NewUpdateCredentialsParamsWithContext creates a new UpdateCredentialsParams object +// with the ability to set a context for a request. +func NewUpdateCredentialsParamsWithContext(ctx context.Context) *UpdateCredentialsParams { + return &UpdateCredentialsParams{ + Context: ctx, + } +} + +// NewUpdateCredentialsParamsWithHTTPClient creates a new UpdateCredentialsParams object +// with the ability to set a custom HTTPClient for a request. +func NewUpdateCredentialsParamsWithHTTPClient(client *http.Client) *UpdateCredentialsParams { + return &UpdateCredentialsParams{ + HTTPClient: client, + } +} + +/* +UpdateCredentialsParams contains all the parameters to send to the API endpoint + + for the update credentials operation. + + Typically these are written to a http.Request. +*/ +type UpdateCredentialsParams struct { + + /* Body. + + Parameters used when updating a GitHub credential. + */ + Body garm_params.UpdateGithubCredentialsParams + + /* ID. + + ID of the GitHub credential. + */ + ID int64 + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the update credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *UpdateCredentialsParams) WithDefaults() *UpdateCredentialsParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the update credentials params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *UpdateCredentialsParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the update credentials params +func (o *UpdateCredentialsParams) WithTimeout(timeout time.Duration) *UpdateCredentialsParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the update credentials params +func (o *UpdateCredentialsParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the update credentials params +func (o *UpdateCredentialsParams) WithContext(ctx context.Context) *UpdateCredentialsParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the update credentials params +func (o *UpdateCredentialsParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the update credentials params +func (o *UpdateCredentialsParams) WithHTTPClient(client *http.Client) *UpdateCredentialsParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the update credentials params +func (o *UpdateCredentialsParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithBody adds the body to the update credentials params +func (o *UpdateCredentialsParams) WithBody(body garm_params.UpdateGithubCredentialsParams) *UpdateCredentialsParams { + o.SetBody(body) + return o +} + +// SetBody adds the body to the update credentials params +func (o *UpdateCredentialsParams) SetBody(body garm_params.UpdateGithubCredentialsParams) { + o.Body = body +} + +// WithID adds the id to the update credentials params +func (o *UpdateCredentialsParams) WithID(id int64) *UpdateCredentialsParams { + o.SetID(id) + return o +} + +// SetID adds the id to the update credentials params +func (o *UpdateCredentialsParams) SetID(id int64) { + o.ID = id +} + +// WriteToRequest writes these params to a swagger request +func (o *UpdateCredentialsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + if err := r.SetBodyParam(o.Body); err != nil { + return err + } + + // path param id + if err := r.SetPathParam("id", swag.FormatInt64(o.ID)); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/client/credentials/update_credentials_responses.go b/client/credentials/update_credentials_responses.go new file mode 100644 index 00000000..1d1d98cd --- /dev/null +++ b/client/credentials/update_credentials_responses.go @@ -0,0 +1,174 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package credentials + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + apiserver_params "github.com/cloudbase/garm/apiserver/params" + garm_params "github.com/cloudbase/garm/params" +) + +// UpdateCredentialsReader is a Reader for the UpdateCredentials structure. +type UpdateCredentialsReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *UpdateCredentialsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewUpdateCredentialsOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewUpdateCredentialsBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("[PUT /github/credentials/{id}] UpdateCredentials", response, response.Code()) + } +} + +// NewUpdateCredentialsOK creates a UpdateCredentialsOK with default headers values +func NewUpdateCredentialsOK() *UpdateCredentialsOK { + return &UpdateCredentialsOK{} +} + +/* +UpdateCredentialsOK describes a response with status code 200, with default header values. + +GithubCredentials +*/ +type UpdateCredentialsOK struct { + Payload garm_params.GithubCredentials +} + +// IsSuccess returns true when this update credentials o k response has a 2xx status code +func (o *UpdateCredentialsOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this update credentials o k response has a 3xx status code +func (o *UpdateCredentialsOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this update credentials o k response has a 4xx status code +func (o *UpdateCredentialsOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this update credentials o k response has a 5xx status code +func (o *UpdateCredentialsOK) IsServerError() bool { + return false +} + +// IsCode returns true when this update credentials o k response a status code equal to that given +func (o *UpdateCredentialsOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the update credentials o k response +func (o *UpdateCredentialsOK) Code() int { + return 200 +} + +func (o *UpdateCredentialsOK) Error() string { + return fmt.Sprintf("[PUT /github/credentials/{id}][%d] updateCredentialsOK %+v", 200, o.Payload) +} + +func (o *UpdateCredentialsOK) String() string { + return fmt.Sprintf("[PUT /github/credentials/{id}][%d] updateCredentialsOK %+v", 200, o.Payload) +} + +func (o *UpdateCredentialsOK) GetPayload() garm_params.GithubCredentials { + return o.Payload +} + +func (o *UpdateCredentialsOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewUpdateCredentialsBadRequest creates a UpdateCredentialsBadRequest with default headers values +func NewUpdateCredentialsBadRequest() *UpdateCredentialsBadRequest { + return &UpdateCredentialsBadRequest{} +} + +/* +UpdateCredentialsBadRequest describes a response with status code 400, with default header values. + +APIErrorResponse +*/ +type UpdateCredentialsBadRequest struct { + Payload apiserver_params.APIErrorResponse +} + +// IsSuccess returns true when this update credentials bad request response has a 2xx status code +func (o *UpdateCredentialsBadRequest) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this update credentials bad request response has a 3xx status code +func (o *UpdateCredentialsBadRequest) IsRedirect() bool { + return false +} + +// IsClientError returns true when this update credentials bad request response has a 4xx status code +func (o *UpdateCredentialsBadRequest) IsClientError() bool { + return true +} + +// IsServerError returns true when this update credentials bad request response has a 5xx status code +func (o *UpdateCredentialsBadRequest) IsServerError() bool { + return false +} + +// IsCode returns true when this update credentials bad request response a status code equal to that given +func (o *UpdateCredentialsBadRequest) IsCode(code int) bool { + return code == 400 +} + +// Code gets the status code for the update credentials bad request response +func (o *UpdateCredentialsBadRequest) Code() int { + return 400 +} + +func (o *UpdateCredentialsBadRequest) Error() string { + return fmt.Sprintf("[PUT /github/credentials/{id}][%d] updateCredentialsBadRequest %+v", 400, o.Payload) +} + +func (o *UpdateCredentialsBadRequest) String() string { + return fmt.Sprintf("[PUT /github/credentials/{id}][%d] updateCredentialsBadRequest %+v", 400, o.Payload) +} + +func (o *UpdateCredentialsBadRequest) GetPayload() apiserver_params.APIErrorResponse { + return o.Payload +} + +func (o *UpdateCredentialsBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/cmd/garm-cli/cmd/credentials.go b/cmd/garm-cli/cmd/credentials.go deleted file mode 100644 index e5ed7b5a..00000000 --- a/cmd/garm-cli/cmd/credentials.go +++ /dev/null @@ -1,76 +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 cmd - -import ( - "fmt" - - "github.com/jedib0t/go-pretty/v6/table" - "github.com/spf13/cobra" - - apiClientCreds "github.com/cloudbase/garm/client/credentials" - "github.com/cloudbase/garm/params" -) - -// credentialsCmd represents the credentials command -var credentialsCmd = &cobra.Command{ - Use: "credentials", - Aliases: []string{"creds"}, - Short: "List configured credentials", - Long: `List all available credentials configured in the service -config file. - -Currently, github personal tokens are configured statically in the config file -of the garm service. This command lists the names of those credentials, -which in turn can be used to define pools of runners within repositories.`, - Run: nil, -} - -func init() { - credentialsCmd.AddCommand( - &cobra.Command{ - Use: "list", - Aliases: []string{"ls"}, - Short: "List configured github credentials", - Long: `List the names of the github personal access tokens available to the garm.`, - SilenceUsage: true, - RunE: func(_ *cobra.Command, _ []string) error { - if needsInit { - return errNeedsInitError - } - - listCredsReq := apiClientCreds.NewListCredentialsParams() - response, err := apiCli.Credentials.ListCredentials(listCredsReq, authToken) - if err != nil { - return err - } - formatGithubCredentials(response.Payload) - return nil - }, - }) - - rootCmd.AddCommand(credentialsCmd) -} - -func formatGithubCredentials(creds []params.GithubCredentials) { - t := table.NewWriter() - header := table.Row{"Name", "Description", "Base URL", "API URL", "Upload URL", "Type"} - t.AppendHeader(header) - for _, val := range creds { - t.AppendRow(table.Row{val.Name, val.Description, val.BaseURL, val.APIBaseURL, val.UploadBaseURL, val.AuthType}) - t.AppendSeparator() - } - fmt.Println(t.Render()) -} diff --git a/cmd/garm-cli/cmd/github_credentials.go b/cmd/garm-cli/cmd/github_credentials.go new file mode 100644 index 00000000..d328f5ee --- /dev/null +++ b/cmd/garm-cli/cmd/github_credentials.go @@ -0,0 +1,388 @@ +// 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 cmd + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "os" + "strconv" + + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" + + apiClientCreds "github.com/cloudbase/garm/client/credentials" + "github.com/cloudbase/garm/params" +) + +var ( + credentialsName string + credentialsDescription string + credentialsOAuthToken string + credentialsAppInstallationID int64 + credentialsAppID int64 + credentialsPrivateKeyPath string + credentialsType string + credentialsEndpoint string +) + +// credentialsCmd represents the credentials command +var credentialsCmd = &cobra.Command{ + Use: "credentials", + Aliases: []string{"creds"}, + Short: "List configured credentials", + Long: `List all available github credentials. + +This command is an alias for the garm-cli github credentials command.`, + Run: nil, +} + +// githubCredentialsCmd represents the github credentials command +var githubCredentialsCmd = &cobra.Command{ + Use: "credentials", + Aliases: []string{"creds"}, + Short: "Manage github credentials", + Long: `Manage GitHub credentials stored in GARM. + +This command allows you to add, update, list and delete GitHub credentials.`, + Run: nil, +} + +var githubCredentialsListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List configured github credentials", + Long: `List the names of the github personal access tokens available to the garm.`, + SilenceUsage: true, + RunE: func(_ *cobra.Command, _ []string) error { + if needsInit { + return errNeedsInitError + } + + listCredsReq := apiClientCreds.NewListCredentialsParams() + response, err := apiCli.Credentials.ListCredentials(listCredsReq, authToken) + if err != nil { + return err + } + formatGithubCredentials(response.Payload) + return nil + }, +} + +var githubCredentialsShowCmd = &cobra.Command{ + Use: "show", + Aliases: []string{"get"}, + Short: "Show details of a configured github credential", + Long: `Show the details of a configured github credential.`, + SilenceUsage: true, + RunE: func(_ *cobra.Command, args []string) error { + if needsInit { + return errNeedsInitError + } + + if len(args) < 1 { + return fmt.Errorf("missing required argument: credential ID") + } + + credID, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return fmt.Errorf("invalid credential ID: %s", args[0]) + } + showCredsReq := apiClientCreds.NewGetCredentialsParams().WithID(credID) + response, err := apiCli.Credentials.GetCredentials(showCredsReq, authToken) + if err != nil { + return err + } + formatOneGithubCredential(response.Payload) + return nil + }, +} + +var githubCredentialsUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update a github credential", + Long: "Update a github credential", + SilenceUsage: true, + RunE: func(_ *cobra.Command, args []string) error { + if needsInit { + return errNeedsInitError + } + + if len(args) < 1 { + return fmt.Errorf("missing required argument: credential ID") + } + + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + + credID, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return fmt.Errorf("invalid credential ID: %s", args[0]) + } + + updateParams, err := parseCredentialsUpdateParams() + if err != nil { + return err + } + + updateCredsReq := apiClientCreds.NewUpdateCredentialsParams().WithID(credID) + updateCredsReq.Body = updateParams + + response, err := apiCli.Credentials.UpdateCredentials(updateCredsReq, authToken) + if err != nil { + return err + } + formatOneGithubCredential(response.Payload) + return nil + }, +} + +var githubCredentialsDeleteCmd = &cobra.Command{ + Use: "delete", + Aliases: []string{"remove", "rm"}, + Short: "Delete a github credential", + Long: "Delete a github credential", + SilenceUsage: true, + RunE: func(_ *cobra.Command, args []string) error { + if needsInit { + return errNeedsInitError + } + + if len(args) < 1 { + return fmt.Errorf("missing required argument: credential ID") + } + + if len(args) > 1 { + return fmt.Errorf("too many arguments") + } + + credID, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return fmt.Errorf("invalid credential ID: %s", args[0]) + } + + deleteCredsReq := apiClientCreds.NewDeleteCredentialsParams().WithID(credID) + if err := apiCli.Credentials.DeleteCredentials(deleteCredsReq, authToken); err != nil { + return err + } + return nil + }, +} + +var githubCredentialsAddCmd = &cobra.Command{ + Use: "add", + Short: "Add a github credential", + Long: "Add a github credential", + SilenceUsage: true, + RunE: func(_ *cobra.Command, args []string) error { + if needsInit { + return errNeedsInitError + } + + if len(args) > 0 { + return fmt.Errorf("too many arguments") + } + + addParams, err := parseCredentialsAddParams() + if err != nil { + return err + } + + addCredsReq := apiClientCreds.NewCreateCredentialsParams() + addCredsReq.Body = addParams + + response, err := apiCli.Credentials.CreateCredentials(addCredsReq, authToken) + if err != nil { + return err + } + formatOneGithubCredential(response.Payload) + return nil + }, +} + +func init() { + githubCredentialsUpdateCmd.Flags().StringVar(&credentialsName, "name", "", "Name of the credential") + githubCredentialsUpdateCmd.Flags().StringVar(&credentialsDescription, "description", "", "Description of the credential") + githubCredentialsUpdateCmd.Flags().StringVar(&credentialsOAuthToken, "pat-oauth-token", "", "If the credential is a personal access token, the OAuth token") + githubCredentialsUpdateCmd.Flags().Int64Var(&credentialsAppInstallationID, "app-installation-id", 0, "If the credential is an app, the installation ID") + githubCredentialsUpdateCmd.Flags().Int64Var(&credentialsAppID, "app-id", 0, "If the credential is an app, the app ID") + githubCredentialsUpdateCmd.Flags().StringVar(&credentialsPrivateKeyPath, "private-key-path", "", "If the credential is an app, the path to the private key file") + + githubCredentialsUpdateCmd.MarkFlagsMutuallyExclusive("pat-oauth-token", "app-installation-id") + githubCredentialsUpdateCmd.MarkFlagsMutuallyExclusive("pat-oauth-token", "app-id") + githubCredentialsUpdateCmd.MarkFlagsMutuallyExclusive("pat-oauth-token", "private-key-path") + githubCredentialsUpdateCmd.MarkFlagsRequiredTogether("app-installation-id", "app-id", "private-key-path") + + githubCredentialsAddCmd.Flags().StringVar(&credentialsName, "name", "", "Name of the credential") + githubCredentialsAddCmd.Flags().StringVar(&credentialsDescription, "description", "", "Description of the credential") + githubCredentialsAddCmd.Flags().StringVar(&credentialsOAuthToken, "pat-oauth-token", "", "If the credential is a personal access token, the OAuth token") + githubCredentialsAddCmd.Flags().Int64Var(&credentialsAppInstallationID, "app-installation-id", 0, "If the credential is an app, the installation ID") + githubCredentialsAddCmd.Flags().Int64Var(&credentialsAppID, "app-id", 0, "If the credential is an app, the app ID") + githubCredentialsAddCmd.Flags().StringVar(&credentialsPrivateKeyPath, "private-key-path", "", "If the credential is an app, the path to the private key file") + githubCredentialsAddCmd.Flags().StringVar(&credentialsType, "auth-type", "", "The type of the credential") + githubCredentialsAddCmd.Flags().StringVar(&credentialsEndpoint, "endpoint", "", "The endpoint to associate the credential with") + + githubCredentialsAddCmd.MarkFlagsMutuallyExclusive("pat-oauth-token", "app-installation-id") + githubCredentialsAddCmd.MarkFlagsMutuallyExclusive("pat-oauth-token", "app-id") + githubCredentialsAddCmd.MarkFlagsMutuallyExclusive("pat-oauth-token", "private-key-path") + githubCredentialsAddCmd.MarkFlagsRequiredTogether("app-installation-id", "app-id", "private-key-path") + + githubCredentialsAddCmd.MarkFlagRequired("name") + githubCredentialsAddCmd.MarkFlagRequired("auth-type") + githubCredentialsAddCmd.MarkFlagRequired("description") + githubCredentialsAddCmd.MarkFlagRequired("endpoint") + + githubCredentialsCmd.AddCommand( + githubCredentialsListCmd, + githubCredentialsShowCmd, + githubCredentialsUpdateCmd, + githubCredentialsDeleteCmd, + githubCredentialsAddCmd, + ) + githubCmd.AddCommand(githubCredentialsCmd) + + credentialsCmd.AddCommand(githubCredentialsListCmd) + rootCmd.AddCommand(credentialsCmd) +} + +func parsePrivateKeyFromPath(path string) ([]byte, error) { + if _, err := os.Stat(path); err != nil { + return nil, fmt.Errorf("private key file not found: %s", credentialsPrivateKeyPath) + } + keyContents, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read private key file: %w", err) + } + pemBlock, _ := pem.Decode(keyContents) + if pemBlock == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + if _, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + return keyContents, nil +} + +func parseCredentialsAddParams() (ret params.CreateGithubCredentialsParams, err error) { + ret.Name = credentialsName + ret.Description = credentialsDescription + ret.AuthType = params.GithubAuthType(credentialsType) + ret.Endpoint = credentialsEndpoint + switch ret.AuthType { + case params.GithubAuthTypePAT: + ret.PAT.OAuth2Token = credentialsOAuthToken + case params.GithubAuthTypeApp: + ret.App.InstallationID = credentialsAppInstallationID + ret.App.AppID = credentialsAppID + keyContents, err := parsePrivateKeyFromPath(credentialsPrivateKeyPath) + if err != nil { + return params.CreateGithubCredentialsParams{}, err + } + ret.App.PrivateKeyBytes = keyContents + default: + return params.CreateGithubCredentialsParams{}, fmt.Errorf("invalid auth type: %s (supported are: app, pat)", credentialsType) + } + + return ret, nil +} + +func parseCredentialsUpdateParams() (params.UpdateGithubCredentialsParams, error) { + var updateParams params.UpdateGithubCredentialsParams + + if credentialsName != "" { + updateParams.Name = &credentialsName + } + + if credentialsDescription != "" { + updateParams.Description = &credentialsDescription + } + + if credentialsOAuthToken != "" { + updateParams.PAT.OAuth2Token = credentialsOAuthToken + } + + if credentialsAppInstallationID != 0 { + updateParams.App.InstallationID = credentialsAppInstallationID + } + + if credentialsAppID != 0 { + updateParams.App.AppID = credentialsAppID + } + + if credentialsPrivateKeyPath != "" { + keyContents, err := parsePrivateKeyFromPath(credentialsPrivateKeyPath) + if err != nil { + return params.UpdateGithubCredentialsParams{}, err + } + updateParams.App.PrivateKeyBytes = keyContents + } + + return updateParams, nil +} + +func formatGithubCredentials(creds []params.GithubCredentials) { + t := table.NewWriter() + header := table.Row{"ID", "Name", "Description", "Base URL", "API URL", "Upload URL", "Type"} + t.AppendHeader(header) + for _, val := range creds { + t.AppendRow(table.Row{val.ID, val.Name, val.Description, val.BaseURL, val.APIBaseURL, val.UploadBaseURL, val.AuthType}) + t.AppendSeparator() + } + fmt.Println(t.Render()) +} + +func formatOneGithubCredential(cred params.GithubCredentials) { + t := table.NewWriter() + header := table.Row{"Field", "Value"} + t.AppendHeader(header) + + t.AppendRow(table.Row{"ID", cred.ID}) + t.AppendRow(table.Row{"Name", cred.Name}) + t.AppendRow(table.Row{"Description", cred.Description}) + t.AppendRow(table.Row{"Base URL", cred.BaseURL}) + t.AppendRow(table.Row{"API URL", cred.APIBaseURL}) + t.AppendRow(table.Row{"Upload URL", cred.UploadBaseURL}) + t.AppendRow(table.Row{"Type", cred.AuthType}) + t.AppendRow(table.Row{"Endpoint", cred.Endpoint.Name}) + + if len(cred.Repositories) > 0 { + t.AppendRow(table.Row{"", ""}) + for _, repo := range cred.Repositories { + t.AppendRow(table.Row{"Repositories", repo.String()}) + } + } + + if len(cred.Organizations) > 0 { + t.AppendRow(table.Row{"", ""}) + for _, org := range cred.Organizations { + t.AppendRow(table.Row{"Organizations", org.Name}) + } + } + + if len(cred.Enterprises) > 0 { + t.AppendRow(table.Row{"", ""}) + for _, ent := range cred.Enterprises { + t.AppendRow(table.Row{"Enterprises", ent.Name}) + } + } + + t.SetColumnConfigs([]table.ColumnConfig{ + {Number: 1, AutoMerge: true}, + {Number: 2, AutoMerge: false, WidthMax: 100}, + }) + fmt.Println(t.Render()) +} diff --git a/cmd/garm-cli/cmd/github_endpoints.go b/cmd/garm-cli/cmd/github_endpoints.go index a841d3dc..b12ba2e7 100644 --- a/cmd/garm-cli/cmd/github_endpoints.go +++ b/cmd/garm-cli/cmd/github_endpoints.go @@ -199,11 +199,13 @@ func init() { githubEndpointUpdateCmd.Flags().StringVar(&endpointAPIBaseURL, "api-base-url", "", "API Base URL of the GitHub endpoint") githubEndpointUpdateCmd.Flags().StringVar(&endpointCACertPath, "ca-cert-path", "", "CA Cert Path of the GitHub endpoint") - githubEndpointCmd.AddCommand(githubEndpointListCmd) - githubEndpointCmd.AddCommand(githubEndpointShowCmd) - githubEndpointCmd.AddCommand(githubEndpointCreateCmd) - githubEndpointCmd.AddCommand(githubEndpointDeleteCmd) - githubEndpointCmd.AddCommand(githubEndpointUpdateCmd) + githubEndpointCmd.AddCommand( + githubEndpointListCmd, + githubEndpointShowCmd, + githubEndpointCreateCmd, + githubEndpointDeleteCmd, + githubEndpointUpdateCmd, + ) githubCmd.AddCommand(githubEndpointCmd) } diff --git a/database/common/common.go b/database/common/common.go index 8f901ab7..5af12780 100644 --- a/database/common/common.go +++ b/database/common/common.go @@ -29,7 +29,7 @@ type GithubEndpointStore interface { } type GithubCredentialsStore interface { - CreateGithubCredentials(ctx context.Context, endpointName string, param params.CreateGithubCredentialsParams) (params.GithubCredentials, error) + CreateGithubCredentials(ctx context.Context, param params.CreateGithubCredentialsParams) (params.GithubCredentials, error) GetGithubCredentials(ctx context.Context, id uint, detailed bool) (params.GithubCredentials, error) GetGithubCredentialsByName(ctx context.Context, name string, detailed bool) (params.GithubCredentials, error) ListGithubCredentials(ctx context.Context) ([]params.GithubCredentials, error) diff --git a/database/common/mocks/Store.go b/database/common/mocks/Store.go index f8877ef7..9310e5c4 100644 --- a/database/common/mocks/Store.go +++ b/database/common/mocks/Store.go @@ -134,9 +134,9 @@ func (_m *Store) CreateEntityPool(ctx context.Context, entity params.GithubEntit return r0, r1 } -// CreateGithubCredentials provides a mock function with given fields: ctx, endpointName, param -func (_m *Store) CreateGithubCredentials(ctx context.Context, endpointName string, param params.CreateGithubCredentialsParams) (params.GithubCredentials, error) { - ret := _m.Called(ctx, endpointName, param) +// CreateGithubCredentials provides a mock function with given fields: ctx, param +func (_m *Store) CreateGithubCredentials(ctx context.Context, param params.CreateGithubCredentialsParams) (params.GithubCredentials, error) { + ret := _m.Called(ctx, param) if len(ret) == 0 { panic("no return value specified for CreateGithubCredentials") @@ -144,17 +144,17 @@ func (_m *Store) CreateGithubCredentials(ctx context.Context, endpointName strin var r0 params.GithubCredentials var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, params.CreateGithubCredentialsParams) (params.GithubCredentials, error)); ok { - return rf(ctx, endpointName, param) + if rf, ok := ret.Get(0).(func(context.Context, params.CreateGithubCredentialsParams) (params.GithubCredentials, error)); ok { + return rf(ctx, param) } - if rf, ok := ret.Get(0).(func(context.Context, string, params.CreateGithubCredentialsParams) params.GithubCredentials); ok { - r0 = rf(ctx, endpointName, param) + if rf, ok := ret.Get(0).(func(context.Context, params.CreateGithubCredentialsParams) params.GithubCredentials); ok { + r0 = rf(ctx, param) } else { r0 = ret.Get(0).(params.GithubCredentials) } - if rf, ok := ret.Get(1).(func(context.Context, string, params.CreateGithubCredentialsParams) error); ok { - r1 = rf(ctx, endpointName, param) + if rf, ok := ret.Get(1).(func(context.Context, params.CreateGithubCredentialsParams) error); ok { + r1 = rf(ctx, param) } else { r1 = ret.Error(1) } diff --git a/database/sql/github.go b/database/sql/github.go index 8a3bb50c..252f8f6f 100644 --- a/database/sql/github.go +++ b/database/sql/github.go @@ -257,15 +257,18 @@ func (s *sqlDatabase) DeleteGithubEndpoint(_ context.Context, name string) error return nil } -func (s *sqlDatabase) CreateGithubCredentials(ctx context.Context, endpointName string, param params.CreateGithubCredentialsParams) (params.GithubCredentials, error) { +func (s *sqlDatabase) CreateGithubCredentials(ctx context.Context, param params.CreateGithubCredentialsParams) (params.GithubCredentials, error) { userID, err := getUIDFromContext(ctx) if err != nil { return params.GithubCredentials{}, errors.Wrap(err, "creating github credentials") } + if param.Endpoint == "" { + return params.GithubCredentials{}, errors.New("endpoint name is required") + } var creds GithubCredentials err = s.conn.Transaction(func(tx *gorm.DB) error { var endpoint GithubEndpoint - if err := tx.Where("name = ?", endpointName).First(&endpoint).Error; err != nil { + if err := tx.Where("name = ?", param.Endpoint).First(&endpoint).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.Wrap(runnerErrors.ErrNotFound, "github endpoint not found") } diff --git a/database/sql/sql.go b/database/sql/sql.go index 2436dc3d..a15cfd71 100644 --- a/database/sql/sql.go +++ b/database/sql/sql.go @@ -297,6 +297,7 @@ func (s *sqlDatabase) migrateCredentialsToDB() (err error) { credParams := params.CreateGithubCredentialsParams{ Name: cred.Name, Description: cred.Description, + Endpoint: endpoint.Name, AuthType: params.GithubAuthType(cred.GetAuthType()), } switch credParams.AuthType { @@ -327,7 +328,7 @@ func (s *sqlDatabase) migrateCredentialsToDB() (err error) { } } - creds, err := s.CreateGithubCredentials(adminCtx, endpoint.Name, credParams) + creds, err := s.CreateGithubCredentials(adminCtx, credParams) if err != nil { return errors.Wrap(err, "creating github credentials") } diff --git a/internal/testing/testing.go b/internal/testing/testing.go index d599aca4..bb1f580a 100644 --- a/internal/testing/testing.go +++ b/internal/testing/testing.go @@ -80,11 +80,12 @@ func CreateTestGithubCredentials(ctx context.Context, credsName string, db commo Name: credsName, Description: "Test creds", AuthType: params.GithubAuthTypePAT, + Endpoint: endpoint.Name, PAT: params.GithubPAT{ OAuth2Token: "test-token", }, } - newCreds, err := db.CreateGithubCredentials(ctx, endpoint.Name, newCredsParams) + newCreds, err := db.CreateGithubCredentials(ctx, newCredsParams) if err != nil { s.Fatalf("failed to create database object (new-creds): %v", err) } diff --git a/params/params.go b/params/params.go index c28990d7..522dc954 100644 --- a/params/params.go +++ b/params/params.go @@ -437,6 +437,10 @@ func (r Repository) GetBalancerType() PoolBalancerType { return r.PoolBalancerType } +func (r Repository) String() string { + return fmt.Sprintf("%s/%s", r.Owner, r.Name) +} + // used by swagger client generated code type Repositories []Repository diff --git a/params/requests.go b/params/requests.go index ff7a3824..db7e7da2 100644 --- a/params/requests.go +++ b/params/requests.go @@ -21,7 +21,9 @@ import ( "fmt" "net/url" - "github.com/cloudbase/garm-provider-common/errors" + "github.com/pkg/errors" + + runnerErrors "github.com/cloudbase/garm-provider-common/errors" commonParams "github.com/cloudbase/garm-provider-common/params" ) @@ -47,24 +49,24 @@ type CreateRepoParams struct { func (c *CreateRepoParams) Validate() error { if c.Owner == "" { - return errors.NewBadRequestError("missing owner") + return runnerErrors.NewBadRequestError("missing owner") } if c.Name == "" { - return errors.NewBadRequestError("missing repo name") + return runnerErrors.NewBadRequestError("missing repo name") } if c.CredentialsName == "" { - return errors.NewBadRequestError("missing credentials name") + return runnerErrors.NewBadRequestError("missing credentials name") } if c.WebhookSecret == "" { - return errors.NewMissingSecretError("missing secret") + return runnerErrors.NewMissingSecretError("missing secret") } switch c.PoolBalancerType { case PoolBalancerTypeRoundRobin, PoolBalancerTypePack, PoolBalancerTypeNone: default: - return errors.NewBadRequestError("invalid pool balancer type") + return runnerErrors.NewBadRequestError("invalid pool balancer type") } return nil @@ -79,20 +81,20 @@ type CreateOrgParams struct { func (c *CreateOrgParams) Validate() error { if c.Name == "" { - return errors.NewBadRequestError("missing org name") + return runnerErrors.NewBadRequestError("missing org name") } if c.CredentialsName == "" { - return errors.NewBadRequestError("missing credentials name") + return runnerErrors.NewBadRequestError("missing credentials name") } if c.WebhookSecret == "" { - return errors.NewMissingSecretError("missing secret") + return runnerErrors.NewMissingSecretError("missing secret") } switch c.PoolBalancerType { case PoolBalancerTypeRoundRobin, PoolBalancerTypePack, PoolBalancerTypeNone: default: - return errors.NewBadRequestError("invalid pool balancer type") + return runnerErrors.NewBadRequestError("invalid pool balancer type") } return nil } @@ -106,19 +108,19 @@ type CreateEnterpriseParams struct { func (c *CreateEnterpriseParams) Validate() error { if c.Name == "" { - return errors.NewBadRequestError("missing enterprise name") + return runnerErrors.NewBadRequestError("missing enterprise name") } if c.CredentialsName == "" { - return errors.NewBadRequestError("missing credentials name") + return runnerErrors.NewBadRequestError("missing credentials name") } if c.WebhookSecret == "" { - return errors.NewMissingSecretError("missing secret") + return runnerErrors.NewMissingSecretError("missing secret") } switch c.PoolBalancerType { case PoolBalancerTypeRoundRobin, PoolBalancerTypePack, PoolBalancerTypeNone: default: - return errors.NewBadRequestError("invalid pool balancer type") + return runnerErrors.NewBadRequestError("invalid pool balancer type") } return nil } @@ -256,7 +258,7 @@ type PasswordLoginParams struct { // Validate checks if the username and password are set func (p PasswordLoginParams) Validate() error { if p.Username == "" || p.Password == "" { - return errors.ErrUnauthorized + return runnerErrors.ErrUnauthorized } return nil } @@ -284,56 +286,56 @@ type CreateGithubEndpointParams struct { func (c CreateGithubEndpointParams) Validate() error { if c.APIBaseURL == "" { - return errors.NewBadRequestError("missing api_base_url") + return runnerErrors.NewBadRequestError("missing api_base_url") } url, err := url.Parse(c.APIBaseURL) if err != nil || url.Scheme == "" || url.Host == "" { - return errors.NewBadRequestError("invalid api_base_url") + return runnerErrors.NewBadRequestError("invalid api_base_url") } switch url.Scheme { case httpsScheme, httpScheme: default: - return errors.NewBadRequestError("invalid api_base_url") + return runnerErrors.NewBadRequestError("invalid api_base_url") } if c.UploadBaseURL == "" { - return errors.NewBadRequestError("missing upload_base_url") + return runnerErrors.NewBadRequestError("missing upload_base_url") } url, err = url.Parse(c.UploadBaseURL) if err != nil || url.Scheme == "" || url.Host == "" { - return errors.NewBadRequestError("invalid upload_base_url") + return runnerErrors.NewBadRequestError("invalid upload_base_url") } switch url.Scheme { case httpsScheme, httpScheme: default: - return errors.NewBadRequestError("invalid api_base_url") + return runnerErrors.NewBadRequestError("invalid api_base_url") } if c.BaseURL == "" { - return errors.NewBadRequestError("missing base_url") + return runnerErrors.NewBadRequestError("missing base_url") } url, err = url.Parse(c.BaseURL) if err != nil || url.Scheme == "" || url.Host == "" { - return errors.NewBadRequestError("invalid base_url") + return runnerErrors.NewBadRequestError("invalid base_url") } switch url.Scheme { case httpsScheme, httpScheme: default: - return errors.NewBadRequestError("invalid api_base_url") + return runnerErrors.NewBadRequestError("invalid api_base_url") } if c.CACertBundle != nil { block, _ := pem.Decode(c.CACertBundle) if block == nil { - return errors.NewBadRequestError("invalid ca_cert_bundle") + return runnerErrors.NewBadRequestError("invalid ca_cert_bundle") } if _, err := x509.ParseCertificates(block.Bytes); err != nil { - return errors.NewBadRequestError("invalid ca_cert_bundle") + return runnerErrors.NewBadRequestError("invalid ca_cert_bundle") } } @@ -352,46 +354,46 @@ func (u UpdateGithubEndpointParams) Validate() error { if u.APIBaseURL != nil { url, err := url.Parse(*u.APIBaseURL) if err != nil || url.Scheme == "" || url.Host == "" { - return errors.NewBadRequestError("invalid api_base_url") + return runnerErrors.NewBadRequestError("invalid api_base_url") } switch url.Scheme { case httpsScheme, httpScheme: default: - return errors.NewBadRequestError("invalid api_base_url") + return runnerErrors.NewBadRequestError("invalid api_base_url") } } if u.UploadBaseURL != nil { url, err := url.Parse(*u.UploadBaseURL) if err != nil || url.Scheme == "" || url.Host == "" { - return errors.NewBadRequestError("invalid upload_base_url") + return runnerErrors.NewBadRequestError("invalid upload_base_url") } switch url.Scheme { case httpsScheme, httpScheme: default: - return errors.NewBadRequestError("invalid api_base_url") + return runnerErrors.NewBadRequestError("invalid api_base_url") } } if u.BaseURL != nil { url, err := url.Parse(*u.BaseURL) if err != nil || url.Scheme == "" || url.Host == "" { - return errors.NewBadRequestError("invalid base_url") + return runnerErrors.NewBadRequestError("invalid base_url") } switch url.Scheme { case httpsScheme, httpScheme: default: - return errors.NewBadRequestError("invalid api_base_url") + return runnerErrors.NewBadRequestError("invalid api_base_url") } } if u.CACertBundle != nil { block, _ := pem.Decode(u.CACertBundle) if block == nil { - return errors.NewBadRequestError("invalid ca_cert_bundle") + return runnerErrors.NewBadRequestError("invalid ca_cert_bundle") } if _, err := x509.ParseCertificates(block.Bytes); err != nil { - return errors.NewBadRequestError("invalid ca_cert_bundle") + return runnerErrors.NewBadRequestError("invalid ca_cert_bundle") } } @@ -410,15 +412,15 @@ type GithubApp struct { func (g GithubApp) Validate() error { if g.AppID == 0 { - return errors.NewBadRequestError("missing app_id") + return runnerErrors.NewBadRequestError("missing app_id") } if g.InstallationID == 0 { - return errors.NewBadRequestError("missing installation_id") + return runnerErrors.NewBadRequestError("missing installation_id") } if len(g.PrivateKeyBytes) == 0 { - return errors.NewBadRequestError("missing private_key_bytes") + return runnerErrors.NewBadRequestError("missing private_key_bytes") } block, _ := pem.Decode(g.PrivateKeyBytes) @@ -434,14 +436,65 @@ func (g GithubApp) Validate() error { type CreateGithubCredentialsParams struct { Name string `json:"name"` Description string `json:"description"` + Endpoint string `json:"endpoint"` AuthType GithubAuthType `json:"auth_type"` PAT GithubPAT `json:"pat,omitempty"` App GithubApp `json:"app,omitempty"` } +func (c CreateGithubCredentialsParams) Validate() error { + if c.Name == "" { + return runnerErrors.NewBadRequestError("missing name") + } + + if c.Endpoint == "" { + return runnerErrors.NewBadRequestError("missing endpoint") + } + + switch c.AuthType { + case GithubAuthTypePAT, GithubAuthTypeApp: + default: + return runnerErrors.NewBadRequestError("invalid auth_type") + } + + if c.AuthType == GithubAuthTypePAT { + if c.PAT.OAuth2Token == "" { + return runnerErrors.NewBadRequestError("missing oauth2_token") + } + } + + if c.AuthType == GithubAuthTypeApp { + if err := c.App.Validate(); err != nil { + return errors.Wrap(err, "invalid app") + } + } + + return nil +} + type UpdateGithubCredentialsParams struct { Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` PAT *GithubPAT `json:"pat,omitempty"` App *GithubApp `json:"app,omitempty"` } + +func (u UpdateGithubCredentialsParams) Validate() error { + if u.PAT != nil && u.App != nil { + return runnerErrors.NewBadRequestError("cannot update both PAT and App") + } + + if u.PAT != nil { + if u.PAT.OAuth2Token == "" { + return runnerErrors.NewBadRequestError("missing oauth2_token") + } + } + + if u.App != nil { + if err := u.App.Validate(); err != nil { + return errors.Wrap(err, "invalid app") + } + } + + return nil +} diff --git a/runner/github_credentials.go b/runner/github_credentials.go new file mode 100644 index 00000000..fbf9d330 --- /dev/null +++ b/runner/github_credentials.go @@ -0,0 +1,83 @@ +package runner + +import ( + "context" + + "github.com/pkg/errors" + + runnerErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/auth" + "github.com/cloudbase/garm/params" +) + +func (r *Runner) ListCredentials(ctx context.Context) ([]params.GithubCredentials, error) { + if !auth.IsAdmin(ctx) { + return nil, runnerErrors.ErrUnauthorized + } + + creds, err := r.store.ListGithubCredentials(ctx) + if err != nil { + return nil, errors.Wrap(err, "fetching github credentials") + } + + return creds, nil +} + +func (r *Runner) CreateGithubCredentials(ctx context.Context, param params.CreateGithubCredentialsParams) (params.GithubCredentials, error) { + if !auth.IsAdmin(ctx) { + return params.GithubCredentials{}, runnerErrors.ErrUnauthorized + } + + if err := param.Validate(); err != nil { + return params.GithubCredentials{}, errors.Wrap(err, "failed to validate github credentials params") + } + + creds, err := r.store.CreateGithubCredentials(ctx, param) + if err != nil { + return params.GithubCredentials{}, errors.Wrap(err, "failed to create github credentials") + } + + return creds, nil +} + +func (r *Runner) GetGithubCredentials(ctx context.Context, id uint) (params.GithubCredentials, error) { + if !auth.IsAdmin(ctx) { + return params.GithubCredentials{}, runnerErrors.ErrUnauthorized + } + + creds, err := r.store.GetGithubCredentials(ctx, id, true) + if err != nil { + return params.GithubCredentials{}, errors.Wrap(err, "failed to get github credentials") + } + + return creds, nil +} + +func (r *Runner) DeleteGithubCredentials(ctx context.Context, id uint) error { + if !auth.IsAdmin(ctx) { + return runnerErrors.ErrUnauthorized + } + + if err := r.store.DeleteGithubCredentials(ctx, id); err != nil { + return errors.Wrap(err, "failed to delete github credentials") + } + + return nil +} + +func (r *Runner) UpdateGithubCredentials(ctx context.Context, id uint, param params.UpdateGithubCredentialsParams) (params.GithubCredentials, error) { + if !auth.IsAdmin(ctx) { + return params.GithubCredentials{}, runnerErrors.ErrUnauthorized + } + + if err := param.Validate(); err != nil { + return params.GithubCredentials{}, errors.Wrap(err, "failed to validate github credentials params") + } + + newCreds, err := r.store.UpdateGithubCredentials(ctx, id, param) + if err != nil { + return params.GithubCredentials{}, errors.Wrap(err, "failed to update github credentials") + } + + return newCreds, nil +} diff --git a/runner/endpoints.go b/runner/github_endpoints.go similarity index 100% rename from runner/endpoints.go rename to runner/github_endpoints.go diff --git a/runner/runner.go b/runner/runner.go index bc28a6c3..453980a0 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -395,19 +395,6 @@ func (r *Runner) GetControllerInfo(ctx context.Context) (params.ControllerInfo, }, nil } -func (r *Runner) ListCredentials(ctx context.Context) ([]params.GithubCredentials, error) { - if !auth.IsAdmin(ctx) { - return nil, runnerErrors.ErrUnauthorized - } - - creds, err := r.store.ListGithubCredentials(ctx) - if err != nil { - return nil, errors.Wrap(err, "fetching github credentials") - } - - return creds, nil -} - func (r *Runner) ListProviders(ctx context.Context) ([]params.Provider, error) { if !auth.IsAdmin(ctx) { return nil, runnerErrors.ErrUnauthorized From 208a4eea371b716c016731ff10e01d045f42bf08 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Fri, 19 Apr 2024 10:04:02 +0000 Subject: [PATCH 09/27] Ensure github endpoint Signed-off-by: Gabriel Adrian Samfira --- database/sql/sql.go | 38 ++++++++++++++++++++++--------------- internal/testing/testing.go | 16 +++++++++++++--- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/database/sql/sql.go b/database/sql/sql.go index a15cfd71..d61840d3 100644 --- a/database/sql/sql.go +++ b/database/sql/sql.go @@ -195,6 +195,25 @@ func (s *sqlDatabase) cascadeMigration() error { return nil } +func (s *sqlDatabase) ensureGithubEndpoint() error { + // Create the default Github endpoint. + createEndpointParams := params.CreateGithubEndpointParams{ + Name: "github.com", + Description: "The github.com endpoint", + APIBaseURL: appdefaults.GithubDefaultBaseURL, + BaseURL: appdefaults.DefaultGithubURL, + UploadBaseURL: appdefaults.GithubDefaultUploadBaseURL, + } + + if _, err := s.CreateGithubEndpoint(context.Background(), createEndpointParams); err != nil { + if !errors.Is(err, runnerErrors.ErrDuplicateEntity) { + return errors.Wrap(err, "creating default github endpoint") + } + } + + return nil +} + func (s *sqlDatabase) migrateCredentialsToDB() (err error) { s.conn.Exec("PRAGMA foreign_keys = OFF") defer s.conn.Exec("PRAGMA foreign_keys = ON") @@ -240,21 +259,6 @@ func (s *sqlDatabase) migrateCredentialsToDB() (err error) { } }() - // Create the default Github endpoint. - createEndpointParams := params.CreateGithubEndpointParams{ - Name: "github.com", - Description: "The github.com endpoint", - APIBaseURL: appdefaults.GithubDefaultBaseURL, - BaseURL: appdefaults.DefaultGithubURL, - UploadBaseURL: appdefaults.GithubDefaultUploadBaseURL, - } - - if _, err := s.CreateGithubEndpoint(adminCtx, createEndpointParams); err != nil { - if !errors.Is(err, runnerErrors.ErrDuplicateEntity) { - return errors.Wrap(err, "creating default github endpoint") - } - } - // Nothing to migrate. if len(s.cfg.MigrateCredentials) == 0 { return nil @@ -416,6 +420,10 @@ func (s *sqlDatabase) migrateDB() error { } s.conn.Exec("PRAGMA foreign_keys = ON") + if err := s.ensureGithubEndpoint(); err != nil { + return errors.Wrap(err, "ensuring github endpoint") + } + if needsCredentialMigration { if err := s.migrateCredentialsToDB(); err != nil { return errors.Wrap(err, "migrating credentials") diff --git a/internal/testing/testing.go b/internal/testing/testing.go index bb1f580a..adc9730b 100644 --- a/internal/testing/testing.go +++ b/internal/testing/testing.go @@ -68,11 +68,21 @@ func CreateDefaultGithubEndpoint(ctx context.Context, db common.Store, s *testin UploadBaseURL: appdefaults.GithubDefaultUploadBaseURL, BaseURL: appdefaults.DefaultGithubURL, } - endpoint, err := db.CreateGithubEndpoint(ctx, endpointParams) + + ep, err := db.GetGithubEndpoint(ctx, endpointParams.Name) if err != nil { - s.Fatalf("failed to create database object (github.com): %v", err) + if !errors.Is(err, runnerErrors.ErrNotFound) { + s.Fatalf("failed to get database object (github.com): %v", err) + } + ep, err = db.CreateGithubEndpoint(ctx, endpointParams) + if err != nil { + if !errors.Is(err, runnerErrors.ErrDuplicateEntity) { + s.Fatalf("failed to create database object (github.com): %v", err) + } + } } - return endpoint + + return ep } func CreateTestGithubCredentials(ctx context.Context, credsName string, db common.Store, s *testing.T, endpoint params.GithubEndpoint) params.GithubCredentials { From eb1456479b3ae70cd89bf5cff95481609e2920f0 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Fri, 19 Apr 2024 10:19:23 +0000 Subject: [PATCH 10/27] Deny deleting the default github.com endpoint Signed-off-by: Gabriel Adrian Samfira --- database/sql/github.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/database/sql/github.go b/database/sql/github.go index 252f8f6f..2f38a7de 100644 --- a/database/sql/github.go +++ b/database/sql/github.go @@ -214,6 +214,10 @@ func (s *sqlDatabase) DeleteGithubEndpoint(_ context.Context, name string) error return errors.Wrap(err, "fetching github endpoint") } + if endpoint.Name == "github.com" { + return errors.New("cannot delete default github endpoint") + } + var credsCount int64 if err := tx.Model(&GithubCredentials{}).Where("endpoint_name = ?", endpoint.Name).Count(&credsCount).Error; err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { From e8ea7117cee8b57a4c103bf81c2a0f42ab3c4b75 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Mon, 22 Apr 2024 14:10:51 +0000 Subject: [PATCH 11/27] Fix tests post-rebase Signed-off-by: Gabriel Adrian Samfira --- database/sql/enterprise_test.go | 4 ++-- database/sql/organizations_test.go | 4 ++-- database/sql/repositories_test.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/database/sql/enterprise_test.go b/database/sql/enterprise_test.go index d49b25bd..2a26c11d 100644 --- a/database/sql/enterprise_test.go +++ b/database/sql/enterprise_test.go @@ -216,8 +216,8 @@ func (s *EnterpriseTestSuite) TestCreateEnterpriseInvalidDBPassphrase() { func (s *EnterpriseTestSuite) TestCreateEnterpriseDBCreateErr() { s.Fixtures.SQLMock.ExpectBegin() s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). - WithArgs(s.Fixtures.Enterprises[0].CredentialsName). + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.Fixtures.Enterprises[0].CredentialsName, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}).AddRow(s.testCreds.ID, s.testCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). WithArgs(s.testCreds.Endpoint.Name). diff --git a/database/sql/organizations_test.go b/database/sql/organizations_test.go index 9954b978..ae880da7 100644 --- a/database/sql/organizations_test.go +++ b/database/sql/organizations_test.go @@ -217,8 +217,8 @@ func (s *OrgTestSuite) TestCreateOrganizationInvalidDBPassphrase() { func (s *OrgTestSuite) TestCreateOrganizationDBCreateErr() { s.Fixtures.SQLMock.ExpectBegin() s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). - WithArgs(s.Fixtures.Orgs[0].CredentialsName). + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.Fixtures.Orgs[0].CredentialsName, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.testCreds.ID, s.githubEndpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). diff --git a/database/sql/repositories_test.go b/database/sql/repositories_test.go index 3ff780a4..5a67abd1 100644 --- a/database/sql/repositories_test.go +++ b/database/sql/repositories_test.go @@ -233,8 +233,8 @@ func (s *RepoTestSuite) TestCreateRepositoryInvalidDBPassphrase() { func (s *RepoTestSuite) TestCreateRepositoryInvalidDBCreateErr() { s.Fixtures.SQLMock.ExpectBegin() s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT 1")). - WithArgs(s.Fixtures.Repos[0].CredentialsName). + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.Fixtures.Repos[0].CredentialsName, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.testCreds.ID, s.githubEndpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). From 8b5584f0835be5554303ddd7ba3eddc1bb1ddb9a Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Wed, 24 Apr 2024 11:17:16 +0000 Subject: [PATCH 12/27] Add some e2e boilerplate Signed-off-by: Gabriel Adrian Samfira --- test/integration/e2e/client_utils.go | 84 +++++++++++++++++++++++----- test/integration/e2e/credentials.go | 18 ++++++ test/integration/e2e/e2e.go | 59 +++++++++++++++++++ test/integration/e2e/endpoints.go | 12 ++++ test/integration/e2e/instances.go | 3 +- test/integration/main.go | 5 ++ 6 files changed, 165 insertions(+), 16 deletions(-) create mode 100644 test/integration/e2e/credentials.go create mode 100644 test/integration/e2e/endpoints.go diff --git a/test/integration/e2e/client_utils.go b/test/integration/e2e/client_utils.go index 39bd6ded..564a48c7 100644 --- a/test/integration/e2e/client_utils.go +++ b/test/integration/e2e/client_utils.go @@ -6,6 +6,7 @@ import ( "github.com/cloudbase/garm/client" clientControllerInfo "github.com/cloudbase/garm/client/controller_info" clientCredentials "github.com/cloudbase/garm/client/credentials" + clientEndpoints "github.com/cloudbase/garm/client/endpoints" clientFirstRun "github.com/cloudbase/garm/client/first_run" clientInstances "github.com/cloudbase/garm/client/instances" clientJobs "github.com/cloudbase/garm/client/jobs" @@ -18,9 +19,7 @@ import ( "github.com/cloudbase/garm/params" ) -// /////////// -// Garm Init / -// /////////// +// firstRun will initialize a new garm installation. func firstRun(apiCli *client.GarmAPI, newUser params.NewUserParams) (params.User, error) { firstRunResponse, err := apiCli.FirstRun.FirstRun( clientFirstRun.NewFirstRunParams().WithBody(newUser), @@ -41,9 +40,7 @@ func login(apiCli *client.GarmAPI, params params.PasswordLoginParams) (string, e return loginResponse.Payload.Token, nil } -// //////////////////////////// -// Credentials and Providers // -// //////////////////////////// +// listCredentials lists all the credentials configured in GARM. func listCredentials(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (params.Credentials, error) { listCredentialsResponse, err := apiCli.Credentials.ListCredentials( clientCredentials.NewListCredentialsParams(), @@ -54,6 +51,69 @@ func listCredentials(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfo return listCredentialsResponse.Payload, nil } +func createGithubCredentials(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, credentialsParams params.CreateGithubCredentialsParams) (*params.GithubCredentials, error) { + createCredentialsResponse, err := apiCli.Credentials.CreateCredentials( + clientCredentials.NewCreateCredentialsParams().WithBody(credentialsParams), + apiAuthToken) + if err != nil { + return nil, err + } + return &createCredentialsResponse.Payload, nil +} + +func deleteGithubCredentials(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, credentialsID int64) error { + return apiCli.Credentials.DeleteCredentials( + clientCredentials.NewDeleteCredentialsParams().WithID(credentialsID), + apiAuthToken) +} + +func getGithubCredential(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, credentialsID int64) (*params.GithubCredentials, error) { + getCredentialsResponse, err := apiCli.Credentials.GetCredentials( + clientCredentials.NewGetCredentialsParams().WithID(credentialsID), + apiAuthToken) + if err != nil { + return nil, err + } + return &getCredentialsResponse.Payload, nil +} + +func createGithubEndpoint(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, endpointParams params.CreateGithubEndpointParams) (*params.GithubEndpoint, error) { + createEndpointResponse, err := apiCli.Endpoints.CreateGithubEndpoint( + clientEndpoints.NewCreateGithubEndpointParams().WithBody(endpointParams), + apiAuthToken) + if err != nil { + return nil, err + } + return &createEndpointResponse.Payload, nil +} + +func listGithubEndpoints(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (params.GithubEndpoints, error) { + listEndpointsResponse, err := apiCli.Endpoints.ListGithubEndpoints( + clientEndpoints.NewListGithubEndpointsParams(), + apiAuthToken) + if err != nil { + return nil, err + } + return listEndpointsResponse.Payload, nil +} + +func getGithubEndpoint(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, endpointName string) (*params.GithubEndpoint, error) { + getEndpointResponse, err := apiCli.Endpoints.GetGithubEndpoint( + clientEndpoints.NewGetGithubEndpointParams().WithName(endpointName), + apiAuthToken) + if err != nil { + return nil, err + } + return &getEndpointResponse.Payload, nil +} + +func deleteGithubEndpoint(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, endpointName string) error { + return apiCli.Endpoints.DeleteGithubEndpoint( + clientEndpoints.NewDeleteGithubEndpointParams().WithName(endpointName), + apiAuthToken) +} + +// listProviders lists all the providers configured in GARM. func listProviders(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (params.Providers, error) { listProvidersResponse, err := apiCli.Providers.ListProviders( clientProviders.NewListProvidersParams(), @@ -64,9 +124,7 @@ func listProviders(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWr return listProvidersResponse.Payload, nil } -// //////////////////////// -// // Controller info //// -// //////////////////////// +// getControllerInfo returns information about the GARM controller. func getControllerInfo(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (params.ControllerInfo, error) { controllerInfoResponse, err := apiCli.ControllerInfo.ControllerInfo( clientControllerInfo.NewControllerInfoParams(), @@ -77,9 +135,7 @@ func getControllerInfo(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthIn return controllerInfoResponse.Payload, nil } -// //////// -// Jobs // -// //////// +// listJobs lists all the jobs configured in GARM. func listJobs(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (params.Jobs, error) { listJobsResponse, err := apiCli.Jobs.ListJobs( clientJobs.NewListJobsParams(), @@ -90,9 +146,7 @@ func listJobs(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) return listJobsResponse.Payload, nil } -// ////////////////// -// / Metrics Token // -// ////////////////// +// getMetricsToken returns the metrics token. func getMetricsToken(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (string, error) { getMetricsTokenResponse, err := apiCli.MetricsToken.GetMetricsToken( clientMetricsToken.NewGetMetricsTokenParams(), diff --git a/test/integration/e2e/credentials.go b/test/integration/e2e/credentials.go new file mode 100644 index 00000000..15ba5a9e --- /dev/null +++ b/test/integration/e2e/credentials.go @@ -0,0 +1,18 @@ +package e2e + +import ( + "github.com/cloudbase/garm/params" +) + +func EnsureTestCredentials(name string, oauthToken string, endpointName string) { + createCredsParams := params.CreateGithubCredentialsParams{ + Name: name, + Endpoint: endpointName, + Description: "GARM test credentials", + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: oauthToken, + }, + } + CreateGithubCredentials(createCredsParams) +} diff --git a/test/integration/e2e/e2e.go b/test/integration/e2e/e2e.go index 7555a835..3bb2f7fe 100644 --- a/test/integration/e2e/e2e.go +++ b/test/integration/e2e/e2e.go @@ -18,6 +18,65 @@ func ListCredentials() params.Credentials { return credentials } +func CreateGithubCredentials(credentialsParams params.CreateGithubCredentialsParams) *params.GithubCredentials { + slog.Info("Create GitHub credentials") + credentials, err := createGithubCredentials(cli, authToken, credentialsParams) + if err != nil { + panic(err) + } + return credentials +} + +func GetGithubCredential(id int64) *params.GithubCredentials { + slog.Info("Get GitHub credential") + credentials, err := getGithubCredential(cli, authToken, id) + if err != nil { + panic(err) + } + return credentials +} + +func DeleteGithubCredential(id int64) { + slog.Info("Delete GitHub credential") + if err := deleteGithubCredentials(cli, authToken, id); err != nil { + panic(err) + } +} + +func CreateGithubEndpoint(endpointParams params.CreateGithubEndpointParams) *params.GithubEndpoint { + slog.Info("Create GitHub endpoint") + endpoint, err := createGithubEndpoint(cli, authToken, endpointParams) + if err != nil { + panic(err) + } + return endpoint +} + +func ListGithubEndpoints() params.GithubEndpoints { + slog.Info("List GitHub endpoints") + endpoints, err := listGithubEndpoints(cli, authToken) + if err != nil { + panic(err) + } + return endpoints +} + +func GetGithubEndpoint(name string) *params.GithubEndpoint { + slog.Info("Get GitHub endpoint") + endpoint, err := getGithubEndpoint(cli, authToken, name) + if err != nil { + panic(err) + } + return endpoint +} + +func DeleteGithubEndpoint(name string) { + slog.Info("Delete GitHub endpoint") + if err := deleteGithubEndpoint(cli, authToken, name); err != nil { + panic(err) + } +} + func ListProviders() params.Providers { slog.Info("List providers") providers, err := listProviders(cli, authToken) diff --git a/test/integration/e2e/endpoints.go b/test/integration/e2e/endpoints.go new file mode 100644 index 00000000..25b46aac --- /dev/null +++ b/test/integration/e2e/endpoints.go @@ -0,0 +1,12 @@ +package e2e + +func MustDefaultGithubEndpoint() { + ep := GetGithubEndpoint("github.com") + if ep == nil { + panic("Default GitHub endpoint not found") + } + + if ep.Name != "github.com" { + panic("Default GitHub endpoint name mismatch") + } +} diff --git a/test/integration/e2e/instances.go b/test/integration/e2e/instances.go index edf2ab57..2f33e79d 100644 --- a/test/integration/e2e/instances.go +++ b/test/integration/e2e/instances.go @@ -12,10 +12,11 @@ import ( func waitInstanceStatus(name string, status commonParams.InstanceStatus, runnerStatus params.RunnerStatus, timeout time.Duration) (*params.Instance, error) { var timeWaited time.Duration // default is 0 var instance *params.Instance + var err error slog.Info("Waiting for instance to reach desired status", "instance", name, "desired_status", status, "desired_runner_status", runnerStatus) for timeWaited < timeout { - instance, err := getInstance(cli, authToken, name) + instance, err = getInstance(cli, authToken, name) if err != nil { return nil, err } diff --git a/test/integration/main.go b/test/integration/main.go index 609db66f..65530fed 100644 --- a/test/integration/main.go +++ b/test/integration/main.go @@ -76,6 +76,11 @@ func main() { e2e.FirstRun(adminUsername, adminPassword, adminFullName, adminEmail) e2e.Login(adminUsername, adminPassword) + // Ensure that the default "github.com" endpoint is automatically created. + e2e.MustDefaultGithubEndpoint() + // Create test credentials + e2e.EnsureTestCredentials(credentialsName, ghToken, "github.com") + // ////////////////// // controller info // // ////////////////// From f5682e6323b363c711f6b7dc66f6bed0e0e227dc Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Wed, 24 Apr 2024 11:30:15 +0000 Subject: [PATCH 13/27] Work around actions/runner-images#7210 Signed-off-by: Gabriel Adrian Samfira --- .github/workflows/integration-tests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 19264582..200485aa 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Golang uses: actions/setup-go@v3 @@ -20,7 +20,9 @@ jobs: uses: canonical/setup-lxd@v0.1.1 - name: Install dependencies - run: sudo apt-get -qq update && sudo apt-get -qq install -y apg coreutils make + run: | + sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list + sudo apt-get -qq update && sudo apt-get -qq install -y apg coreutils make - name: Set up ngrok id: ngrok From c2b974dfa05c7f9461767b628a12601e1b2329bb Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Wed, 24 Apr 2024 11:39:20 +0000 Subject: [PATCH 14/27] Create clone creds for repo update Signed-off-by: Gabriel Adrian Samfira --- test/integration/e2e/credentials.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/integration/e2e/credentials.go b/test/integration/e2e/credentials.go index 15ba5a9e..480cdcd7 100644 --- a/test/integration/e2e/credentials.go +++ b/test/integration/e2e/credentials.go @@ -1,6 +1,8 @@ package e2e import ( + "fmt" + "github.com/cloudbase/garm/params" ) @@ -15,4 +17,7 @@ func EnsureTestCredentials(name string, oauthToken string, endpointName string) }, } CreateGithubCredentials(createCredsParams) + + createCredsParams.Name = fmt.Sprintf("%s-clone", name) + CreateGithubCredentials(createCredsParams) } From 1256473089e0f09a541e86997fb2bd030830ddbe Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Wed, 24 Apr 2024 12:23:45 +0000 Subject: [PATCH 15/27] Fetch credentials from DB Do not rely on the entity object to hold updated or detailed credentials, fetch them from the DB every time. This change also ensures that we pass in the user context instead of the runner context to the DB methods. Signed-off-by: Gabriel Adrian Samfira --- .github/workflows/integration-tests.yml | 3 +- cmd/garm/main.go | 2 ++ database/sql/util.go | 13 ++++++++ params/params.go | 38 +++++++++++++++-------- runner/enterprises.go | 4 +++ runner/organizations.go | 4 +++ runner/repositories.go | 4 +++ runner/runner.go | 40 +++++++++++++++++++++---- 8 files changed, 88 insertions(+), 20 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 200485aa..95b95e8b 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -54,7 +54,7 @@ jobs: run: | set -o pipefail set -o errexit - make integration 2>&1 | tee /artifacts-logs/e2e.log + make integration 2>&1 env: GARM_BASE_URL: ${{ steps.ngrok.outputs.tunnel-url }} ORG_NAME: gsamfira @@ -68,6 +68,7 @@ jobs: run: | sudo systemctl status garm@runner || true sudo journalctl --no-pager 2>&1 > /artifacts-logs/system.log + sudo journalctl -u garm@runner --no-pager 2>&1 > /artifacts-logs/garm.log - name: Upload GARM and e2e logs if: always() diff --git a/cmd/garm/main.go b/cmd/garm/main.go index 454d766f..526e2017 100644 --- a/cmd/garm/main.go +++ b/cmd/garm/main.go @@ -151,6 +151,8 @@ func main() { ctx, stop := signal.NotifyContext(context.Background(), signals...) defer stop() + ctx = auth.GetAdminContext(ctx) + cfg, err := config.NewConfig(*conf) if err != nil { log.Fatalf("Fetching config: %+v", err) //nolint:gocritic diff --git a/database/sql/util.go b/database/sql/util.go index c291e5d9..b7f8a058 100644 --- a/database/sql/util.go +++ b/database/sql/util.go @@ -127,6 +127,11 @@ func (s *sqlDatabase) sqlToCommonOrganization(org Organization, detailed bool) ( PoolBalancerType: org.PoolBalancerType, Endpoint: endpoint, } + + if org.CredentialsID != nil { + ret.CredentialsID = *org.CredentialsID + } + if detailed { creds, err := s.sqlToCommonGithubCredentials(org.Credentials) if err != nil { @@ -172,6 +177,10 @@ func (s *sqlDatabase) sqlToCommonEnterprise(enterprise Enterprise, detailed bool Endpoint: endpoint, } + if enterprise.CredentialsID != nil { + ret.CredentialsID = *enterprise.CredentialsID + } + if detailed { creds, err := s.sqlToCommonGithubCredentials(enterprise.Credentials) if err != nil { @@ -278,6 +287,10 @@ func (s *sqlDatabase) sqlToCommonRepository(repo Repository, detailed bool) (par Endpoint: endpoint, } + if repo.CredentialsID != nil { + ret.CredentialsID = *repo.CredentialsID + } + if detailed { creds, err := s.sqlToCommonGithubCredentials(repo.Credentials) if err != nil { diff --git a/params/params.go b/params/params.go index 522dc954..f9629fc7 100644 --- a/params/params.go +++ b/params/params.go @@ -397,11 +397,15 @@ type Internal struct { } type Repository struct { - ID string `json:"id"` - Owner string `json:"owner"` - Name string `json:"name"` - Pools []Pool `json:"pool,omitempty"` - CredentialsName string `json:"credentials_name"` + ID string `json:"id"` + Owner string `json:"owner"` + Name string `json:"name"` + Pools []Pool `json:"pool,omitempty"` + // CredentialName is the name of the credentials associated with the enterprise. + // This field is now deprecated. Use CredentialsID instead. This field will be + // removed in v0.2.0. + CredentialsName string `json:"credentials_name,omitempty"` + CredentialsID uint `json:"credentials_id"` Credentials GithubCredentials `json:"credentials"` PoolManagerStatus PoolManagerStatus `json:"pool_manager_status,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancing_type"` @@ -445,11 +449,15 @@ func (r Repository) String() string { type Repositories []Repository type Organization struct { - ID string `json:"id"` - Name string `json:"name"` - Pools []Pool `json:"pool,omitempty"` - CredentialsName string `json:"credentials_name"` + ID string `json:"id"` + Name string `json:"name"` + Pools []Pool `json:"pool,omitempty"` + // CredentialName is the name of the credentials associated with the enterprise. + // This field is now deprecated. Use CredentialsID instead. This field will be + // removed in v0.2.0. + CredentialsName string `json:"credentials_name,omitempty"` Credentials GithubCredentials `json:"credentials"` + CredentialsID uint `json:"credentials_id"` PoolManagerStatus PoolManagerStatus `json:"pool_manager_status,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancing_type"` Endpoint GithubEndpoint `json:"endpoint"` @@ -488,11 +496,15 @@ func (o Organization) GetBalancerType() PoolBalancerType { type Organizations []Organization type Enterprise struct { - ID string `json:"id"` - Name string `json:"name"` - Pools []Pool `json:"pool,omitempty"` - CredentialsName string `json:"credentials_name"` + ID string `json:"id"` + Name string `json:"name"` + Pools []Pool `json:"pool,omitempty"` + // CredentialName is the name of the credentials associated with the enterprise. + // This field is now deprecated. Use CredentialsID instead. This field will be + // removed in v0.2.0. + CredentialsName string `json:"credentials_name,omitempty"` Credentials GithubCredentials `json:"credentials"` + CredentialsID uint `json:"credentials_id"` PoolManagerStatus PoolManagerStatus `json:"pool_manager_status,omitempty"` PoolBalancerType PoolBalancerType `json:"pool_balancing_type"` Endpoint GithubEndpoint `json:"endpoint"` diff --git a/runner/enterprises.go b/runner/enterprises.go index 8f8dbf31..7b12b245 100644 --- a/runner/enterprises.go +++ b/runner/enterprises.go @@ -54,6 +54,8 @@ func (r *Runner) CreateEnterprise(ctx context.Context, param params.CreateEnterp } }() + // Use the admin context in the pool manager. Any access control is already done above when + // updating the store. var poolMgr common.PoolManager poolMgr, err = r.poolManagerCtrl.CreateEnterprisePoolManager(r.ctx, enterprise, r.providers, r.store) if err != nil { @@ -172,6 +174,8 @@ func (r *Runner) UpdateEnterprise(ctx context.Context, enterpriseID string, para return params.Enterprise{}, errors.Wrap(err, "updating enterprise") } + // Use the admin context in the pool manager. Any access control is already done above when + // updating the store. poolMgr, err := r.poolManagerCtrl.UpdateEnterprisePoolManager(r.ctx, enterprise) if err != nil { return params.Enterprise{}, fmt.Errorf("failed to update enterprise pool manager: %w", err) diff --git a/runner/organizations.go b/runner/organizations.go index c0663505..ae2e853f 100644 --- a/runner/organizations.go +++ b/runner/organizations.go @@ -67,6 +67,8 @@ func (r *Runner) CreateOrganization(ctx context.Context, param params.CreateOrgP } }() + // Use the admin context in the pool manager. Any access control is already done above when + // updating the store. poolMgr, err := r.poolManagerCtrl.CreateOrgPoolManager(r.ctx, org, r.providers, r.store) if err != nil { return params.Organization{}, errors.Wrap(err, "creating org pool manager") @@ -201,6 +203,8 @@ func (r *Runner) UpdateOrganization(ctx context.Context, orgID string, param par return params.Organization{}, errors.Wrap(err, "updating org") } + // Use the admin context in the pool manager. Any access control is already done above when + // updating the store. poolMgr, err := r.poolManagerCtrl.UpdateOrgPoolManager(r.ctx, org) if err != nil { return params.Organization{}, fmt.Errorf("updating org pool manager: %w", err) diff --git a/runner/repositories.go b/runner/repositories.go index b2a6ef54..e316aa47 100644 --- a/runner/repositories.go +++ b/runner/repositories.go @@ -67,6 +67,8 @@ func (r *Runner) CreateRepository(ctx context.Context, param params.CreateRepoPa } }() + // Use the admin context in the pool manager. Any access control is already done above when + // updating the store. poolMgr, err := r.poolManagerCtrl.CreateRepoPoolManager(r.ctx, repo, r.providers, r.store) if err != nil { return params.Repository{}, errors.Wrap(err, "creating repo pool manager") @@ -200,6 +202,8 @@ func (r *Runner) UpdateRepository(ctx context.Context, repoID string, param para return params.Repository{}, errors.Wrap(err, "updating repo") } + // Use the admin context in the pool manager. Any access control is already done above when + // updating the store. poolMgr, err := r.poolManagerCtrl.UpdateRepoPoolManager(r.ctx, repo) if err != nil { return params.Repository{}, fmt.Errorf("failed to update pool manager: %w", err) diff --git a/runner/runner.go b/runner/runner.go index 453980a0..9873695d 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -67,6 +67,7 @@ func NewRunner(ctx context.Context, cfg config.Config, db dbCommon.Store) (*Runn poolManagerCtrl := &poolManagerCtrl{ controllerID: ctrlID.ControllerID.String(), config: cfg, + store: db, repositories: map[string]common.PoolManager{}, organizations: map[string]common.PoolManager{}, enterprises: map[string]common.PoolManager{}, @@ -92,6 +93,7 @@ type poolManagerCtrl struct { controllerID string config config.Config + store dbCommon.Store repositories map[string]common.PoolManager organizations map[string]common.PoolManager @@ -102,7 +104,12 @@ func (p *poolManagerCtrl) CreateRepoPoolManager(ctx context.Context, repo params p.mux.Lock() defer p.mux.Unlock() - cfgInternal, err := p.getInternalConfig(ctx, repo.Credentials, repo.GetBalancerType()) + creds, err := p.store.GetGithubCredentials(ctx, repo.CredentialsID, true) + if err != nil { + return nil, errors.Wrap(err, "fetching credentials") + } + + cfgInternal, err := p.getInternalConfig(ctx, creds, repo.GetBalancerType()) if err != nil { return nil, errors.Wrap(err, "fetching internal config") } @@ -130,7 +137,12 @@ func (p *poolManagerCtrl) UpdateRepoPoolManager(ctx context.Context, repo params return nil, errors.Wrapf(runnerErrors.ErrNotFound, "repository %s/%s pool manager not loaded", repo.Owner, repo.Name) } - internalCfg, err := p.getInternalConfig(ctx, repo.Credentials, repo.GetBalancerType()) + creds, err := p.store.GetGithubCredentials(ctx, repo.CredentialsID, true) + if err != nil { + return nil, errors.Wrap(err, "fetching credentials") + } + + internalCfg, err := p.getInternalConfig(ctx, creds, repo.GetBalancerType()) if err != nil { return nil, errors.Wrap(err, "fetching internal config") } @@ -175,7 +187,11 @@ func (p *poolManagerCtrl) CreateOrgPoolManager(ctx context.Context, org params.O p.mux.Lock() defer p.mux.Unlock() - cfgInternal, err := p.getInternalConfig(ctx, org.Credentials, org.GetBalancerType()) + creds, err := p.store.GetGithubCredentials(ctx, org.CredentialsID, true) + if err != nil { + return nil, errors.Wrap(err, "fetching credentials") + } + cfgInternal, err := p.getInternalConfig(ctx, creds, org.GetBalancerType()) if err != nil { return nil, errors.Wrap(err, "fetching internal config") } @@ -202,7 +218,11 @@ func (p *poolManagerCtrl) UpdateOrgPoolManager(ctx context.Context, org params.O return nil, errors.Wrapf(runnerErrors.ErrNotFound, "org %s pool manager not loaded", org.Name) } - internalCfg, err := p.getInternalConfig(ctx, org.Credentials, org.GetBalancerType()) + creds, err := p.store.GetGithubCredentials(ctx, org.CredentialsID, true) + if err != nil { + return nil, errors.Wrap(err, "fetching credentials") + } + internalCfg, err := p.getInternalConfig(ctx, creds, org.GetBalancerType()) if err != nil { return nil, errors.Wrap(err, "fetching internal config") } @@ -247,7 +267,11 @@ func (p *poolManagerCtrl) CreateEnterprisePoolManager(ctx context.Context, enter p.mux.Lock() defer p.mux.Unlock() - cfgInternal, err := p.getInternalConfig(ctx, enterprise.Credentials, enterprise.GetBalancerType()) + creds, err := p.store.GetGithubCredentials(ctx, enterprise.CredentialsID, true) + if err != nil { + return nil, errors.Wrap(err, "fetching credentials") + } + cfgInternal, err := p.getInternalConfig(ctx, creds, enterprise.GetBalancerType()) if err != nil { return nil, errors.Wrap(err, "fetching internal config") } @@ -275,7 +299,11 @@ func (p *poolManagerCtrl) UpdateEnterprisePoolManager(ctx context.Context, enter return nil, errors.Wrapf(runnerErrors.ErrNotFound, "enterprise %s pool manager not loaded", enterprise.Name) } - internalCfg, err := p.getInternalConfig(ctx, enterprise.Credentials, enterprise.GetBalancerType()) + creds, err := p.store.GetGithubCredentials(ctx, enterprise.CredentialsID, true) + if err != nil { + return nil, errors.Wrap(err, "fetching credentials") + } + internalCfg, err := p.getInternalConfig(ctx, creds, enterprise.GetBalancerType()) if err != nil { return nil, errors.Wrap(err, "fetching internal config") } From ccf51053b5c6e29b330b1b90e6ebaf4f7adb0419 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Wed, 24 Apr 2024 14:43:11 +0000 Subject: [PATCH 16/27] Add github endpoint operations e2e test Signed-off-by: Gabriel Adrian Samfira --- .github/workflows/integration-tests.yml | 1 + test/integration/e2e/endpoints.go | 102 ++++++++++++++++++++++++ test/integration/main.go | 3 + 3 files changed, 106 insertions(+) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 95b95e8b..729777e0 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -45,6 +45,7 @@ jobs: echo "GARM_PASSWORD=$GARM_PASSWORD" >> $GITHUB_ENV echo "REPO_WEBHOOK_SECRET=$REPO_WEBHOOK_SECRET" >> $GITHUB_ENV echo "ORG_WEBHOOK_SECRET=$ORG_WEBHOOK_SECRET" >> $GITHUB_ENV + echo "GARM_CHECKOUT_DIR=$GITHUB_WORKSPACE" >> $GITHUB_ENV - name: Create logs directory if: always() diff --git a/test/integration/e2e/endpoints.go b/test/integration/e2e/endpoints.go index 25b46aac..9f4e5794 100644 --- a/test/integration/e2e/endpoints.go +++ b/test/integration/e2e/endpoints.go @@ -1,5 +1,12 @@ package e2e +import ( + "os" + "path/filepath" + + "github.com/cloudbase/garm/params" +) + func MustDefaultGithubEndpoint() { ep := GetGithubEndpoint("github.com") if ep == nil { @@ -10,3 +17,98 @@ func MustDefaultGithubEndpoint() { panic("Default GitHub endpoint name mismatch") } } + +func checkEndpointParamsAreEqual(a, b params.GithubEndpoint) { + if a.Name != b.Name { + panic("Endpoint name mismatch") + } + + if a.Description != b.Description { + panic("Endpoint description mismatch") + } + + if a.BaseURL != b.BaseURL { + panic("Endpoint base URL mismatch") + } + + if a.APIBaseURL != b.APIBaseURL { + panic("Endpoint API base URL mismatch") + } + + if a.UploadBaseURL != b.UploadBaseURL { + panic("Endpoint upload base URL mismatch") + } + + if string(a.CACertBundle) != string(b.CACertBundle) { + panic("Endpoint CA cert bundle mismatch") + } +} + +func TestGithubEndpointOperations() { + baseDir := os.Getenv("GARM_CHECKOUT_DIR") + if baseDir == "" { + panic("GARM_CHECKOUT_DIR not set") + } + caBundle, err := os.ReadFile(filepath.Join(baseDir, "testdata/certs/srv-pub.pem")) + if err != nil { + panic(err) + } + endpointParams := params.CreateGithubEndpointParams{ + Name: "test-endpoint", + Description: "Test endpoint", + BaseURL: "https://ghes.example.com", + APIBaseURL: "https://api.ghes.example.com/", + UploadBaseURL: "https://uploads.ghes.example.com/", + CACertBundle: caBundle, + } + + endpoint := CreateGithubEndpoint(endpointParams) + if endpoint.Name != endpointParams.Name { + panic("Endpoint name mismatch") + } + + if endpoint.Description != endpointParams.Description { + panic("Endpoint description mismatch") + } + + if endpoint.BaseURL != endpointParams.BaseURL { + panic("Endpoint base URL mismatch") + } + + if endpoint.APIBaseURL != endpointParams.APIBaseURL { + panic("Endpoint API base URL mismatch") + } + + if endpoint.UploadBaseURL != endpointParams.UploadBaseURL { + panic("Endpoint upload base URL mismatch") + } + + if string(endpoint.CACertBundle) != string(caBundle) { + panic("Endpoint CA cert bundle mismatch") + } + + endpoint2 := GetGithubEndpoint(endpointParams.Name) + if endpoint == nil || endpoint2 == nil { + panic("endpoint is nil") + } + checkEndpointParamsAreEqual(*endpoint, *endpoint2) + + endpoints := ListGithubEndpoints() + var found bool + for _, ep := range endpoints { + if ep.Name == endpointParams.Name { + checkEndpointParamsAreEqual(*endpoint, ep) + found = true + break + } + } + if !found { + panic("Endpoint not found in list") + } + + DeleteGithubEndpoint(endpoint.Name) + + if err := deleteGithubEndpoint(cli, authToken, "github.com"); err == nil { + panic("expected error when attempting to delete the default github.com endpoint") + } +} diff --git a/test/integration/main.go b/test/integration/main.go index 65530fed..b9bf2cc9 100644 --- a/test/integration/main.go +++ b/test/integration/main.go @@ -81,6 +81,9 @@ func main() { // Create test credentials e2e.EnsureTestCredentials(credentialsName, ghToken, "github.com") + // Test endpoint operations + e2e.TestGithubEndpointOperations() + // ////////////////// // controller info // // ////////////////// From 39a5e14eb16963301a716381cf3cef21bb2407db Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Thu, 25 Apr 2024 07:45:09 +0000 Subject: [PATCH 17/27] Add more e2e GH endpoint tests Signed-off-by: Gabriel Adrian Samfira --- test/integration/e2e/credentials.go | 13 ++++++ test/integration/e2e/endpoints.go | 69 ++++++++++++++++++++++++++++- test/integration/main.go | 12 ++--- 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/test/integration/e2e/credentials.go b/test/integration/e2e/credentials.go index 480cdcd7..54c60440 100644 --- a/test/integration/e2e/credentials.go +++ b/test/integration/e2e/credentials.go @@ -21,3 +21,16 @@ func EnsureTestCredentials(name string, oauthToken string, endpointName string) createCredsParams.Name = fmt.Sprintf("%s-clone", name) CreateGithubCredentials(createCredsParams) } + +func createDummyCredentials(name, endpointName string) *params.GithubCredentials { + createCredsParams := params.CreateGithubCredentialsParams{ + Name: name, + Endpoint: endpointName, + Description: "GARM test credentials", + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "dummy", + }, + } + return CreateGithubCredentials(createCredsParams) +} diff --git a/test/integration/e2e/endpoints.go b/test/integration/e2e/endpoints.go index 9f4e5794..433da53f 100644 --- a/test/integration/e2e/endpoints.go +++ b/test/integration/e2e/endpoints.go @@ -1,6 +1,7 @@ package e2e import ( + "log/slog" "os" "path/filepath" @@ -44,15 +45,23 @@ func checkEndpointParamsAreEqual(a, b params.GithubEndpoint) { } } -func TestGithubEndpointOperations() { +func getTestFileContents(relPath string) []byte { baseDir := os.Getenv("GARM_CHECKOUT_DIR") if baseDir == "" { panic("GARM_CHECKOUT_DIR not set") } - caBundle, err := os.ReadFile(filepath.Join(baseDir, "testdata/certs/srv-pub.pem")) + contents, err := os.ReadFile(filepath.Join(baseDir, "testdata", relPath)) if err != nil { panic(err) } + return contents +} + +func TestGithubEndpointOperations() { + MustDefaultGithubEndpoint() + + caBundle := getTestFileContents("certs/srv-pub.pem") + endpointParams := params.CreateGithubEndpointParams{ Name: "test-endpoint", Description: "Test endpoint", @@ -107,8 +116,64 @@ func TestGithubEndpointOperations() { } DeleteGithubEndpoint(endpoint.Name) +} +func TestGithubEndpointMustFailToDeleteDefaultGithubEndpoint() { if err := deleteGithubEndpoint(cli, authToken, "github.com"); err == nil { panic("expected error when attempting to delete the default github.com endpoint") } } + +func TestGithubEndpointFailsOnInvalidCABundle() { + slog.Info("Testing endpoint creation with invalid CA cert bundle") + badCABundle := getTestFileContents("certs/srv-key.pem") + + endpointParams := params.CreateGithubEndpointParams{ + Name: "dummy", + Description: "Dummy endpoint", + BaseURL: "https://ghes.example.com", + APIBaseURL: "https://api.ghes.example.com/", + UploadBaseURL: "https://uploads.ghes.example.com/", + CACertBundle: badCABundle, + } + + if _, err := createGithubEndpoint(cli, authToken, endpointParams); err == nil { + panic("expected error when creating endpoint with invalid CA cert bundle") + } +} + +func TestGithubEndpointDeletionFailsWhenCredentialsExist() { + slog.Info("Testing endpoint deletion when credentials exist") + endpointParams := params.CreateGithubEndpointParams{ + Name: "dummy", + Description: "Dummy endpoint", + BaseURL: "https://ghes.example.com", + APIBaseURL: "https://api.ghes.example.com/", + UploadBaseURL: "https://uploads.ghes.example.com/", + } + + endpoint := CreateGithubEndpoint(endpointParams) + creds := createDummyCredentials("test-creds", endpoint.Name) + + if err := deleteGithubEndpoint(cli, authToken, endpoint.Name); err == nil { + panic("expected error when deleting endpoint with credentials") + } + + DeleteGithubCredential(int64(creds.ID)) + DeleteGithubEndpoint(endpoint.Name) +} + +func TestGithubEndpointFailsOnDuplicateName() { + slog.Info("Testing endpoint creation with duplicate name") + endpointParams := params.CreateGithubEndpointParams{ + Name: "github.com", + Description: "Dummy endpoint", + BaseURL: "https://ghes.example.com", + APIBaseURL: "https://api.ghes.example.com/", + UploadBaseURL: "https://uploads.ghes.example.com/", + } + + if _, err := createGithubEndpoint(cli, authToken, endpointParams); err == nil { + panic("expected error when creating endpoint with duplicate name") + } +} diff --git a/test/integration/main.go b/test/integration/main.go index b9bf2cc9..711478b8 100644 --- a/test/integration/main.go +++ b/test/integration/main.go @@ -76,13 +76,15 @@ func main() { e2e.FirstRun(adminUsername, adminPassword, adminFullName, adminEmail) e2e.Login(adminUsername, adminPassword) - // Ensure that the default "github.com" endpoint is automatically created. - e2e.MustDefaultGithubEndpoint() - // Create test credentials - e2e.EnsureTestCredentials(credentialsName, ghToken, "github.com") - // Test endpoint operations e2e.TestGithubEndpointOperations() + e2e.TestGithubEndpointFailsOnInvalidCABundle() + e2e.TestGithubEndpointDeletionFailsWhenCredentialsExist() + e2e.TestGithubEndpointFailsOnDuplicateName() + e2e.TestGithubEndpointMustFailToDeleteDefaultGithubEndpoint() + + // Create test credentials + e2e.EnsureTestCredentials(credentialsName, ghToken, "github.com") // ////////////////// // controller info // From 0128f59344c90574ac766854cb47d0da4b6bd0a8 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Thu, 25 Apr 2024 09:17:54 +0000 Subject: [PATCH 18/27] Add some credentials e2e tests Signed-off-by: Gabriel Adrian Samfira --- database/sql/github.go | 8 +- test/integration/e2e/client_utils.go | 20 +++ test/integration/e2e/credentials.go | 182 +++++++++++++++++++++++++++ test/integration/e2e/e2e.go | 9 ++ test/integration/e2e/endpoints.go | 61 +++++++++ test/integration/e2e/utils.go | 18 +++ test/integration/main.go | 9 ++ 7 files changed, 303 insertions(+), 4 deletions(-) diff --git a/database/sql/github.go b/database/sql/github.go index 2f38a7de..33dfe7da 100644 --- a/database/sql/github.go +++ b/database/sql/github.go @@ -279,8 +279,8 @@ func (s *sqlDatabase) CreateGithubCredentials(ctx context.Context, param params. return errors.Wrap(err, "fetching github endpoint") } - if err := tx.Where("name = ?", param.Name).First(&creds).Error; err == nil { - return errors.New("github credentials already exists") + if err := tx.Where("name = ? and user_id = ?", param.Name, userID).First(&creds).Error; err == nil { + return errors.Wrap(runnerErrors.ErrDuplicateEntity, "github credentials already exists") } var data []byte @@ -449,7 +449,7 @@ func (s *sqlDatabase) UpdateGithubCredentials(ctx context.Context, id uint, para } if param.App != nil { - return errors.New("cannot update app credentials for PAT") + return errors.Wrap(runnerErrors.ErrBadRequest, "cannot update app credentials for PAT") } case params.GithubAuthTypeApp: if param.App != nil { @@ -457,7 +457,7 @@ func (s *sqlDatabase) UpdateGithubCredentials(ctx context.Context, id uint, para } if param.PAT != nil { - return errors.New("cannot update PAT credentials for app") + return errors.Wrap(runnerErrors.ErrBadRequest, "cannot update PAT credentials for app") } } diff --git a/test/integration/e2e/client_utils.go b/test/integration/e2e/client_utils.go index 564a48c7..43a28ee7 100644 --- a/test/integration/e2e/client_utils.go +++ b/test/integration/e2e/client_utils.go @@ -77,6 +77,16 @@ func getGithubCredential(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuth return &getCredentialsResponse.Payload, nil } +func updateGithubCredentials(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, credentialsID int64, credentialsParams params.UpdateGithubCredentialsParams) (*params.GithubCredentials, error) { + updateCredentialsResponse, err := apiCli.Credentials.UpdateCredentials( + clientCredentials.NewUpdateCredentialsParams().WithID(credentialsID).WithBody(credentialsParams), + apiAuthToken) + if err != nil { + return nil, err + } + return &updateCredentialsResponse.Payload, nil +} + func createGithubEndpoint(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, endpointParams params.CreateGithubEndpointParams) (*params.GithubEndpoint, error) { createEndpointResponse, err := apiCli.Endpoints.CreateGithubEndpoint( clientEndpoints.NewCreateGithubEndpointParams().WithBody(endpointParams), @@ -113,6 +123,16 @@ func deleteGithubEndpoint(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAut apiAuthToken) } +func updateGithubEndpoint(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, endpointName string, endpointParams params.UpdateGithubEndpointParams) (*params.GithubEndpoint, error) { + updateEndpointResponse, err := apiCli.Endpoints.UpdateGithubEndpoint( + clientEndpoints.NewUpdateGithubEndpointParams().WithName(endpointName).WithBody(endpointParams), + apiAuthToken) + if err != nil { + return nil, err + } + return &updateEndpointResponse.Payload, nil +} + // listProviders lists all the providers configured in GARM. func listProviders(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (params.Providers, error) { listProvidersResponse, err := apiCli.Providers.ListProviders( diff --git a/test/integration/e2e/credentials.go b/test/integration/e2e/credentials.go index 54c60440..22c0e4fd 100644 --- a/test/integration/e2e/credentials.go +++ b/test/integration/e2e/credentials.go @@ -34,3 +34,185 @@ func createDummyCredentials(name, endpointName string) *params.GithubCredentials } return CreateGithubCredentials(createCredsParams) } + +func TestGithubCredentialsErrorOnDuplicateCredentialsName() { + name := "dummy" + endpointName := "github.com" + creds := createDummyCredentials(name, endpointName) + defer DeleteGithubCredential(int64(creds.ID)) + + createCredsParams := params.CreateGithubCredentialsParams{ + Name: name, + Endpoint: endpointName, + Description: "GARM test credentials", + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "dummy", + }, + } + if _, err := createGithubCredentials(cli, authToken, createCredsParams); err == nil { + panic("expected error when creating credentials with duplicate name") + } +} + +func TestGithubCredentialsFailsToDeleteWhenInUse() { + name := "dummy" + endpointName := "github.com" + creds := createDummyCredentials(name, endpointName) + + repo := CreateRepo("dummy-owner", "dummy-repo", creds.Name, "superSecret@123BlaBla") + defer func() { + deleteRepo(cli, authToken, repo.ID) + deleteGithubCredentials(cli, authToken, int64(creds.ID)) + }() + + if err := deleteGithubCredentials(cli, authToken, int64(creds.ID)); err == nil { + panic("expected error when deleting credentials in use") + } +} + +func TestGithubCredentialsFailsOnInvalidAuthType() { + name := "dummy" + endpointName := "github.com" + + createCredsParams := params.CreateGithubCredentialsParams{ + Name: name, + Endpoint: endpointName, + Description: "GARM test credentials", + AuthType: params.GithubAuthType("invalid"), + PAT: params.GithubPAT{ + OAuth2Token: "dummy", + }, + } + _, err := createGithubCredentials(cli, authToken, createCredsParams) + if err == nil { + panic("expected error when creating credentials with invalid auth type") + } + expectAPIStatusCode(err, 400) +} + +func TestGithubCredentialsFailsWhenAuthTypeParamsAreIncorrect() { + name := "dummy" + endpointName := "github.com" + + createCredsParams := params.CreateGithubCredentialsParams{ + Name: name, + Endpoint: endpointName, + Description: "GARM test credentials", + AuthType: params.GithubAuthTypePAT, + App: params.GithubApp{ + AppID: 123, + InstallationID: 456, + PrivateKeyBytes: getTestFileContents("certs/srv-key.pem"), + }, + } + _, err := createGithubCredentials(cli, authToken, createCredsParams) + if err == nil { + panic("expected error when creating credentials with invalid auth type params") + } + expectAPIStatusCode(err, 400) +} + +func TestGithubCredentialsFailsWhenAuthTypeParamsAreMissing() { + name := "dummy" + endpointName := "github.com" + + createCredsParams := params.CreateGithubCredentialsParams{ + Name: name, + Endpoint: endpointName, + Description: "GARM test credentials", + AuthType: params.GithubAuthTypeApp, + } + _, err := createGithubCredentials(cli, authToken, createCredsParams) + if err == nil { + panic("expected error when creating credentials with missing auth type params") + } + expectAPIStatusCode(err, 400) +} + +func TestGithubCredentialsUpdateFailsWhenBothPATAndAppAreSupplied() { + name := "dummy" + endpointName := "github.com" + creds := createDummyCredentials(name, endpointName) + defer DeleteGithubCredential(int64(creds.ID)) + + updateCredsParams := params.UpdateGithubCredentialsParams{ + PAT: ¶ms.GithubPAT{ + OAuth2Token: "dummy", + }, + App: ¶ms.GithubApp{ + AppID: 123, + InstallationID: 456, + PrivateKeyBytes: getTestFileContents("certs/srv-key.pem"), + }, + } + _, err := updateGithubCredentials(cli, authToken, int64(creds.ID), updateCredsParams) + if err == nil { + panic("expected error when updating credentials with both PAT and App") + } + expectAPIStatusCode(err, 400) +} + +func TestGithubCredentialsFailWhenAppKeyIsInvalid() { + name := "dummy" + endpointName := "github.com" + + createCredsParams := params.CreateGithubCredentialsParams{ + Name: name, + Endpoint: endpointName, + Description: "GARM test credentials", + AuthType: params.GithubAuthTypeApp, + App: params.GithubApp{ + AppID: 123, + InstallationID: 456, + PrivateKeyBytes: []byte("invalid"), + }, + } + _, err := createGithubCredentials(cli, authToken, createCredsParams) + if err == nil { + panic("expected error when creating credentials with invalid app key") + } + expectAPIStatusCode(err, 400) +} + +func TestGithubCredentialsFailWhenEndpointDoesntExist() { + name := "dummy" + endpointName := "nonexistent" + + createCredsParams := params.CreateGithubCredentialsParams{ + Name: name, + Endpoint: endpointName, + Description: "GARM test credentials", + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "dummy", + }, + } + _, err := createGithubCredentials(cli, authToken, createCredsParams) + if err == nil { + panic("expected error when creating credentials with invalid endpoint") + } + expectAPIStatusCode(err, 404) +} + +func TestGithubCredentialsFailsOnDuplicateName() { + name := "dummy" + endpointName := "github.com" + creds := createDummyCredentials(name, endpointName) + defer DeleteGithubCredential(int64(creds.ID)) + + createCredsParams := params.CreateGithubCredentialsParams{ + Name: name, + Endpoint: endpointName, + Description: "GARM test credentials", + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "dummy", + }, + } + _, err := createGithubCredentials(cli, authToken, createCredsParams) + if err == nil { + panic("expected error when creating credentials with duplicate name") + } + expectAPIStatusCode(err, 409) +} diff --git a/test/integration/e2e/e2e.go b/test/integration/e2e/e2e.go index 3bb2f7fe..84742a67 100644 --- a/test/integration/e2e/e2e.go +++ b/test/integration/e2e/e2e.go @@ -77,6 +77,15 @@ func DeleteGithubEndpoint(name string) { } } +func UpdateGithubEndpoint(name string, updateParams params.UpdateGithubEndpointParams) *params.GithubEndpoint { + slog.Info("Update GitHub endpoint") + updated, err := updateGithubEndpoint(cli, authToken, name, updateParams) + if err != nil { + panic(err) + } + return updated +} + func ListProviders() params.Providers { slog.Info("List providers") providers, err := listProviders(cli, authToken) diff --git a/test/integration/e2e/endpoints.go b/test/integration/e2e/endpoints.go index 433da53f..21c0dc1b 100644 --- a/test/integration/e2e/endpoints.go +++ b/test/integration/e2e/endpoints.go @@ -177,3 +177,64 @@ func TestGithubEndpointFailsOnDuplicateName() { panic("expected error when creating endpoint with duplicate name") } } + +func TestGithubEndpointUpdateEndpoint() { + slog.Info("Testing endpoint update") + endpoint := createDummyEndpoint("dummy") + defer DeleteGithubEndpoint(endpoint.Name) + + newDescription := "Updated description" + newBaseURL := "https://ghes2.example.com" + newAPIBaseURL := "https://api.ghes2.example.com/" + newUploadBaseURL := "https://uploads.ghes2.example.com/" + newCABundle := getTestFileContents("certs/srv-pub.pem") + + updateParams := params.UpdateGithubEndpointParams{ + Description: &newDescription, + BaseURL: &newBaseURL, + APIBaseURL: &newAPIBaseURL, + UploadBaseURL: &newUploadBaseURL, + CACertBundle: newCABundle, + } + + updated, err := updateGithubEndpoint(cli, authToken, endpoint.Name, updateParams) + if err != nil { + panic(err) + } + + if updated.Name != endpoint.Name { + panic("Endpoint name mismatch") + } + + if updated.Description != newDescription { + panic("Endpoint description mismatch") + } + + if updated.BaseURL != newBaseURL { + panic("Endpoint base URL mismatch") + } + + if updated.APIBaseURL != newAPIBaseURL { + panic("Endpoint API base URL mismatch") + } + + if updated.UploadBaseURL != newUploadBaseURL { + panic("Endpoint upload base URL mismatch") + } + + if string(updated.CACertBundle) != string(newCABundle) { + panic("Endpoint CA cert bundle mismatch") + } +} + +func createDummyEndpoint(name string) *params.GithubEndpoint { + endpointParams := params.CreateGithubEndpointParams{ + Name: name, + Description: "Dummy endpoint", + BaseURL: "https://ghes.example.com", + APIBaseURL: "https://api.ghes.example.com/", + UploadBaseURL: "https://uploads.ghes.example.com/", + } + + return CreateGithubEndpoint(endpointParams) +} diff --git a/test/integration/e2e/utils.go b/test/integration/e2e/utils.go index 37e48fe4..84c9f9e7 100644 --- a/test/integration/e2e/utils.go +++ b/test/integration/e2e/utils.go @@ -2,6 +2,7 @@ package e2e import ( "encoding/json" + "log" "log/slog" ) @@ -13,3 +14,20 @@ func printJSONResponse(resp interface{}) error { slog.Info(string(b)) return nil } + +type apiCodeGetter interface { + IsCode(code int) bool +} + +func expectAPIStatusCode(err error, expectedCode int) { + if err == nil { + panic("expected error") + } + apiErr, ok := err.(apiCodeGetter) + if !ok { + log.Fatalf("expected API error, got %v (%T)", err, err) + } + if !apiErr.IsCode(expectedCode) { + log.Fatalf("expected status code %d", expectedCode) + } +} diff --git a/test/integration/main.go b/test/integration/main.go index 711478b8..c1ab0de3 100644 --- a/test/integration/main.go +++ b/test/integration/main.go @@ -85,6 +85,15 @@ func main() { // Create test credentials e2e.EnsureTestCredentials(credentialsName, ghToken, "github.com") + e2e.TestGithubCredentialsErrorOnDuplicateCredentialsName() + e2e.TestGithubCredentialsFailsToDeleteWhenInUse() + e2e.TestGithubCredentialsFailsOnInvalidAuthType() + e2e.TestGithubCredentialsFailsWhenAuthTypeParamsAreIncorrect() + e2e.TestGithubCredentialsFailsWhenAuthTypeParamsAreMissing() + e2e.TestGithubCredentialsUpdateFailsWhenBothPATAndAppAreSupplied() + e2e.TestGithubCredentialsFailWhenAppKeyIsInvalid() + e2e.TestGithubCredentialsFailWhenEndpointDoesntExist() + e2e.TestGithubCredentialsFailsOnDuplicateName() // ////////////////// // controller info // From 87943db62c418cbe971c1df329f020fbb0efc8b6 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Sat, 27 Apr 2024 18:10:03 +0000 Subject: [PATCH 19/27] Fix lint and add integration tests logging Signed-off-by: Gabriel Adrian Samfira --- test/integration/e2e/credentials.go | 70 ++++++++++++----------------- test/integration/e2e/endpoints.go | 7 +++ test/integration/e2e/utils.go | 2 +- 3 files changed, 37 insertions(+), 42 deletions(-) diff --git a/test/integration/e2e/credentials.go b/test/integration/e2e/credentials.go index 22c0e4fd..2ca027c5 100644 --- a/test/integration/e2e/credentials.go +++ b/test/integration/e2e/credentials.go @@ -2,11 +2,13 @@ package e2e import ( "fmt" + "log/slog" "github.com/cloudbase/garm/params" ) func EnsureTestCredentials(name string, oauthToken string, endpointName string) { + slog.Info("Ensuring test credentials exist") createCredsParams := params.CreateGithubCredentialsParams{ Name: name, Endpoint: endpointName, @@ -36,14 +38,13 @@ func createDummyCredentials(name, endpointName string) *params.GithubCredentials } func TestGithubCredentialsErrorOnDuplicateCredentialsName() { - name := "dummy" - endpointName := "github.com" - creds := createDummyCredentials(name, endpointName) + slog.Info("Testing error on duplicate credentials name") + creds := createDummyCredentials(dummyCredentialsName, defaultEndpointName) defer DeleteGithubCredential(int64(creds.ID)) createCredsParams := params.CreateGithubCredentialsParams{ - Name: name, - Endpoint: endpointName, + Name: dummyCredentialsName, + Endpoint: defaultEndpointName, Description: "GARM test credentials", AuthType: params.GithubAuthTypePAT, PAT: params.GithubPAT{ @@ -56,9 +57,8 @@ func TestGithubCredentialsErrorOnDuplicateCredentialsName() { } func TestGithubCredentialsFailsToDeleteWhenInUse() { - name := "dummy" - endpointName := "github.com" - creds := createDummyCredentials(name, endpointName) + slog.Info("Testing error when deleting credentials in use") + creds := createDummyCredentials(dummyCredentialsName, defaultEndpointName) repo := CreateRepo("dummy-owner", "dummy-repo", creds.Name, "superSecret@123BlaBla") defer func() { @@ -72,12 +72,10 @@ func TestGithubCredentialsFailsToDeleteWhenInUse() { } func TestGithubCredentialsFailsOnInvalidAuthType() { - name := "dummy" - endpointName := "github.com" - + slog.Info("Testing error on invalid auth type") createCredsParams := params.CreateGithubCredentialsParams{ - Name: name, - Endpoint: endpointName, + Name: dummyCredentialsName, + Endpoint: defaultEndpointName, Description: "GARM test credentials", AuthType: params.GithubAuthType("invalid"), PAT: params.GithubPAT{ @@ -92,12 +90,10 @@ func TestGithubCredentialsFailsOnInvalidAuthType() { } func TestGithubCredentialsFailsWhenAuthTypeParamsAreIncorrect() { - name := "dummy" - endpointName := "github.com" - + slog.Info("Testing error when auth type params are incorrect") createCredsParams := params.CreateGithubCredentialsParams{ - Name: name, - Endpoint: endpointName, + Name: dummyCredentialsName, + Endpoint: defaultEndpointName, Description: "GARM test credentials", AuthType: params.GithubAuthTypePAT, App: params.GithubApp{ @@ -114,12 +110,10 @@ func TestGithubCredentialsFailsWhenAuthTypeParamsAreIncorrect() { } func TestGithubCredentialsFailsWhenAuthTypeParamsAreMissing() { - name := "dummy" - endpointName := "github.com" - + slog.Info("Testing error when auth type params are missing") createCredsParams := params.CreateGithubCredentialsParams{ - Name: name, - Endpoint: endpointName, + Name: dummyCredentialsName, + Endpoint: defaultEndpointName, Description: "GARM test credentials", AuthType: params.GithubAuthTypeApp, } @@ -131,9 +125,8 @@ func TestGithubCredentialsFailsWhenAuthTypeParamsAreMissing() { } func TestGithubCredentialsUpdateFailsWhenBothPATAndAppAreSupplied() { - name := "dummy" - endpointName := "github.com" - creds := createDummyCredentials(name, endpointName) + slog.Info("Testing error when both PAT and App are supplied") + creds := createDummyCredentials(dummyCredentialsName, defaultEndpointName) defer DeleteGithubCredential(int64(creds.ID)) updateCredsParams := params.UpdateGithubCredentialsParams{ @@ -154,12 +147,10 @@ func TestGithubCredentialsUpdateFailsWhenBothPATAndAppAreSupplied() { } func TestGithubCredentialsFailWhenAppKeyIsInvalid() { - name := "dummy" - endpointName := "github.com" - + slog.Info("Testing error when app key is invalid") createCredsParams := params.CreateGithubCredentialsParams{ - Name: name, - Endpoint: endpointName, + Name: dummyCredentialsName, + Endpoint: defaultEndpointName, Description: "GARM test credentials", AuthType: params.GithubAuthTypeApp, App: params.GithubApp{ @@ -176,12 +167,10 @@ func TestGithubCredentialsFailWhenAppKeyIsInvalid() { } func TestGithubCredentialsFailWhenEndpointDoesntExist() { - name := "dummy" - endpointName := "nonexistent" - + slog.Info("Testing error when endpoint doesn't exist") createCredsParams := params.CreateGithubCredentialsParams{ - Name: name, - Endpoint: endpointName, + Name: dummyCredentialsName, + Endpoint: defaultEndpointName, Description: "GARM test credentials", AuthType: params.GithubAuthTypePAT, PAT: params.GithubPAT{ @@ -196,14 +185,13 @@ func TestGithubCredentialsFailWhenEndpointDoesntExist() { } func TestGithubCredentialsFailsOnDuplicateName() { - name := "dummy" - endpointName := "github.com" - creds := createDummyCredentials(name, endpointName) + slog.Info("Testing error on duplicate credentials name") + creds := createDummyCredentials(dummyCredentialsName, defaultEndpointName) defer DeleteGithubCredential(int64(creds.ID)) createCredsParams := params.CreateGithubCredentialsParams{ - Name: name, - Endpoint: endpointName, + Name: dummyCredentialsName, + Endpoint: defaultEndpointName, Description: "GARM test credentials", AuthType: params.GithubAuthTypePAT, PAT: params.GithubPAT{ diff --git a/test/integration/e2e/endpoints.go b/test/integration/e2e/endpoints.go index 21c0dc1b..9a276627 100644 --- a/test/integration/e2e/endpoints.go +++ b/test/integration/e2e/endpoints.go @@ -8,6 +8,11 @@ import ( "github.com/cloudbase/garm/params" ) +const ( + defaultEndpointName string = "github.com" + dummyCredentialsName string = "dummy" +) + func MustDefaultGithubEndpoint() { ep := GetGithubEndpoint("github.com") if ep == nil { @@ -58,6 +63,7 @@ func getTestFileContents(relPath string) []byte { } func TestGithubEndpointOperations() { + slog.Info("Testing endpoint operations") MustDefaultGithubEndpoint() caBundle := getTestFileContents("certs/srv-pub.pem") @@ -119,6 +125,7 @@ func TestGithubEndpointOperations() { } func TestGithubEndpointMustFailToDeleteDefaultGithubEndpoint() { + slog.Info("Testing error when deleting default github.com endpoint") if err := deleteGithubEndpoint(cli, authToken, "github.com"); err == nil { panic("expected error when attempting to delete the default github.com endpoint") } diff --git a/test/integration/e2e/utils.go b/test/integration/e2e/utils.go index 84c9f9e7..3c449ddc 100644 --- a/test/integration/e2e/utils.go +++ b/test/integration/e2e/utils.go @@ -28,6 +28,6 @@ func expectAPIStatusCode(err error, expectedCode int) { log.Fatalf("expected API error, got %v (%T)", err, err) } if !apiErr.IsCode(expectedCode) { - log.Fatalf("expected status code %d", expectedCode) + log.Fatalf("expected status code %d: %v", expectedCode, err) } } From 349ba1f9e8485ed88d99c4010e5534ac7b58b332 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Sat, 27 Apr 2024 18:17:35 +0000 Subject: [PATCH 20/27] Fix nil pointer dereference Signed-off-by: Gabriel Adrian Samfira --- cmd/garm-cli/cmd/github_credentials.go | 22 ++++++++++------------ params/requests.go | 3 +++ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/cmd/garm-cli/cmd/github_credentials.go b/cmd/garm-cli/cmd/github_credentials.go index d328f5ee..ca345332 100644 --- a/cmd/garm-cli/cmd/github_credentials.go +++ b/cmd/garm-cli/cmd/github_credentials.go @@ -15,8 +15,6 @@ package cmd import ( - "crypto/x509" - "encoding/pem" "fmt" "os" "strconv" @@ -260,20 +258,20 @@ func init() { } func parsePrivateKeyFromPath(path string) ([]byte, error) { - if _, err := os.Stat(path); err != nil { - return nil, fmt.Errorf("private key file not found: %s", credentialsPrivateKeyPath) - } + // if _, err := os.Stat(path); err != nil { + // return nil, fmt.Errorf("private key file not found: %s", credentialsPrivateKeyPath) + // } keyContents, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read private key file: %w", err) } - pemBlock, _ := pem.Decode(keyContents) - if pemBlock == nil { - return nil, fmt.Errorf("failed to decode PEM block") - } - if _, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err != nil { - return nil, fmt.Errorf("failed to parse private key: %w", err) - } + // pemBlock, _ := pem.Decode(keyContents) + // if pemBlock == nil { + // return nil, fmt.Errorf("failed to decode PEM block") + // } + // if _, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err != nil { + // return nil, fmt.Errorf("failed to parse private key: %w", err) + // } return keyContents, nil } diff --git a/params/requests.go b/params/requests.go index db7e7da2..4a842ef6 100644 --- a/params/requests.go +++ b/params/requests.go @@ -424,6 +424,9 @@ func (g GithubApp) Validate() error { } block, _ := pem.Decode(g.PrivateKeyBytes) + if block == nil { + return runnerErrors.NewBadRequestError("invalid private_key_bytes") + } // Parse the private key as PCKS1 _, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { From 402c8b70e22ccba3b0f6f98da910a88abaf2afc6 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Sat, 27 Apr 2024 18:22:21 +0000 Subject: [PATCH 21/27] Use different creds than the default ones Signed-off-by: Gabriel Adrian Samfira --- cmd/garm-cli/cmd/github_credentials.go | 24 +++++++++++++----------- params/params.go | 1 + test/integration/e2e/credentials.go | 2 +- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/cmd/garm-cli/cmd/github_credentials.go b/cmd/garm-cli/cmd/github_credentials.go index ca345332..5f09a8e4 100644 --- a/cmd/garm-cli/cmd/github_credentials.go +++ b/cmd/garm-cli/cmd/github_credentials.go @@ -15,6 +15,8 @@ package cmd import ( + "crypto/x509" + "encoding/pem" "fmt" "os" "strconv" @@ -41,7 +43,7 @@ var ( var credentialsCmd = &cobra.Command{ Use: "credentials", Aliases: []string{"creds"}, - Short: "List configured credentials", + Short: "List configured credentials. This is an alias for the github credentials command.", Long: `List all available github credentials. This command is an alias for the garm-cli github credentials command.`, @@ -258,20 +260,20 @@ func init() { } func parsePrivateKeyFromPath(path string) ([]byte, error) { - // if _, err := os.Stat(path); err != nil { - // return nil, fmt.Errorf("private key file not found: %s", credentialsPrivateKeyPath) - // } + if _, err := os.Stat(path); err != nil { + return nil, fmt.Errorf("private key file not found: %s", credentialsPrivateKeyPath) + } keyContents, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read private key file: %w", err) } - // pemBlock, _ := pem.Decode(keyContents) - // if pemBlock == nil { - // return nil, fmt.Errorf("failed to decode PEM block") - // } - // if _, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err != nil { - // return nil, fmt.Errorf("failed to parse private key: %w", err) - // } + pemBlock, _ := pem.Decode(keyContents) + if pemBlock == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + if _, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } return keyContents, nil } diff --git a/params/params.go b/params/params.go index f9629fc7..b17a2d14 100644 --- a/params/params.go +++ b/params/params.go @@ -585,6 +585,7 @@ type GithubCredentials struct { Enterprises []Enterprise `json:"enterprises,omitempty"` Endpoint GithubEndpoint `json:"endpoint"` + // Do not serialize sensitive info. CredentialsPayload []byte `json:"-"` } diff --git a/test/integration/e2e/credentials.go b/test/integration/e2e/credentials.go index 2ca027c5..67e1d35c 100644 --- a/test/integration/e2e/credentials.go +++ b/test/integration/e2e/credentials.go @@ -170,7 +170,7 @@ func TestGithubCredentialsFailWhenEndpointDoesntExist() { slog.Info("Testing error when endpoint doesn't exist") createCredsParams := params.CreateGithubCredentialsParams{ Name: dummyCredentialsName, - Endpoint: defaultEndpointName, + Endpoint: "iDontExist.example.com", Description: "GARM test credentials", AuthType: params.GithubAuthTypePAT, PAT: params.GithubPAT{ From 2b1414d1505a22286ae8689657949525e5734c9f Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Sun, 28 Apr 2024 17:20:40 +0000 Subject: [PATCH 22/27] Add some unit tests Signed-off-by: Gabriel Adrian Samfira --- database/sql/github.go | 8 +- database/sql/github_test.go | 305 ++++++++++++++++++++++++++++++++++++ 2 files changed, 310 insertions(+), 3 deletions(-) create mode 100644 database/sql/github_test.go diff --git a/database/sql/github.go b/database/sql/github.go index 33dfe7da..7878c246 100644 --- a/database/sql/github.go +++ b/database/sql/github.go @@ -196,7 +196,7 @@ func (s *sqlDatabase) GetGithubEndpoint(_ context.Context, name string) (params. err := s.conn.Where("name = ?", name).First(&endpoint).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return params.GithubEndpoint{}, errors.Wrap(err, "github endpoint not found") + return params.GithubEndpoint{}, errors.Wrap(runnerErrors.ErrNotFound, "github endpoint not found") } return params.GithubEndpoint{}, errors.Wrap(err, "fetching github endpoint") } @@ -215,7 +215,7 @@ func (s *sqlDatabase) DeleteGithubEndpoint(_ context.Context, name string) error } if endpoint.Name == "github.com" { - return errors.New("cannot delete default github endpoint") + return errors.Wrap(runnerErrors.ErrBadRequest, "cannot delete default github endpoint") } var credsCount int64 @@ -267,7 +267,7 @@ func (s *sqlDatabase) CreateGithubCredentials(ctx context.Context, param params. return params.GithubCredentials{}, errors.Wrap(err, "creating github credentials") } if param.Endpoint == "" { - return params.GithubCredentials{}, errors.New("endpoint name is required") + return params.GithubCredentials{}, errors.Wrap(runnerErrors.ErrBadRequest, "endpoint name is required") } var creds GithubCredentials err = s.conn.Transaction(func(tx *gorm.DB) error { @@ -290,6 +290,8 @@ func (s *sqlDatabase) CreateGithubCredentials(ctx context.Context, param params. data, err = s.marshalAndSeal(param.PAT) case params.GithubAuthTypeApp: data, err = s.marshalAndSeal(param.App) + default: + return errors.Wrap(runnerErrors.ErrBadRequest, "invalid auth type") } if err != nil { return errors.Wrap(err, "marshaling and sealing credentials") diff --git a/database/sql/github_test.go b/database/sql/github_test.go new file mode 100644 index 00000000..2f6144e6 --- /dev/null +++ b/database/sql/github_test.go @@ -0,0 +1,305 @@ +// Copyright 2024 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 sql + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/suite" + + runnerErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/database/common" + garmTesting "github.com/cloudbase/garm/internal/testing" + "github.com/cloudbase/garm/params" +) + +const ( + defaultGithubEndpoint string = "github.com" + testUploadBaseURL string = "https://uploads.example.com" + testBaseURL string = "https://example.com" + testAPIBaseURL string = "https://api.example.com" + testEndpointName string = "test-endpoint" + testEndpointDescription string = "test description" + testCredsName string = "test-creds" + testCredsDescription string = "test creds" +) + +type GithubTestSuite struct { + suite.Suite + + db common.Store +} + +func (s *GithubTestSuite) SetupTest() { + db, err := NewSQLDatabase(context.Background(), garmTesting.GetTestSqliteDBConfig(s.T())) + if err != nil { + s.FailNow(fmt.Sprintf("failed to create db connection: %s", err)) + } + s.db = db +} + +func (s *GithubTestSuite) TestDefaultEndpointGetsCreatedAutomatically() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + endpoint, err := s.db.GetGithubEndpoint(ctx, defaultGithubEndpoint) + s.Require().NoError(err) + s.Require().NotNil(endpoint) +} + +func (s *GithubTestSuite) TestDeletingDefaultEndpointFails() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + err := s.db.DeleteGithubEndpoint(ctx, defaultGithubEndpoint) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrBadRequest) +} + +func (s *GithubTestSuite) TestCreatingEndpoint() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + createEpParams := params.CreateGithubEndpointParams{ + Name: testEndpointName, + Description: testEndpointDescription, + APIBaseURL: testAPIBaseURL, + UploadBaseURL: testUploadBaseURL, + BaseURL: testBaseURL, + } + + endpoint, err := s.db.CreateGithubEndpoint(ctx, createEpParams) + s.Require().NoError(err) + s.Require().NotNil(endpoint) + s.Require().Equal(testEndpointName, endpoint.Name) +} + +func (s *GithubTestSuite) TestCreatingDuplicateEndpointFails() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + createEpParams := params.CreateGithubEndpointParams{ + Name: testEndpointName, + Description: testEndpointDescription, + APIBaseURL: testAPIBaseURL, + UploadBaseURL: testUploadBaseURL, + BaseURL: testBaseURL, + } + + _, err := s.db.CreateGithubEndpoint(ctx, createEpParams) + s.Require().NoError(err) + + _, err = s.db.CreateGithubEndpoint(ctx, createEpParams) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrDuplicateEntity) +} + +func (s *GithubTestSuite) TestGetEndpoint() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + endpoint, err := s.db.GetGithubEndpoint(ctx, defaultGithubEndpoint) + s.Require().NoError(err) + s.Require().NotNil(endpoint) + s.Require().Equal(defaultGithubEndpoint, endpoint.Name) +} + +func (s *GithubTestSuite) TestGetNonExistingEndpointFailsWithNotFoundError() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + _, err := s.db.GetGithubEndpoint(ctx, "non-existing") + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GithubTestSuite) TestDeletingNonExistingEndpointIsANoop() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + err := s.db.DeleteGithubEndpoint(ctx, "non-existing") + s.Require().NoError(err) +} + +func (s *GithubTestSuite) TestDeletingEndpoint() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + createEpParams := params.CreateGithubEndpointParams{ + Name: testEndpointName, + Description: testEndpointDescription, + APIBaseURL: testAPIBaseURL, + UploadBaseURL: testUploadBaseURL, + BaseURL: testBaseURL, + } + + endpoint, err := s.db.CreateGithubEndpoint(ctx, createEpParams) + s.Require().NoError(err) + s.Require().NotNil(endpoint) + + err = s.db.DeleteGithubEndpoint(ctx, testEndpointName) + s.Require().NoError(err) + + _, err = s.db.GetGithubEndpoint(ctx, testEndpointName) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GithubTestSuite) TestUpdateEndpoint() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + createEpParams := params.CreateGithubEndpointParams{ + Name: testEndpointName, + Description: testEndpointDescription, + APIBaseURL: testAPIBaseURL, + UploadBaseURL: testUploadBaseURL, + BaseURL: testBaseURL, + } + + endpoint, err := s.db.CreateGithubEndpoint(ctx, createEpParams) + s.Require().NoError(err) + s.Require().NotNil(endpoint) + + newDescription := "new description" + newAPIBaseURL := "https://new-api.example.com" + newUploadBaseURL := "https://new-uploads.example.com" + newBaseURL := "https://new.example.com" + caCertBundle, err := os.ReadFile("../../testdata/certs/srv-pub.pem") + s.Require().NoError(err) + updateEpParams := params.UpdateGithubEndpointParams{ + Description: &newDescription, + APIBaseURL: &newAPIBaseURL, + UploadBaseURL: &newUploadBaseURL, + BaseURL: &newBaseURL, + CACertBundle: caCertBundle, + } + + updatedEndpoint, err := s.db.UpdateGithubEndpoint(ctx, testEndpointName, updateEpParams) + s.Require().NoError(err) + s.Require().NotNil(updatedEndpoint) + s.Require().Equal(newDescription, updatedEndpoint.Description) + s.Require().Equal(newAPIBaseURL, updatedEndpoint.APIBaseURL) + s.Require().Equal(newUploadBaseURL, updatedEndpoint.UploadBaseURL) + s.Require().Equal(newBaseURL, updatedEndpoint.BaseURL) + s.Require().Equal(caCertBundle, updatedEndpoint.CACertBundle) +} + +func (s *GithubTestSuite) TestUpdatingNonExistingEndpointReturnsNotFoundError() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + newDescription := "test" + updateEpParams := params.UpdateGithubEndpointParams{ + Description: &newDescription, + } + + _, err := s.db.UpdateGithubEndpoint(ctx, "non-existing", updateEpParams) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GithubTestSuite) TestListEndpoints() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + createEpParams := params.CreateGithubEndpointParams{ + Name: testEndpointName, + Description: testEndpointDescription, + APIBaseURL: testAPIBaseURL, + UploadBaseURL: testUploadBaseURL, + BaseURL: testBaseURL, + } + + _, err := s.db.CreateGithubEndpoint(ctx, createEpParams) + s.Require().NoError(err) + + endpoints, err := s.db.ListGithubEndpoints(ctx) + s.Require().NoError(err) + s.Require().Len(endpoints, 2) +} + +func (s *GithubTestSuite) TestCreateCredentialsFailsWithUnauthorizedForAnonUser() { + ctx := context.Background() + + _, err := s.db.CreateGithubCredentials(ctx, params.CreateGithubCredentialsParams{}) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized) +} + +func (s *GithubTestSuite) TestCreateCredentialsFailsWhenEndpointNameIsEmpty() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + _, err := s.db.CreateGithubCredentials(ctx, params.CreateGithubCredentialsParams{}) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrBadRequest) + s.Require().Regexp("endpoint name is required", err.Error()) +} + +func (s *GithubTestSuite) TestCreateCredentialsFailsWhenEndpointDoesNotExist() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + _, err := s.db.CreateGithubCredentials(ctx, params.CreateGithubCredentialsParams{Endpoint: "non-existing"}) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) + s.Require().Regexp("endpoint not found", err.Error()) +} + +func (s *GithubTestSuite) TestCreateCredentialsFailsWhenAuthTypeIsInvalid() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + _, err := s.db.CreateGithubCredentials(ctx, params.CreateGithubCredentialsParams{Endpoint: defaultGithubEndpoint, AuthType: "invalid"}) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrBadRequest) + s.Require().Regexp("invalid auth type", err.Error()) +} + +func (s *GithubTestSuite) TestCreateCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + credParams := params.CreateGithubCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: defaultGithubEndpoint, + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + creds, err := s.db.CreateGithubCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + s.Require().Equal(credParams.Name, creds.Name) + s.Require().Equal(credParams.Description, creds.Description) + s.Require().Equal(credParams.Endpoint, creds.Endpoint.Name) + s.Require().Equal(credParams.AuthType, creds.AuthType) +} + +func (s *GithubTestSuite) TestCreateCredentialsFailsOnDuplicateCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + credParams := params.CreateGithubCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: defaultGithubEndpoint, + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + _, err := s.db.CreateGithubCredentials(ctx, credParams) + s.Require().NoError(err) + + _, err = s.db.CreateGithubCredentials(ctx, credParams) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrDuplicateEntity) +} + +func TestGithubTestSuite(t *testing.T) { + t.Parallel() + suite.Run(t, new(GithubTestSuite)) +} From 2a3d524a71adbf5fa5374137fc4073c6d4699e43 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Mon, 29 Apr 2024 09:47:26 +0000 Subject: [PATCH 23/27] Add more unit tests Signed-off-by: Gabriel Adrian Samfira --- database/sql/enterprise_test.go | 23 +- database/sql/github.go | 22 +- database/sql/github_test.go | 436 +++++++++++++++++++++++++++++ database/sql/organizations_test.go | 23 +- database/sql/repositories_test.go | 23 +- internal/testing/testing.go | 25 ++ runner/repositories_test.go | 3 +- 7 files changed, 516 insertions(+), 39 deletions(-) diff --git a/database/sql/enterprise_test.go b/database/sql/enterprise_test.go index 2a26c11d..1283ead7 100644 --- a/database/sql/enterprise_test.go +++ b/database/sql/enterprise_test.go @@ -29,6 +29,7 @@ import ( "gorm.io/gorm/logger" runnerErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/auth" dbCommon "github.com/cloudbase/garm/database/common" garmTesting "github.com/cloudbase/garm/internal/testing" "github.com/cloudbase/garm/params" @@ -50,7 +51,9 @@ type EnterpriseTestSuite struct { StoreSQLMocked *sqlDatabase Fixtures *EnterpriseTestFixtures - adminCtx context.Context + adminCtx context.Context + adminUserID string + testCreds params.GithubCredentials secondaryTestCreds params.GithubCredentials githubEndpoint params.GithubEndpoint @@ -84,6 +87,8 @@ func (s *EnterpriseTestSuite) SetupTest() { adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T()) s.adminCtx = adminCtx + s.adminUserID = auth.UserID(adminCtx) + s.Require().NotEmpty(s.adminUserID) s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) s.testCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), s.githubEndpoint) @@ -216,8 +221,8 @@ func (s *EnterpriseTestSuite) TestCreateEnterpriseInvalidDBPassphrase() { func (s *EnterpriseTestSuite) TestCreateEnterpriseDBCreateErr() { s.Fixtures.SQLMock.ExpectBegin() s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). - WithArgs(s.Fixtures.Enterprises[0].CredentialsName, 1). + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE user_id = ? AND name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.adminUserID, s.Fixtures.Enterprises[0].CredentialsName, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}).AddRow(s.testCreds.ID, s.testCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). WithArgs(s.testCreds.Endpoint.Name). @@ -353,8 +358,8 @@ func (s *EnterpriseTestSuite) TestUpdateEnterpriseDBEncryptErr() { WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.Fixtures.Enterprises[0].ID, s.Fixtures.Enterprises[0].Endpoint.Name)) s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). - WithArgs(s.secondaryTestCreds.Name, 1). + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE user_id = ? AND name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.adminUserID, s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). @@ -378,8 +383,8 @@ func (s *EnterpriseTestSuite) TestUpdateEnterpriseDBSaveErr() { WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.Fixtures.Enterprises[0].ID, s.Fixtures.Enterprises[0].Endpoint.Name)) s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). - WithArgs(s.secondaryTestCreds.Name, 1). + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE user_id = ? AND name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.adminUserID, s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). @@ -409,8 +414,8 @@ func (s *EnterpriseTestSuite) TestUpdateEnterpriseDBDecryptingErr() { WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.Fixtures.Enterprises[0].ID, s.Fixtures.Enterprises[0].Endpoint.Name)) s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). - WithArgs(s.secondaryTestCreds.Name, 1). + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE user_id = ? AND name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.adminUserID, s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). diff --git a/database/sql/github.go b/database/sql/github.go index 7878c246..7a775e31 100644 --- a/database/sql/github.go +++ b/database/sql/github.go @@ -331,15 +331,13 @@ func (s *sqlDatabase) getGithubCredentialsByName(ctx context.Context, tx *gorm.D Preload("Enterprises") } - if !auth.IsAdmin(ctx) { - userID, err := getUIDFromContext(ctx) - if err != nil { - return GithubCredentials{}, errors.Wrap(err, "fetching github credentials") - } - q = q.Where("user_id = ?", userID) + userID, err := getUIDFromContext(ctx) + if err != nil { + return GithubCredentials{}, errors.Wrap(err, "fetching github credentials") } + q = q.Where("user_id = ?", userID) - err := q.Where("name = ?", name).First(&creds).Error + err = q.Where("name = ?", name).First(&creds).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return GithubCredentials{}, errors.Wrap(runnerErrors.ErrNotFound, "github credentials not found") @@ -461,6 +459,10 @@ func (s *sqlDatabase) UpdateGithubCredentials(ctx context.Context, id uint, para if param.PAT != nil { return errors.Wrap(runnerErrors.ErrBadRequest, "cannot update PAT credentials for app") } + default: + // This should never happen, unless there was a bug in the DB migration code, + // or the DB was manually modified. + return errors.Wrap(runnerErrors.ErrBadRequest, "invalid auth type") } if err != nil { @@ -504,13 +506,13 @@ func (s *sqlDatabase) DeleteGithubCredentials(ctx context.Context, id uint) erro return errors.Wrap(err, "fetching github credentials") } if len(creds.Repositories) > 0 { - return errors.New("cannot delete credentials with repositories") + return errors.Wrap(runnerErrors.ErrBadRequest, "cannot delete credentials with repositories") } if len(creds.Organizations) > 0 { - return errors.New("cannot delete credentials with organizations") + return errors.Wrap(runnerErrors.ErrBadRequest, "cannot delete credentials with organizations") } if len(creds.Enterprises) > 0 { - return errors.New("cannot delete credentials with enterprises") + return errors.Wrap(runnerErrors.ErrBadRequest, "cannot delete credentials with enterprises") } if err := tx.Unscoped().Delete(&creds).Error; err != nil { diff --git a/database/sql/github_test.go b/database/sql/github_test.go index 2f6144e6..0098418b 100644 --- a/database/sql/github_test.go +++ b/database/sql/github_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/suite" runnerErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/auth" "github.com/cloudbase/garm/database/common" garmTesting "github.com/cloudbase/garm/internal/testing" "github.com/cloudbase/garm/params" @@ -280,6 +281,8 @@ func (s *GithubTestSuite) TestCreateCredentials() { func (s *GithubTestSuite) TestCreateCredentialsFailsOnDuplicateCredentials() { ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + testUser := garmTesting.CreateGARMTestUser(ctx, "testuser", s.db, s.T()) + testUserCtx := auth.PopulateContext(context.Background(), testUser) credParams := params.CreateGithubCredentialsParams{ Name: testCredsName, @@ -294,9 +297,442 @@ func (s *GithubTestSuite) TestCreateCredentialsFailsOnDuplicateCredentials() { _, err := s.db.CreateGithubCredentials(ctx, credParams) s.Require().NoError(err) + // Creating creds with the same parameters should fail for the same user. _, err = s.db.CreateGithubCredentials(ctx, credParams) s.Require().Error(err) s.Require().ErrorIs(err, runnerErrors.ErrDuplicateEntity) + + // Creating creds with the same parameters should work for different users. + _, err = s.db.CreateGithubCredentials(testUserCtx, credParams) + s.Require().NoError(err) +} + +func (s *GithubTestSuite) TestNormalUsersCanOnlySeeTheirOwnCredentialsAdminCanSeeAll() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + testUser := garmTesting.CreateGARMTestUser(ctx, "testuser1", s.db, s.T()) + testUser2 := garmTesting.CreateGARMTestUser(ctx, "testuser2", s.db, s.T()) + testUserCtx := auth.PopulateContext(context.Background(), testUser) + testUser2Ctx := auth.PopulateContext(context.Background(), testUser2) + + credParams := params.CreateGithubCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: defaultGithubEndpoint, + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + creds, err := s.db.CreateGithubCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + credParams.Name = "test-creds2" + creds2, err := s.db.CreateGithubCredentials(testUserCtx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds2) + + credParams.Name = "test-creds3" + creds3, err := s.db.CreateGithubCredentials(testUser2Ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds3) + + credsList, err := s.db.ListGithubCredentials(ctx) + s.Require().NoError(err) + s.Require().Len(credsList, 3) + + credsList, err = s.db.ListGithubCredentials(testUserCtx) + s.Require().NoError(err) + s.Require().Len(credsList, 1) + s.Require().Equal("test-creds2", credsList[0].Name) + + credsList, err = s.db.ListGithubCredentials(testUser2Ctx) + s.Require().NoError(err) + s.Require().Len(credsList, 1) + s.Require().Equal("test-creds3", credsList[0].Name) +} + +func (s *GithubTestSuite) TestGetGithubCredentialsFailsWhenCredentialsDontExist() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + _, err := s.db.GetGithubCredentials(ctx, 1, true) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) + + _, err = s.db.GetGithubCredentialsByName(ctx, "non-existing", true) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GithubTestSuite) TestGetGithubCredentialsByNameReturnsOnlyCurrentUserCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + testUser := garmTesting.CreateGARMTestUser(ctx, "test-user1", s.db, s.T()) + testUserCtx := auth.PopulateContext(context.Background(), testUser) + + credParams := params.CreateGithubCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: defaultGithubEndpoint, + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + creds, err := s.db.CreateGithubCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + creds2, err := s.db.CreateGithubCredentials(testUserCtx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds2) + + creds2Get, err := s.db.GetGithubCredentialsByName(testUserCtx, testCredsName, true) + s.Require().NoError(err) + s.Require().NotNil(creds2) + s.Require().Equal(testCredsName, creds2Get.Name) + s.Require().Equal(creds2.ID, creds2Get.ID) + + credsGet, err := s.db.GetGithubCredentialsByName(ctx, testCredsName, true) + s.Require().NoError(err) + s.Require().NotNil(creds) + s.Require().Equal(testCredsName, credsGet.Name) + s.Require().Equal(creds.ID, credsGet.ID) + + // Admin can get any creds by ID + credsGet, err = s.db.GetGithubCredentials(ctx, creds2.ID, true) + s.Require().NoError(err) + s.Require().NotNil(creds2) + s.Require().Equal(creds2.ID, credsGet.ID) + + // Normal user cannot get other user creds by ID + _, err = s.db.GetGithubCredentials(testUserCtx, creds.ID, true) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GithubTestSuite) TestGetGithubCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + credParams := params.CreateGithubCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: defaultGithubEndpoint, + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + creds, err := s.db.CreateGithubCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + creds2, err := s.db.GetGithubCredentialsByName(ctx, testCredsName, true) + s.Require().NoError(err) + s.Require().NotNil(creds2) + s.Require().Equal(creds.Name, creds2.Name) + s.Require().Equal(creds.ID, creds2.ID) + + creds2, err = s.db.GetGithubCredentials(ctx, creds.ID, true) + s.Require().NoError(err) + s.Require().NotNil(creds2) + s.Require().Equal(creds.Name, creds2.Name) + s.Require().Equal(creds.ID, creds2.ID) +} + +func (s *GithubTestSuite) TestDeleteGithubCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + credParams := params.CreateGithubCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: defaultGithubEndpoint, + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + creds, err := s.db.CreateGithubCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + err = s.db.DeleteGithubCredentials(ctx, creds.ID) + s.Require().NoError(err) + + _, err = s.db.GetGithubCredentials(ctx, creds.ID, true) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GithubTestSuite) TestDeleteGithubCredentialsByNonAdminUser() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + testUser := garmTesting.CreateGARMTestUser(ctx, "test-user4", s.db, s.T()) + testUserCtx := auth.PopulateContext(context.Background(), testUser) + + credParams := params.CreateGithubCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: defaultGithubEndpoint, + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test-creds4", + }, + } + + // Create creds as admin + creds, err := s.db.CreateGithubCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + // Deleting non existent creds will return a nil error. For the test user + // the creds created by the admin should not be visible, which leads to not found + // which in turn returns no error. + err = s.db.DeleteGithubCredentials(testUserCtx, creds.ID) + s.Require().NoError(err) + + // Check that the creds created by the admin are still there. + credsGet, err := s.db.GetGithubCredentials(ctx, creds.ID, true) + s.Require().NoError(err) + s.Require().NotNil(credsGet) + s.Require().Equal(creds.ID, credsGet.ID) + + // Create the same creds with the test user. + creds2, err := s.db.CreateGithubCredentials(testUserCtx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds2) + + // Remove creds created by test user. + err = s.db.DeleteGithubCredentials(testUserCtx, creds2.ID) + s.Require().NoError(err) + + // The creds created by the test user should be gone. + _, err = s.db.GetGithubCredentials(testUserCtx, creds2.ID, true) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GithubTestSuite) TestDeleteCredentialsFailsIfReposOrgsOrEntitiesUseIt() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + credParams := params.CreateGithubCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: defaultGithubEndpoint, + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + creds, err := s.db.CreateGithubCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + repo, err := s.db.CreateRepository(ctx, "test-owner", "test-repo", creds.Name, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) + s.Require().NoError(err) + s.Require().NotNil(repo) + + err = s.db.DeleteGithubCredentials(ctx, creds.ID) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrBadRequest) + + err = s.db.DeleteRepository(ctx, repo.ID) + s.Require().NoError(err) + + org, err := s.db.CreateOrganization(ctx, "test-org", creds.Name, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) + s.Require().NoError(err) + s.Require().NotNil(org) + + err = s.db.DeleteGithubCredentials(ctx, creds.ID) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrBadRequest) + + err = s.db.DeleteOrganization(ctx, org.ID) + s.Require().NoError(err) + + enterprise, err := s.db.CreateEnterprise(ctx, "test-enterprise", creds.Name, "superSecret@123BlaBla", params.PoolBalancerTypeRoundRobin) + s.Require().NoError(err) + s.Require().NotNil(enterprise) + + err = s.db.DeleteGithubCredentials(ctx, creds.ID) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrBadRequest) + + err = s.db.DeleteEnterprise(ctx, enterprise.ID) + s.Require().NoError(err) + + err = s.db.DeleteGithubCredentials(ctx, creds.ID) + s.Require().NoError(err) + + _, err = s.db.GetGithubCredentials(ctx, creds.ID, true) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GithubTestSuite) TestUpdateCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + credParams := params.CreateGithubCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: defaultGithubEndpoint, + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + creds, err := s.db.CreateGithubCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + newDescription := "new description" + newName := "new-name" + newToken := "new-token" + updateCredParams := params.UpdateGithubCredentialsParams{ + Description: &newDescription, + Name: &newName, + PAT: ¶ms.GithubPAT{ + OAuth2Token: newToken, + }, + } + + updatedCreds, err := s.db.UpdateGithubCredentials(ctx, creds.ID, updateCredParams) + s.Require().NoError(err) + s.Require().NotNil(updatedCreds) + s.Require().Equal(newDescription, updatedCreds.Description) + s.Require().Equal(newName, updatedCreds.Name) +} + +func (s *GithubTestSuite) TestUpdateGithubCredentialsFailIfWrongCredentialTypeIsPassed() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + credParams := params.CreateGithubCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: defaultGithubEndpoint, + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test", + }, + } + + creds, err := s.db.CreateGithubCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + updateCredParams := params.UpdateGithubCredentialsParams{ + App: ¶ms.GithubApp{ + AppID: 1, + InstallationID: 2, + PrivateKeyBytes: []byte("test"), + }, + } + + _, err = s.db.UpdateGithubCredentials(ctx, creds.ID, updateCredParams) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrBadRequest) + s.Require().EqualError(err, "updating github credentials: cannot update app credentials for PAT: invalid request") + + credParamsWithApp := params.CreateGithubCredentialsParams{ + Name: "test-credsApp", + Description: "test credsApp", + Endpoint: defaultGithubEndpoint, + AuthType: params.GithubAuthTypeApp, + App: params.GithubApp{ + AppID: 1, + InstallationID: 2, + PrivateKeyBytes: []byte("test"), + }, + } + + credsApp, err := s.db.CreateGithubCredentials(ctx, credParamsWithApp) + s.Require().NoError(err) + s.Require().NotNil(credsApp) + + updateCredParams = params.UpdateGithubCredentialsParams{ + PAT: ¶ms.GithubPAT{ + OAuth2Token: "test", + }, + } + + _, err = s.db.UpdateGithubCredentials(ctx, credsApp.ID, updateCredParams) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrBadRequest) + s.Require().EqualError(err, "updating github credentials: cannot update PAT credentials for app: invalid request") +} + +func (s *GithubTestSuite) TestUpdateCredentialsFailsForNonExistingCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + + updateCredParams := params.UpdateGithubCredentialsParams{ + Description: nil, + } + + _, err := s.db.UpdateGithubCredentials(ctx, 1, updateCredParams) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GithubTestSuite) TestUpdateCredentialsFailsIfCredentialsAreOwnedByNonAdminUser() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + testUser := garmTesting.CreateGARMTestUser(ctx, "test-user5", s.db, s.T()) + testUserCtx := auth.PopulateContext(context.Background(), testUser) + + credParams := params.CreateGithubCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: defaultGithubEndpoint, + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test-creds5", + }, + } + + creds, err := s.db.CreateGithubCredentials(ctx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + newDescription := "new description2" + updateCredParams := params.UpdateGithubCredentialsParams{ + Description: &newDescription, + } + + _, err = s.db.UpdateGithubCredentials(testUserCtx, creds.ID, updateCredParams) + s.Require().Error(err) + s.Require().ErrorIs(err, runnerErrors.ErrNotFound) +} + +func (s *GithubTestSuite) TestAdminUserCanUpdateAnyGithubCredentials() { + ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T()) + testUser := garmTesting.CreateGARMTestUser(ctx, "test-user5", s.db, s.T()) + testUserCtx := auth.PopulateContext(context.Background(), testUser) + + credParams := params.CreateGithubCredentialsParams{ + Name: testCredsName, + Description: testCredsDescription, + Endpoint: defaultGithubEndpoint, + AuthType: params.GithubAuthTypePAT, + PAT: params.GithubPAT{ + OAuth2Token: "test-creds5", + }, + } + + creds, err := s.db.CreateGithubCredentials(testUserCtx, credParams) + s.Require().NoError(err) + s.Require().NotNil(creds) + + newDescription := "new description2" + updateCredParams := params.UpdateGithubCredentialsParams{ + Description: &newDescription, + } + + newCreds, err := s.db.UpdateGithubCredentials(ctx, creds.ID, updateCredParams) + s.Require().NoError(err) + s.Require().Equal(newDescription, newCreds.Description) } func TestGithubTestSuite(t *testing.T) { diff --git a/database/sql/organizations_test.go b/database/sql/organizations_test.go index ae880da7..23736e2b 100644 --- a/database/sql/organizations_test.go +++ b/database/sql/organizations_test.go @@ -29,6 +29,7 @@ import ( "gorm.io/gorm/logger" runnerErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm/auth" dbCommon "github.com/cloudbase/garm/database/common" garmTesting "github.com/cloudbase/garm/internal/testing" "github.com/cloudbase/garm/params" @@ -50,7 +51,9 @@ type OrgTestSuite struct { StoreSQLMocked *sqlDatabase Fixtures *OrgTestFixtures - adminCtx context.Context + adminCtx context.Context + adminUserID string + testCreds params.GithubCredentials secondaryTestCreds params.GithubCredentials githubEndpoint params.GithubEndpoint @@ -85,6 +88,8 @@ func (s *OrgTestSuite) SetupTest() { adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T()) s.adminCtx = adminCtx + s.adminUserID = auth.UserID(adminCtx) + s.Require().NotEmpty(s.adminUserID) s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) s.testCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), s.githubEndpoint) @@ -217,8 +222,8 @@ func (s *OrgTestSuite) TestCreateOrganizationInvalidDBPassphrase() { func (s *OrgTestSuite) TestCreateOrganizationDBCreateErr() { s.Fixtures.SQLMock.ExpectBegin() s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). - WithArgs(s.Fixtures.Orgs[0].CredentialsName, 1). + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE user_id = ? AND name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.adminUserID, s.Fixtures.Orgs[0].CredentialsName, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.testCreds.ID, s.githubEndpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). @@ -355,8 +360,8 @@ func (s *OrgTestSuite) TestUpdateOrganizationDBEncryptErr() { WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.Fixtures.Orgs[0].ID, s.Fixtures.Orgs[0].Endpoint.Name)) s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). - WithArgs(s.secondaryTestCreds.Name, 1). + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE user_id = ? AND name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.adminUserID, s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). @@ -380,8 +385,8 @@ func (s *OrgTestSuite) TestUpdateOrganizationDBSaveErr() { WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.Fixtures.Orgs[0].ID, s.Fixtures.Orgs[0].Endpoint.Name)) s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). - WithArgs(s.secondaryTestCreds.Name, 1). + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE user_id = ? AND name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.adminUserID, s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). @@ -411,8 +416,8 @@ func (s *OrgTestSuite) TestUpdateOrganizationDBDecryptingErr() { WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.Fixtures.Orgs[0].ID, s.Fixtures.Orgs[0].Endpoint.Name)) s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). - WithArgs(s.secondaryTestCreds.Name, 1). + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE user_id = ? AND name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.adminUserID, s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). diff --git a/database/sql/repositories_test.go b/database/sql/repositories_test.go index 5a67abd1..95c5a4e6 100644 --- a/database/sql/repositories_test.go +++ b/database/sql/repositories_test.go @@ -28,6 +28,7 @@ import ( "gorm.io/gorm" "gorm.io/gorm/logger" + "github.com/cloudbase/garm/auth" dbCommon "github.com/cloudbase/garm/database/common" garmTesting "github.com/cloudbase/garm/internal/testing" "github.com/cloudbase/garm/params" @@ -49,7 +50,9 @@ type RepoTestSuite struct { StoreSQLMocked *sqlDatabase Fixtures *RepoTestFixtures - adminCtx context.Context + adminCtx context.Context + adminUserID string + testCreds params.GithubCredentials secondaryTestCreds params.GithubCredentials githubEndpoint params.GithubEndpoint @@ -94,6 +97,8 @@ func (s *RepoTestSuite) SetupTest() { adminCtx := garmTesting.ImpersonateAdminContext(context.Background(), db, s.T()) s.adminCtx = adminCtx + s.adminUserID = auth.UserID(adminCtx) + s.Require().NotEmpty(s.adminUserID) s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) s.testCreds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), s.githubEndpoint) @@ -233,8 +238,8 @@ func (s *RepoTestSuite) TestCreateRepositoryInvalidDBPassphrase() { func (s *RepoTestSuite) TestCreateRepositoryInvalidDBCreateErr() { s.Fixtures.SQLMock.ExpectBegin() s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). - WithArgs(s.Fixtures.Repos[0].CredentialsName, 1). + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE user_id = ? AND name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.adminUserID, s.Fixtures.Repos[0].CredentialsName, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.testCreds.ID, s.githubEndpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). @@ -393,8 +398,8 @@ func (s *RepoTestSuite) TestUpdateRepositoryDBEncryptErr() { WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.Fixtures.Repos[0].ID, s.githubEndpoint.Name)) s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). - WithArgs(s.secondaryTestCreds.Name, 1). + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE user_id = ? AND name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.adminUserID, s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). @@ -418,8 +423,8 @@ func (s *RepoTestSuite) TestUpdateRepositoryDBSaveErr() { WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.Fixtures.Repos[0].ID, s.githubEndpoint.Name)) s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). - WithArgs(s.secondaryTestCreds.Name, 1). + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE user_id = ? AND name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.adminUserID, s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). @@ -448,8 +453,8 @@ func (s *RepoTestSuite) TestUpdateRepositoryDBDecryptingErr() { WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.Fixtures.Repos[0].ID, s.githubEndpoint.Name)) s.Fixtures.SQLMock. - ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). - WithArgs(s.secondaryTestCreds.Name, 1). + ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_credentials` WHERE user_id = ? AND name = ? AND `github_credentials`.`deleted_at` IS NULL ORDER BY `github_credentials`.`id` LIMIT ?")). + WithArgs(s.adminUserID, s.secondaryTestCreds.Name, 1). WillReturnRows(sqlmock.NewRows([]string{"id", "endpoint_name"}). AddRow(s.secondaryTestCreds.ID, s.secondaryTestCreds.Endpoint.Name)) s.Fixtures.SQLMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `github_endpoints` WHERE `github_endpoints`.`name` = ? AND `github_endpoints`.`deleted_at` IS NULL")). diff --git a/internal/testing/testing.go b/internal/testing/testing.go index adc9730b..8949f7cf 100644 --- a/internal/testing/testing.go +++ b/internal/testing/testing.go @@ -19,6 +19,7 @@ package testing import ( "context" + "fmt" "os" "path/filepath" "sort" @@ -60,6 +61,30 @@ func ImpersonateAdminContext(ctx context.Context, db common.Store, s *testing.T) return ctx } +func CreateGARMTestUser(ctx context.Context, username string, db common.Store, s *testing.T) params.User { + newUserParams := params.NewUserParams{ + Email: fmt.Sprintf("%s@localhost", username), + Username: username, + Password: "superSecretPassword@123", + IsAdmin: false, + Enabled: true, + } + + user, err := db.CreateUser(ctx, newUserParams) + if err != nil { + if errors.Is(err, runnerErrors.ErrDuplicateEntity) { + user, err = db.GetUser(ctx, newUserParams.Username) + if err != nil { + s.Fatalf("failed to get user by email: %v", err) + } + return user + } + s.Fatalf("failed to create user: %v", err) + } + + return user +} + func CreateDefaultGithubEndpoint(ctx context.Context, db common.Store, s *testing.T) params.GithubEndpoint { endpointParams := params.CreateGithubEndpointParams{ Name: "github.com", diff --git a/runner/repositories_test.go b/runner/repositories_test.go index aa8da725..717e795d 100644 --- a/runner/repositories_test.go +++ b/runner/repositories_test.go @@ -24,7 +24,6 @@ import ( "github.com/stretchr/testify/suite" runnerErrors "github.com/cloudbase/garm-provider-common/errors" - "github.com/cloudbase/garm/auth" "github.com/cloudbase/garm/database" dbCommon "github.com/cloudbase/garm/database/common" garmTesting "github.com/cloudbase/garm/internal/testing" @@ -98,7 +97,7 @@ func (s *RepoTestSuite) SetupTest() { var minIdleRunners uint = 20 providerMock := runnerCommonMocks.NewProvider(s.T()) fixtures := &RepoTestFixtures{ - AdminContext: auth.GetAdminContext(context.Background()), + AdminContext: adminCtx, Store: db, StoreRepos: repos, Providers: map[string]common.Provider{ From 8726cb994eeeee709a98ae49f2d793f35646d6a1 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Tue, 7 May 2024 10:07:47 +0000 Subject: [PATCH 24/27] Move the name check before tx No point in making a DB query if we know we don't want to be able to delete/update the default endpoint. Signed-off-by: Gabriel Adrian Samfira --- database/sql/github.go | 14 ++++++++++---- database/sql/github_test.go | 1 - doc/quickstart.md | 30 ++++++++++++++++++++++++------ 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/database/sql/github.go b/database/sql/github.go index 7a775e31..e62cc8ba 100644 --- a/database/sql/github.go +++ b/database/sql/github.go @@ -27,6 +27,10 @@ import ( "github.com/cloudbase/garm/params" ) +const ( + defaultGithubEndpoint string = "github.com" +) + func (s *sqlDatabase) sqlToCommonGithubCredentials(creds GithubCredentials) (params.GithubCredentials, error) { if len(creds.Payload) == 0 { return params.GithubCredentials{}, errors.New("empty credentials payload") @@ -150,6 +154,9 @@ func (s *sqlDatabase) ListGithubEndpoints(_ context.Context) ([]params.GithubEnd } func (s *sqlDatabase) UpdateGithubEndpoint(_ context.Context, name string, param params.UpdateGithubEndpointParams) (params.GithubEndpoint, error) { + if name == defaultGithubEndpoint { + return params.GithubEndpoint{}, errors.Wrap(runnerErrors.ErrBadRequest, "cannot update default github endpoint") + } var endpoint GithubEndpoint err := s.conn.Transaction(func(tx *gorm.DB) error { if err := tx.Where("name = ?", name).First(&endpoint).Error; err != nil { @@ -205,6 +212,9 @@ func (s *sqlDatabase) GetGithubEndpoint(_ context.Context, name string) (params. } func (s *sqlDatabase) DeleteGithubEndpoint(_ context.Context, name string) error { + if name == defaultGithubEndpoint { + return errors.Wrap(runnerErrors.ErrBadRequest, "cannot delete default github endpoint") + } err := s.conn.Transaction(func(tx *gorm.DB) error { var endpoint GithubEndpoint if err := tx.Where("name = ?", name).First(&endpoint).Error; err != nil { @@ -214,10 +224,6 @@ func (s *sqlDatabase) DeleteGithubEndpoint(_ context.Context, name string) error return errors.Wrap(err, "fetching github endpoint") } - if endpoint.Name == "github.com" { - return errors.Wrap(runnerErrors.ErrBadRequest, "cannot delete default github endpoint") - } - var credsCount int64 if err := tx.Model(&GithubCredentials{}).Where("endpoint_name = ?", endpoint.Name).Count(&credsCount).Error; err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { diff --git a/database/sql/github_test.go b/database/sql/github_test.go index 0098418b..b7621ec9 100644 --- a/database/sql/github_test.go +++ b/database/sql/github_test.go @@ -30,7 +30,6 @@ import ( ) const ( - defaultGithubEndpoint string = "github.com" testUploadBaseURL string = "https://uploads.example.com" testBaseURL string = "https://example.com" testAPIBaseURL string = "https://api.example.com" diff --git a/doc/quickstart.md b/doc/quickstart.md index f18cd133..514caf05 100644 --- a/doc/quickstart.md +++ b/doc/quickstart.md @@ -48,6 +48,19 @@ Open `/etc/garm/config.toml` in your favorite editor and paste the following: [default] callback_url = "https://garm.example.com/api/v1/callbacks" metadata_url = "https://garm.example.com/api/v1/metadata" +webhook_url = "https://garm.example.com/webhooks" +enable_webhook_management = true + +[logging] +# If using nginx, you'll need to configure connection upgrade headers +# for the /api/v1/ws location. See the sample config in the testdata +# folder. +enable_log_streamer = true +# Set this to "json" if you want to consume these logs in something like +# Loki or ELK. +log_format = "text" +log_level = "info" +log_source = false [metrics] enable = true @@ -71,11 +84,12 @@ time_to_live = "8760h" db_file = "/etc/garm/garm.db" ``` -This is a minimal config, with no providers or credentials defined. In this example we have the [default](./config_default.md), [metrics](./config_metrics.md), [jwt_auth](./config_jwt_auth.md), [apiserver](./config_api_server.md) and [database](./database.md) sections. Each are documented separately. Feel free to read through the available docs if, for example you need to enable TLS without using an nginx reverse proxy or if you want to enable the debug server, the log streamer or a log file. +This is a minimal config, with no providers or credentials defined. In this example we have the [default](./config_default.md), [logging](./config_logging.md), [metrics](./config_metrics.md), [jwt_auth](./config_jwt_auth.md), [apiserver](./config_api_server.md) and [database](./database.md) sections. Each are documented separately. Feel free to read through the available docs if, for example you need to enable TLS without using an nginx reverse proxy or if you want to enable the debug server, the log streamer or a log file. In this sample config we: -* define the callback and the metadata URLs +* define the callback, webhooks and the metadata URLs +* set up logging prefrences * enable metrics with authentication * set a JWT secret which is used to sign JWT tokens * set a time to live for the JWT tokens @@ -88,6 +102,8 @@ We need to tell garm by which addresses it can be reached. There are many ways b The information in these two options is used by the instances we spin up to phone home their status and to fetch the needed metadata to finish setting themselves up. For now, the metadata URL is only used to fetch the runner registration token. +The webhook URL is used by GARM itself to know how to set up the webhooks in GitHub. Each controller will have a unique ID and GARM will use the value in `webhook_url` as a base. It will append the controller ID to it and set up the webhook in GitHub. This way we won't overlap with other controllers that may use the same base URL. + We won't go too much into detail about each of the options here. Have a look at the different config sections and their respective docs for more information. At this point, we have a valid config file, but we still need to add `provider` and `credentials` sections. @@ -131,7 +147,7 @@ Go ahead and create a new config somwhere where GARM can access it and paste tha The credentials section is where we define out GitHub credentials. GARM is capable of using either GitHub proper or [GitHub Enterprise Server](https://docs.github.com/en/enterprise-server@3.6/get-started/onboarding/getting-started-with-github-enterprise-server). The credentials section allows you to override the default GitHub API endpoint and point it to your own deployment of GHES. The credentials section is [documented in a separate doc](./github_credentials.md), but we will include a small snippet here for clarity. - +wget -q -O - https://github.com/cloudbase/garm/releases/download/v0.1.4/garm-linux-amd64.tgz | tar xzf - -C /usr/local/bin/ ```toml # This is a list of credentials that you can define as part of the repository # or organization definitions. They are not saved inside the database, as there @@ -194,7 +210,7 @@ useradd --shell /usr/bin/false \ --no-create-home garm ``` -Adding the `garm` user to the LXD group will allow it to connect to the LXD unix socket. We'll need that considering the config we crafted above. +Adding the `garm` user to the LXD group will allow it to connect to the LXD unix socket. We'll need that considering the config we crafted above. The recommendation is to use TCP connections to connect to a remote LXD installation. The local setup of an LXD provider is just for demonstration purposes/testing. Next, download the latest release from the [releases page](https://github.com/cloudbase/garm/releases). @@ -217,7 +233,9 @@ sudo mkdir -p /opt/garm/providers.d Download the LXD provider binary: ```bash -wget -q -O - https://github.com/cloudbase/garm-provider-lxd/releases/download/v0.1.0/garm-linux-amd64.tgz | sudo tar xzf - -C /opt/garm/providers.d/ +git clone https://github.com/cloudbase/garm-provider-lxd +cd garm-provider-lxd +go build -o /opt/garm/providers.d/garm-provider-lxd ``` Change the permissions on the config dir: @@ -230,7 +248,7 @@ Copy the sample `systemd` service file: ```bash wget -O /etc/systemd/system/garm.service \ - https://raw.githubusercontent.com/cloudbase/garm/v0.1.3/contrib/garm.service + https://raw.githubusercontent.com/cloudbase/garm/v0.1.4/contrib/garm.service ``` Reload the `systemd` daemon and start the service: From 75eb45c97a0ab6c3aa1bdcca1d001dd83b348a6e Mon Sep 17 00:00:00 2001 From: Gabriel Date: Tue, 7 May 2024 13:13:16 +0300 Subject: [PATCH 25/27] Update database/sql/sql.go Co-authored-by: Mario Constanti --- database/sql/sql.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/database/sql/sql.go b/database/sql/sql.go index d61840d3..5f970558 100644 --- a/database/sql/sql.go +++ b/database/sql/sql.go @@ -337,15 +337,15 @@ func (s *sqlDatabase) migrateCredentialsToDB() (err error) { return errors.Wrap(err, "creating github credentials") } - if err := s.conn.Exec("update repositories set credentials_id = ?,endpoint_name = ? where credentials_name = ?", creds.ID, creds.Endpoint, creds.Name).Error; err != nil { + if err := s.conn.Exec("update repositories set credentials_id = ?,endpoint_name = ? where credentials_name = ?", creds.ID, creds.Endpoint.Name, creds.Name).Error; err != nil { return errors.Wrap(err, "updating repositories") } - if err := s.conn.Exec("update organizations set credentials_id = ?,endpoint_name = ? where credentials_name = ?", creds.ID, creds.Endpoint, creds.Name).Error; err != nil { + if err := s.conn.Exec("update organizations set credentials_id = ?,endpoint_name = ? where credentials_name = ?", creds.ID, creds.Endpoint.Name, creds.Name).Error; err != nil { return errors.Wrap(err, "updating organizations") } - if err := s.conn.Exec("update enterprises set credentials_id = ?,endpoint_name = ? where credentials_name = ?", creds.ID, creds.Endpoint, creds.Name).Error; err != nil { + if err := s.conn.Exec("update enterprises set credentials_id = ?,endpoint_name = ? where credentials_name = ?", creds.ID, creds.Endpoint.Name, creds.Name).Error; err != nil { return errors.Wrap(err, "updating enterprises") } } From 27e74ef277fc28d186fa8957681fd62d6a030fa5 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Tue, 7 May 2024 11:41:45 +0000 Subject: [PATCH 26/27] Add DB migration test Signed-off-by: Gabriel Adrian Samfira --- database/sql/github_test.go | 140 ++++++++++++++++++++++++++++++++++++ testdata/db/v0.1.4/garm.db | Bin 0 -> 245760 bytes 2 files changed, 140 insertions(+) create mode 100644 testdata/db/v0.1.4/garm.db diff --git a/database/sql/github_test.go b/database/sql/github_test.go index b7621ec9..101ac411 100644 --- a/database/sql/github_test.go +++ b/database/sql/github_test.go @@ -15,7 +15,9 @@ package sql import ( + "bytes" "context" + "encoding/json" "fmt" "os" "testing" @@ -24,6 +26,7 @@ import ( runnerErrors "github.com/cloudbase/garm-provider-common/errors" "github.com/cloudbase/garm/auth" + "github.com/cloudbase/garm/config" "github.com/cloudbase/garm/database/common" garmTesting "github.com/cloudbase/garm/internal/testing" "github.com/cloudbase/garm/params" @@ -738,3 +741,140 @@ func TestGithubTestSuite(t *testing.T) { t.Parallel() suite.Run(t, new(GithubTestSuite)) } + +func TestCredentialsAndEndpointMigration(t *testing.T) { + cfg := garmTesting.GetTestSqliteDBConfig(t) + + // Copy the sample DB + data, err := os.ReadFile("../../testdata/db/v0.1.4/garm.db") + if err != nil { + t.Fatalf("failed to read test data: %s", err) + } + + if cfg.SQLite.DBFile == "" { + t.Fatalf("DB file not set") + } + if err := os.WriteFile(cfg.SQLite.DBFile, data, 0o600); err != nil { + t.Fatalf("failed to write test data: %s", err) + } + + // define some credentials + credentials := []config.Github{ + { + Name: "test-creds", + Description: "test creds", + AuthType: config.GithubAuthTypePAT, + PAT: config.GithubPAT{ + OAuth2Token: "test", + }, + }, + { + Name: "ghes-test", + Description: "ghes creds", + APIBaseURL: testAPIBaseURL, + UploadBaseURL: testUploadBaseURL, + BaseURL: testBaseURL, + AuthType: config.GithubAuthTypeApp, + App: config.GithubApp{ + AppID: 1, + InstallationID: 99, + PrivateKeyPath: "../../testdata/certs/srv-key.pem", + }, + }, + } + // Set the config credentials in the cfg. This is what happens in the main function. + // of GARM as well. + cfg.MigrateCredentials = credentials + + db, err := NewSQLDatabase(context.Background(), cfg) + if err != nil { + t.Fatalf("failed to create db connection: %s", err) + } + + // We expect that 2 endpoints will exist in the migrated DB and 2 credentials. + ctx := garmTesting.ImpersonateAdminContext(context.Background(), db, t) + + endpoints, err := db.ListGithubEndpoints(ctx) + if err != nil { + t.Fatalf("failed to list endpoints: %s", err) + } + if len(endpoints) != 2 { + t.Fatalf("expected 2 endpoints, got %d", len(endpoints)) + } + if endpoints[0].Name != defaultGithubEndpoint { + t.Fatalf("expected default endpoint to exist, got %s", endpoints[0].Name) + } + if endpoints[1].Name != "example.com" { + t.Fatalf("expected example.com endpoint to exist, got %s", endpoints[1].Name) + } + if endpoints[1].UploadBaseURL != testUploadBaseURL { + t.Fatalf("expected upload base URL to be %s, got %s", testUploadBaseURL, endpoints[1].UploadBaseURL) + } + if endpoints[1].BaseURL != testBaseURL { + t.Fatalf("expected base URL to be %s, got %s", testBaseURL, endpoints[1].BaseURL) + } + if endpoints[1].APIBaseURL != testAPIBaseURL { + t.Fatalf("expected API base URL to be %s, got %s", testAPIBaseURL, endpoints[1].APIBaseURL) + } + + creds, err := db.ListGithubCredentials(ctx) + if err != nil { + t.Fatalf("failed to list credentials: %s", err) + } + if len(creds) != 2 { + t.Fatalf("expected 2 credentials, got %d", len(creds)) + } + if creds[0].Name != "test-creds" { + t.Fatalf("expected test-creds to exist, got %s", creds[0].Name) + } + if creds[1].Name != "ghes-test" { + t.Fatalf("expected ghes-test to exist, got %s", creds[1].Name) + } + if creds[0].Endpoint.Name != defaultGithubEndpoint { + t.Fatalf("expected test-creds to be associated with default endpoint, got %s", creds[0].Endpoint.Name) + } + if creds[1].Endpoint.Name != "example.com" { + t.Fatalf("expected ghes-test to be associated with example.com endpoint, got %s", creds[1].Endpoint.Name) + } + + if creds[0].AuthType != params.GithubAuthTypePAT { + t.Fatalf("expected test-creds to have PAT auth type, got %s", creds[0].AuthType) + } + if creds[1].AuthType != params.GithubAuthTypeApp { + t.Fatalf("expected ghes-test to have App auth type, got %s", creds[1].AuthType) + } + if len(creds[0].CredentialsPayload) == 0 { + t.Fatalf("expected test-creds to have credentials payload, got empty") + } + + var pat params.GithubPAT + if err := json.Unmarshal(creds[0].CredentialsPayload, &pat); err != nil { + t.Fatalf("failed to unmarshal test-creds credentials payload: %s", err) + } + if pat.OAuth2Token != "test" { + t.Fatalf("expected test-creds to have PAT token test, got %s", pat.OAuth2Token) + } + + var app params.GithubApp + if err := json.Unmarshal(creds[1].CredentialsPayload, &app); err != nil { + t.Fatalf("failed to unmarshal ghes-test credentials payload: %s", err) + } + if app.AppID != 1 { + t.Fatalf("expected ghes-test to have app ID 1, got %d", app.AppID) + } + if app.InstallationID != 99 { + t.Fatalf("expected ghes-test to have installation ID 99, got %d", app.InstallationID) + } + if app.PrivateKeyBytes == nil { + t.Fatalf("expected ghes-test to have private key bytes, got nil") + } + + certBundle, err := credentials[1].App.PrivateKeyBytes() + if err != nil { + t.Fatalf("failed to read CA cert bundle: %s", err) + } + + if !bytes.Equal(app.PrivateKeyBytes, certBundle) { + t.Fatalf("expected ghes-test private key to be equal to the CA cert bundle") + } +} diff --git a/testdata/db/v0.1.4/garm.db b/testdata/db/v0.1.4/garm.db new file mode 100644 index 0000000000000000000000000000000000000000..7308e31f413bb5fceea4fd1e0848195a44df2d68 GIT binary patch literal 245760 zcmeI*ZEPF&eFt!!D3O*eiB4?g;yRfxc2t=$&%Pt?_#h`pWtk2X%aW~!#_mJzfjmkV zQ>1ctlw>FEK&RP52kh-$>{U^qSPKjT)>nJey&BK~1BL;6krzX=HwC((#nu&Qhb}Mn zzXu-gPNc=8aw^+j!KQfkf4tBC_y6eU*_1Ez+>OOUUza}2nbpCXLE}x-ddZViCn(x-h zFi1+vYC7AJ-Y_ktQPE6^_n*G-;A_t(=`&}j50jRn*0rr>vo2Z6y6HbTHMd-zT`e=K zvsV_%jO_21ne*<3UX__vORr{RQcm)>ZZ6N?m|ecZTrb}V3?dDax^WL8RZIZ4>oAiz*+@ekMDH=@krye#U-DBqcr(aO%BrVcZd&C(vy}Ijw zm{L`%YnE1(6wA(QKJSgo{NmN}ZH5r+%_rLe&LG)+?v|I+8_y-_Y0}1-P#cnUZ_9}Q zlkQ!)lObH|meJhStC}G-lub>}$oi(TZhx=UmF=b>r*BUt=u0$pFVv7J8KfN&E6$NT z*Sj%4Q@BB|q?}$mnV?ykYDH>p8g>vm6;Jl7*v%8J*V~uAdm>3+CN$4P>eU*SW^5U{ zsdcJ9*{}Lg)^ID~r1Xai&m?J@roO%7&pv17na_H42WFjV&p3a2$rVFW+;EVYDtTt< zn_5P0ZQ0N5APMdb41FMPc<2~Q+qE*X(P}ivXl@x=P2ZKhotw(8dpa$ zm#Sd7K2>)QwolF73}mFq8XH`EyJ?*jF`a%kLBB@#d;M|xE;%cmdE)-@Bt3hMYQN}D zvp{`;wJxytnZ5HZkQylCPPsr@Wc6#Ml5~!={Zg=PX91HM&5B}bfuhHIHX6*_t2Jja zWagF@7VK9ax6*X`_IQ#$caD1CPWC_}fu|GUi62Ng=;==ukSeKOSL&v_czd(Y?Ylem zb~JUP*}N;68p%y&uUc=anZ45?Elwop`E&gSK5!5cAJ|BJb1X^EO$7%c7_@;A4elP_ zJ0ij4Kq+@*f{BqpXs3=PX_j>0jvool9eeq6M=V%!%xiURasJj?nF+k=221p8(a9i3 zq9G&qj**SgBt11neQ?5gK?wFB`21)%N`gtBSD1K5<;KZpiJ8LD1buy~U(5s#N#cWB z$J#?jl5}~RYEN}XEwui2U+zNd>*(IV38n9eoKR{cbnYd|8WQb8a9IcyKGL)4V0Jfh zLWM|+y<_C<5pwo6O+9$eiIGs5&_*I0C84y>D@;C1WRwyK`o?s>$Os*V#D_MMZx54U znWWm39~$-hSbS)^!7K}UqqS=eInAD^h&isnSMzLDscQ&~YQ39OQ>X{=bP@Fi6f3)Nz^Pot!R zLMi*uMuS;QtzKg{nkISk=_rZmk(3Zi{ZUFteLeLzsgHhdL5_wY009U<00Izz00bZa z0SG_<0*6ZAe0+@>OT$@H97+U2tWV=5P$##AOHafKmY;|fIvXtSo}2QeP(0iX#5oAevD#xG)_~_ zCmZ6)_z8-9*kEWPer|Nn`~N?Sr9MghCiOq5U!;B(s1#2j009U<00Izz00bZa0SG_< z0uXqt1cu`=YQ%mRwjbj5!%#dP8?}G_pZwB4d_e#L5P$##AOHafKmY;|fB*y_@B|Ck zpZ~}8{}VjGI5Y@A00Izz00bZa0SG_<0uX=z5y167!T~00bZa0SG_<0uX=z z1Rwx`-=DyPuTe+WR#sxCUp)N*$BFq$wpOaL`4U%P1x4gorJChfKA+E5m6}%2vZXAS z6n#*RgnNlfJDr8E0hUbd*v#glS@I^5r6f?Y-&ty3I=(hK z$lVh6`v2##)XyKY14MTr009U<00Izz00bZa0SG_<0ubmaaANqy)1#43G`QFQpTtt1 z^elp%5P$##AOHafKmY;|fB*y_009U*<^u6}Y}EewfBgNw$9y2seF#7R0uX=z1Rwwb z2tWV=5P-lw0{H#^`v4&s0uX=z1Rwwb2tWV=5P$##An*hT;QIdw7(^Tb1Rwwb2tWV= z5P$##AOHafKwuvMT>tL_gk%Un00Izz00bZa0SG_<0uX?}6Chy!{6CfAW8@#cAOHaf zKmY;|fB*y_009U<00Izj1=@@41?uG5%2MpiBz1(EI!)7~vGKK7OcZ#YQ?hwhRB8oQ zP*jao^Hq)Id9BEmvNeUPXjv{Ru$;(p1%~HmIDRIVzbJ@>T!|~@#S0ua!*RW{6a^uh zbu-j=t5UsLQR?f8u{m;F$@hTIT<00bZa0SG_<0uX=z1Rwx`$4#Jn{g2Q8KW<}({z3o(5P$## zAOHafKmY;|fB*z+0sHg+ga7>hPhzQ`zzqTrfB*y_009U<00Izz00bZafyYzerI9mJ zr$_zY8Q9~u1>*DnkLSpu*ARdJ1Rwwb2tWV=5P$##AOL|u1?oU+kS&w|J(2X$BsYck%+^G00bZa0SG_<0uX=z1Rwwb2t45e_WS?1 z{(r&;8HWb}2tWV=5P$##AOHafKmYurorbaF#e>?o6;q~G1_;=&4Q~w?xqwZ7ppL)LE0f0Z9pvz~d znBJ&ryXL!f-O?nbWi_2`NpF~z(x_;r#QRTQc<{C7lk}N0)Q3q+QR~`PvssrcW!?0j zoSIuM&#so4)!8cxWkx3ZKSL3>PMJAxm#<5DRc2Z(y_%6pQ{Anb%kwv8m+vsw%Xb1f z{Y>f1+|uI8>hkRT;wmH8?n-_ioo6z0ZE3kY|HfjZe41GNjnH=5R?8@Bi)v~>3?dDbcc)dOS+az6&Ht8KrxJ6s@Q#6=X=_D=ERC~l3 zHNCp)9GxjuwYp|$RY|ez<2Il7#$|r-YWX%pj%RN^**+Xc-0ncPyqw;6E=f<5HqM0F zkgR)KP6U{A@5-GF;aazh=C)qd45^`PYH~)_H^qN#hKhD^yI z?T~?Yj^w%Cjrp0v4SFTz^xDY;&C*mWQghR=Bg(0GvR}n+o^ZY1zVzJ_N%}IOc_vb? z*03~V%g{}&Q~k+))rYc%TL~wnKU{bwNz*j-?HzyiIWy0E)~mZa>qyj+Nzt7-6+=_p zaFCfQd1mRGT1IYd+0X4D3GNLHeIRdm=om}ewKB5NYBb1bZW&rl-<7?co64@_CXi0( z1Y%t?GV-S0AQW}2N0L{I+H6{;Whh&c-MeOsbjmO728!2!7cJga)ARz(?Go8kjLJr* zGg`x5sK^S^Xl-hSUdhO$Ty7qiaJazCx=}sGPS*MC-P@YwA|h(8Rd)}zPtDy7WTeT08C*%bX`SUQoqje!zee|a{c-v(@y@ps_m3y( z*>hC;MSq$F>IIj? zVCG(}Ig24Px3sWezm&L@rrWp2lk~ZB)B|_22O0@Hod{3-K*~W+f3kp7N%gu?H{HeC zn|*HI-Kn>usTo@R$gOK>ZM(UemNqTN7I1s_0 z4UA}T_xRos2_^?hxg!%yj08eEbu3AX!9sVVA%6V3}lum{2CN5fGPO!~aS#5*cCPCiS_6pkk7>r?$= zCU{5^AKW_D9y*ew%hObQsyk|-^|$+S7g}FO_XbWVeNW_sQX`>rFG<#rXdi;hLa6YO zo=pd{yO9$rL|W_}BX5t8v$tvL!Fx`Ogvx|A65%KbrF~vu@>wFIlt|Dwru#)k=rANc zw3&Q+m<-D#)u#N=uus1OBWrJt>>Z6rE_XD%Gxu&d$?4^If}WnFcEZ)$2We*>_A1`H zNI3Po7WO&0SF^Jt-F|^e(vy?agVWA<+G+Oxq44NAMVNy<19UsIZJ%8!#`(OE?zOg0 z4<+c!ll?}+?v}*ck8yJ0dc?kX9Y4~DJrjRFmik`m)rpVB?~T26ba?b%MmLW9=aKEi zFOOVKd}H`86Bmcm@%JbGn>rf%c;c@n=EgrJh3v17oyWuckj^~pB&TI+hACy8Z+ z_Rq>YSueIH(nu(z$mcky&cBOv%0EGX2{tXS2AnLAp2GLk{_Q|vSJ z_A66K`VQ&ujeYyujV8Z_@DZIKyu01H_UmmRXL{>pa_foo^@P(`ZJV4Lu97{ zgE!7N_fWs}b!Vycv-wZXh9k${f6%ewRon>!`*vRExexi7LL$)PM;Z(~lX%DtY`+&0 z?>{{^_HFm9!}x>fz;_NgIPm_ym-Y_4pSEY<{glYSzyD&Aew_r{v>$AKPI3p$yD{2* zIWQTo(Y@z~J-L$QU+0+tn(dyO`o&0l?g{E=cw6CH zH^1Oai9cyNQ^K#)e{woJCH(ydJtcx-J#KK2MYCbqD_Qr(E^RMV`=M(7slJLG;SIk~Cn>n+2%OBt-~VK+;as&3!ZRO(V)Q8oJ} zw0ncqzKiF$Ik!#se|d3l{so3PypD9L3*TaN3Xw3h&ur5CsfVo%P6h78i2w8p3pMZ^t>4KluJ)XJ;Cp|3A#P zFVHs#KmY;|fB*y_009U<00Izzz`+RM`u|`YQ40hh009U<00Izz00bZa0SG|gFbm-N z|1ggZ`UU|AKmY;|fB*y_009U<00Iy=7yX4UnZ!QuEoWl4C2PP-BI>T49S~ zt;8y7F~Z2I>hc5^~00Izz00bZa0SG_<0uX=z1RgVi>G;~{Aa@$v z>;F$;sZSoW14MTr009U<00Izz00bZa0SG_<0uUHTARdp6x}X0q#Zsk#iXamNAOHaf zKmY;|fB*y_009U<;OP=*U!}&@ZrzH};}_Z^qob-=%n7_$W5rsn!U`4D{+K;iV2ioD zkmbZ|UddJ>AK5R>h^32MKF{&lyx8+Y{XKFh)lI$OJbbC%tSI%3rfI#LRbJ+~m-9QT z%KbOD?>4@oUb|i0X^7F}Udg^&+g_QwbE&cY);&%u)CIn(xbOe} zB9{8a(=~iJS_nV@0uX=z1Rwwb2tWV=5P-lFEAVXm!f52)0e=4fi5+bm7z7{y0SG_< z0uX=z1Rwwb2s{M>_Rs$h@|gs@|Nj&O0}c@a5P$##AOHafKmY;|fB*!ZE`h%9|NkbI z`pwftfun^01Rwwb2tWV=5P$##AOHafJOu(r;D0S6BO2tWV=5P$##AOHafKmY;|c;W@@zyEh4{`axe&51vo zcys*k#=bxH&9RlSW5>RC>`HPY`CQ`r34Y`sM!q!sx5G0e8@?a_fx{zkub8A?pQhTU z^hQCIH1-c+@^W@%MPu|kri{kqaE*SjBq^z=JIk`_rHM*8$2 z5&*^h+SaSR+a&wb+qs7iyHc_AW+Qa0h5g!d%k*y3OH1F)CTW%&W6D29y7ZMmAav68(PGjNCEaR*cGqVw}&4LOLT?nvF`mWs(t-8B5!>GBPo> zT4raRRs>J4oTSg2e`Jj0h_Y{k~!^4!v9JtnZ-%3l$r@wN8XNEkz+aiF+A4DLoM9 zW>@BBua@nX#I>7O?Uzq)Pg=c17GFBJ__j>VFs1I8JKNd4i&8Y7yD&L>$)R@3%jwF+ zB+ZjHj`@q9Th@NGgWuWWy%yFM=WnfrUf#o*!_9Q6kki-L1bv34G`|gPQ_;!dKn{l6 z+j%E3T$Q^koz5ia*Jvu{)Z;CPO3P|G+mhRttn=+s`ojKMc_-_|2jj0KX_}@!xaTaO z&Q9ln_4?*6z_PPXX559+ozK7XOm!*^E;6-Nz3#0#;q}y03GQUv@yN(qifNLio-E3p zna&i3mL$=KNwTAb{Orj%J9&=Cx?1 z!s8xIKIjk#)=x&pd9my@EbJGP^j9xa?OXmd>Z~e~`_d}8FA*jgBk%l0uli5JS<-t) zsZY*eW$tM8$!M>k-FD@)ef^L3{}0av9eM%*2tWV=5P$##AOHafKmY;|coYKm&;R54 z|53=IP6$8%0uX=z1Rwwb2tWV=5P-np5y18T;Ta3`1OgC%00bZa0SG_<0uX=z1R(IJ z1aSTTsAN$u1Rwwb2tWV=5P$##AOHafK;ZBQ;QIgYj0JiE0SG_<0uX=z1Rwwb2tWV= z5O`Dqxc+}svZxmV5P$##AOHafKmY;|fB*y_aCii8{eO7I0zH8M1Rwwb2tWV=5P$## nAOHafJSqWP|34~O)C&O!KmY;|fB*y_009U<00Iy=JOcj@?*&dZ literal 0 HcmV?d00001 From b3e2c584bffe6234da4b387376bc43eba7e2d230 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Tue, 7 May 2024 21:09:41 +0000 Subject: [PATCH 27/27] Update docs Signed-off-by: Gabriel Adrian Samfira --- README.md | 1 - doc/github_credentials.md | 117 ++++++++++++---------- doc/quickstart.md | 199 +++++++++++++++++++++++--------------- 3 files changed, 186 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index a09189ac..1da1d91b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,6 @@ The ```GARM``` configuration is a simple ```toml```. The sample config file in [ * [The default section](/doc/config_default.md) * [Logging](/doc/config_logging.md) * [Database](/doc/database.md) -* [Github credentials](/doc/github_credentials.md) * [Providers](/doc/providers.md) * [Metrics](/doc/config_metrics.md) * [JWT authentication](/doc/config_jwt_auth.md) diff --git a/doc/github_credentials.md b/doc/github_credentials.md index c2912240..3d525fa5 100644 --- a/doc/github_credentials.md +++ b/doc/github_credentials.md @@ -1,8 +1,32 @@ -# Configuring github credentials +# Configuring github endpoints and credentials -The ```github``` config section holds credentials and API endpoint information for accessing the GitHub APIs. Credentials are tied to the instance of GitHub you're using. Whether you're using [github.com](https://github.com) or your own deployment of GitHub Enterprise server, this section is how ```garm``` knows where it should create the runners. +Starting with version `v0.1.5`, GARM saves github endpoints and github credentials in the database. -Tying the API endpoint info to the credentials allows us to use the same ```garm``` installation with both [github.com](https://github.com) and private deployments. All you have to do is to add the needed endpoint info (see bellow). + + +- [Configuring github endpoints and credentials](#configuring-github-endpoints-and-credentials) + - [Listing GitHub endpoints](#listing-github-endpoints) + - [Adding GitHub credentials](#adding-github-credentials) + - [Listing GitHub credentials](#listing-github-credentials) + - [Deleting GitHub credentials](#deleting-github-credentials) + + +## Listing GitHub endpoints + +To list the available GitHub endpoints, you can use the following command: + +```bash +ubuntu@garm:~/garm$ garm-cli github endpoint list ++------------+--------------------------+-------------------------------+ +| NAME | BASE URL | DESCRIPTION | ++------------+--------------------------+-------------------------------+ +| github.com | https://github.com | The github.com endpoint | ++------------+--------------------------+-------------------------------+ +| example | https://ghes.example.com | Just an example ghes endpoint | ++------------+--------------------------+-------------------------------+ +``` + +## Adding GitHub credentials GARM has the option to use both [Personal Access Tokens (PAT)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) or a [GitHub App](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app). @@ -28,55 +52,44 @@ If you plan to use github apps, you'll need to select the following permissions: **Note** :warning:: Github Apps are not available at the enterprise level. -The resulting credentials (app or PAT) must be configured in the ```[[github]]``` section of the config. Sample as follows: - -```toml -# This is a list of credentials that you can define as part of the repository -# or organization definitions. They are not saved inside the database, as there -# is no Vault integration (yet). This will change in the future. -# Credentials defined here can be listed using the API. Obviously, only the name -# and descriptions are returned. -[[github]] - name = "gabriel" - description = "github token or user gabriel" - # This is the type of authentication to use. It can be "pat" or "app" - auth_type = "pat" - [github.pat] - # This is a personal token with access to the repositories and organizations - # you plan on adding to garm. The "workflow" option needs to be selected in order - # to work with repositories, and the admin:org needs to be set if you plan on - # adding an organization. - oauth2_token = "super secret token" - [github.app] - # This is the app_id of the GitHub App that you want to use to authenticate - # with the GitHub API. - # This needs to be changed - app_id = 1 - # This is the private key path of the GitHub App that you want to use to authenticate - # with the GitHub API. - # This needs to be changed - private_key_path = "/etc/garm/yourAppName.2024-03-01.private-key.pem" - # This is the installation_id of the GitHub App that you want to use to authenticate - # with the GitHub API. - # This needs to be changed - installation_id = 99 - # base_url (optional) is the URL at which your GitHub Enterprise Server can be accessed. - # If these credentials are for github.com, leave this setting blank - base_url = "https://ghe.example.com" - # api_base_url (optional) is the base URL where the GitHub Enterprise Server API can be accessed. - # Leave this blank if these credentials are for github.com. - api_base_url = "https://ghe.example.com" - # upload_base_url (optional) is the base URL where the GitHub Enterprise Server upload API can be accessed. - # Leave this blank if these credentials are for github.com, or if you don't have a separate URL - # for the upload API. - upload_base_url = "https://api.ghe.example.com" - # ca_cert_bundle (optional) is the CA certificate bundle in PEM format that will be used by the github - # client to talk to the API. This bundle will also be sent to all runners as bootstrap params. - # Use this option if you're using a self signed certificate. - # Leave this blank if you're using github.com or if your certificate is signed by a valid CA. - ca_cert_bundle = "/etc/garm/ghe.crt" +To add a new GitHub credential, you can use the following command: + +```bash +garm-cli github credentials add \ + --name gabriel \ + --description "GitHub PAT for user gabriel" \ + --auth-type pat \ + --pat-oauth-token gh_theRestOfThePAT \ + --endpoint github.com +``` + +To add a new GitHub App credential, you can use the following command: + +```bash +garm-cli github credentials add \ + --name gabriel_app \ + --description "Github App with access to repos" \ + --endpoint github.com \ + --auth-type app \ + --app-id 1 \ + --app-installation-id 99 \ + --private-key-path $HOME/yourAppName.2024-03-01.private-key.pem +``` + +All sensitive data is encrypted at rest. The API will not return any sensitive info. + +## Listing GitHub credentials + +To list the available GitHub credentials, you can use the following command: + +```bash +garm-cli github credentials list ``` -The double parenthesis means that this is an array. You can specify the ```[[github]]``` section multiple times, with different tokens from different users, or with different access levels. You will then be able to list the available credentials using the API, and reference these credentials when adding repositories or organizations. +## Deleting GitHub credentials + +To delete a GitHub credential, you can use the following command: -The API will only ever return the name and description to the API consumer. +```bash +garm-cli github credentials delete +``` \ No newline at end of file diff --git a/doc/quickstart.md b/doc/quickstart.md index 514caf05..c498adda 100644 --- a/doc/quickstart.md +++ b/doc/quickstart.md @@ -25,6 +25,8 @@ For a `classic` PAT, GARM needs the following permissions to function properly ( * ```repo``` - for access to a private repository * ```admin:org``` - if you plan on using this with an organization to which you have access * ```manage_runners:enterprise``` - if you plan to use garm at the enterprise level +* ```admin:repo_hook``` - if you want to allow GARM to install webhooks on repositories +* ```admin:org_hook``` - if you want to allow GARM to install webhooks on organizations This doc will be updated at a future date with the exact permissions needed in case you want to use a fine grained PAT. @@ -48,6 +50,7 @@ Open `/etc/garm/config.toml` in your favorite editor and paste the following: [default] callback_url = "https://garm.example.com/api/v1/callbacks" metadata_url = "https://garm.example.com/api/v1/metadata" +# This is important for webhook management. webhook_url = "https://garm.example.com/webhooks" enable_webhook_management = true @@ -84,7 +87,7 @@ time_to_live = "8760h" db_file = "/etc/garm/garm.db" ``` -This is a minimal config, with no providers or credentials defined. In this example we have the [default](./config_default.md), [logging](./config_logging.md), [metrics](./config_metrics.md), [jwt_auth](./config_jwt_auth.md), [apiserver](./config_api_server.md) and [database](./database.md) sections. Each are documented separately. Feel free to read through the available docs if, for example you need to enable TLS without using an nginx reverse proxy or if you want to enable the debug server, the log streamer or a log file. +This is a minimal config, with no providers defined. In this example we have the [default](./config_default.md), [logging](./config_logging.md), [metrics](./config_metrics.md), [jwt_auth](./config_jwt_auth.md), [apiserver](./config_api_server.md) and [database](./database.md) sections. Each are documented separately. Feel free to read through the available docs if, for example you need to enable TLS without using an nginx reverse proxy or if you want to enable the debug server, the log streamer or a log file. In this sample config we: @@ -102,11 +105,10 @@ We need to tell garm by which addresses it can be reached. There are many ways b The information in these two options is used by the instances we spin up to phone home their status and to fetch the needed metadata to finish setting themselves up. For now, the metadata URL is only used to fetch the runner registration token. -The webhook URL is used by GARM itself to know how to set up the webhooks in GitHub. Each controller will have a unique ID and GARM will use the value in `webhook_url` as a base. It will append the controller ID to it and set up the webhook in GitHub. This way we won't overlap with other controllers that may use the same base URL. - +The webhook URL is used by GARM itself to know how to set up the webhooks in GitHub. Each controller will have a unique ID and GARM will use the value in `webhook_url` as a base. It will appen We won't go too much into detail about each of the options here. Have a look at the different config sections and their respective docs for more information. -At this point, we have a valid config file, but we still need to add `provider` and `credentials` sections. +At this point, we have a valid config file, but we still need to add the `provider` section. ## The provider section @@ -126,9 +128,7 @@ All currently available providers are `external`. The easiest provider to set up is probably the LXD or Incus provider. Incus is a fork of LXD so the functionality is identical (for now). For the purpose of this document, we'll continue with LXD. You don't need an account on an external cloud. You can just use your machine. -You will need to have LXD installed and configured. There is an excellent [getting started guide](https://documentation.ubuntu.com/lxd/en/latest/getting_started/) for LXD. Follow the instructions there to install and configure LXD, then come back here. - -Once you have LXD installed and configured, you can add the provider section to your config file. If you're connecting to the `local` LXD installation, the [config snippet for the LXD provider](https://github.com/cloudbase/garm-provider-lxd/blob/main/testdata/garm-provider-lxd.toml) will work out of the box. We'll be connecting using the unix socket so no further configuration will be needed. +You will need to have LXD installed and configured. There is an excellent [getting started guide](https://documentation.ubuntu.com/lxd/en/latest/getting_started/) for LXD. Follow the unix socket so no further configuration will be needed. Go ahead and create a new config somwhere where GARM can access it and paste that entire snippet. For the purposes of this doc, we'll assume you created a new file called `/etc/garm/garm-provider-lxd.toml`. Now we need to define the external provider config in `/etc/garm/config.toml`: @@ -142,32 +142,6 @@ Go ahead and create a new config somwhere where GARM can access it and paste tha config_file = "/etc/garm/garm-provider-lxd.toml" ``` -## The credentials section - -The credentials section is where we define out GitHub credentials. GARM is capable of using either GitHub proper or [GitHub Enterprise Server](https://docs.github.com/en/enterprise-server@3.6/get-started/onboarding/getting-started-with-github-enterprise-server). The credentials section allows you to override the default GitHub API endpoint and point it to your own deployment of GHES. - -The credentials section is [documented in a separate doc](./github_credentials.md), but we will include a small snippet here for clarity. -wget -q -O - https://github.com/cloudbase/garm/releases/download/v0.1.4/garm-linux-amd64.tgz | tar xzf - -C /usr/local/bin/ -```toml -# This is a list of credentials that you can define as part of the repository -# or organization definitions. They are not saved inside the database, as there -# is no Vault integration (yet). This will change in the future. -# Credentials defined here can be listed using the API. Obviously, only the name -# and descriptions are returned. -[[github]] - name = "gabriel" - description = "github token for user gabriel" - # This is a personal token with access to the repositories and organizations - # you plan on adding to garm. The "workflow" option needs to be selected in order - # to work with repositories, and the admin:org needs to be set if you plan on - # adding an organization. - oauth2_token = "super secret token" -``` - -The `oauth2_token` option will hold the PAT we created earlier. You can add multiple credentials to the config file. Each will be referenced by name when we define the repo/org/enterprise. - -Alright, we're almost there. We have a config file with a provider and a credentials section. We now have to start the service and create a webhook in GitHub pointing at our `webhook` endpoint. - ## Starting the service You can start GARM using docker or directly on your system. I'll show you both ways. @@ -281,15 +255,11 @@ Excellent! We have a working GARM installation. Now we need to set up the webhoo ## Setting up the webhook -Before we create a pool, we need to set up the webhook in GitHub. This is a fairly simple process. - -Head over to the [webhooks doc](./webhooks.md) and follow the instructions there. Come back here when you're done. +There are two options when it comes to setting up the webhook in GitHub. You can manually set up the webhook in the GitHub UI, and then use the resulting secret when creating the entity (repo, org, enterprise), or you can let GARM do it automatically if the app or PAT you're using has the [required privileges](./github_credentials.md). -After you've finished setting up the webhook, there are just a few more things to do: +If you want to manually set up the webhooks, have a look at the [webhooks doc](./webhooks.md) for more information. -* Initialize GARM -* Add a repo/org/enterprise -* Create a pool +In this guide, I'll show you how to do it automatically when adding a new repo, assuming you have the required privileges. Note, you'll still have to manually set up webhooks if you want to use GARM at the enterprise level. Automatic webhook management is only available for repos and orgs. ## Initializing GARM @@ -298,7 +268,7 @@ Before we can start using GARM, we need initialize it. This will create the `adm To initialize GARM, we'll use the `garm-cli` tool. You can download the latest release from the [releases page](https://github.com/cloudbase/garm/releases): ```bash -wget -q -O - https://github.com/cloudbase/garm/releases/download/v0.1.3/garm-cli-linux-amd64.tgz | tar xzf - -C /usr/local/bin/ +wget -q -O - https://github.com/cloudbase/garm/releases/download/v0.1.4/garm-cli-linux-amd64.tgz | tar xzf - -C /usr/local/bin/ ``` Now we can initialize GARM: @@ -332,7 +302,9 @@ ubuntu@garm:~$ garm-cli profile list Every time you init a new GARM instance, a new profile will be created in your local `garm-cli` config. You can also log into an already initialized instance using: ```bash -garm-cli profile add --name="another_garm" --url https://garm2.example.com +garm-cli profile add \ + --name="another_garm" \ + --url https://garm2.example.com ``` Then you can switch between profiles using: @@ -341,6 +313,69 @@ Then you can switch between profiles using: garm-cli profile switch another_garm ``` +## Creating a gitHub endpoint (Optional) + +This section is only of interest if you're using a GitHub Enterprise Server (GHES) deployment. If you're using [github.com](https://github.com), you can skip this section. + +Let's list existing endpoints: + +```bash +gabriel@rossak:~$ garm-cli github endpoint list ++------------+--------------------+-------------------------+ +| NAME | BASE URL | DESCRIPTION | ++------------+--------------------+-------------------------+ +| github.com | https://github.com | The github.com endpoint | ++------------+--------------------+-------------------------+ +``` + +By default, GARM creates a default `github.com` endpoint. This endpoint cannot be updated or deleted. If you want to add a new endpoint, you can do so using the `github endpoint create` command: + +```bash +garm-cli github endpoint create \ + --name example \ + --description "Just an example ghes endpoint" \ + --base-url https://ghes.example.com \ + --upload-url https://upload.ghes.example.com \ + --api-base-url https://api.ghes.example.com \ + --ca-cert-path $HOME/ca-cert.pem +``` + +In this exampe, we add a new github endpoint called `example`. The `ca-cert-path` is optional and is used to verify the server's certificate. If you don't provide a path, GARM will use the system's default CA certificates. + +## Adding credentials + +Before we can add a new entity, we need github credentials to interact with that entity (manipulate runners, create webhooks, etc). Credentials are tied to a specific github endpoint. In this section we'll be adding credentials that are valid for either [github.com](https://github.com) or your own GHES server (if you added one in the previous section). + +When creating a new entity (repo, org, enterprise) using the credentials you define here, GARM will automatically associate that entity with the gitHub endpoint the credentials use. + +If you want to swap the credentials for an entity, the new credentials will need to be associated with the same endpoint as the old credentials. + +Let's add some credentials: + +```bash +garm-cli github credentials add \ + --name gabriel \ + --description "GitHub PAT for user gabriel" \ + --auth-type pat \ + --pat-oauth-token gh_theRestOfThePAT \ + --endpoint github.com +``` + +You can also add a GitHub App as credentials. The process is similar, but you'll need to provide the `app_id`, `private_key_path` and `installation_id`: + +```bash +garm-cli github credentials add \ + --name gabriel_app \ + --description "Github App with access to repos" \ + --endpoint github.com \ + --auth-type app \ + --app-id 1 \ + --app-installation-id 99 \ + --private-key-path $HOME/yourAppName.2024-03-01.private-key.pem +``` + +All sensitive info is encrypted at rest. Also, the API will not return sensitive data. + ## Define a repo We now have a working GARM installation, with github credentials and a provider added. It's time to add a repo. @@ -348,45 +383,51 @@ We now have a working GARM installation, with github credentials and a provider Before we add a repo, let's list credentials. We'll need their names when we'll add a new repo. ```bash -gabriel@rossak:~$ garm-cli credentials list -+---------+-------------------------------+--------------------+-------------------------+-----------------------------+ -| NAME | DESCRIPTION | BASE URL | API URL | UPLOAD URL | -+---------+-------------------------------+--------------------+-------------------------+-----------------------------+ -| gabriel | github token for user gabriel | https://github.com | https://api.github.com/ | https://uploads.github.com/ | -+---------+-------------------------------+--------------------+-------------------------+-----------------------------+ +ubuntu@garm:~$ garm-cli github credentials list ++----+-------------+------------------------------------+--------------------+-------------------------+-----------------------------+------+ +| ID | NAME | DESCRIPTION | BASE URL | API URL | UPLOAD URL | TYPE | ++----+-------------+------------------------------------+--------------------+-------------------------+-----------------------------+------+ +| 1 | gabriel | GitHub PAT for user gabriel | https://github.com | https://api.github.com/ | https://uploads.github.com/ | pat | ++----+-------------+------------------------------------+--------------------+-------------------------+-----------------------------+------+ +| 2 | gabriel_app | Github App with access to repos | https://github.com | https://api.github.com/ | https://uploads.github.com/ | app | ++----+-------------+------------------------------------+--------------------+-------------------------+-----------------------------+------+ ``` -Even though you didn't explicitly set the URLs, GARM will default to the GitHub ones. You can override them if you want to use a GHES deployment. - Now we can add a repo: ```bash garm-cli repo add \ - --credentials gabriel \ --owner gsamfira \ --name scripts \ - --webhook-secret $SECRET + --credentials gabriel \ + --random-webhook-secret \ + --install-webhook \ + --pool-balancer-type roundrobin ``` -In this case, `$SECRET` holds the webhook secret you set previously when you defined the webhook in GitHub. This secret is mandatory as GARM will always validate the webhook payloads it receives. +This will add a new repo called `scripts` under the `gsamfira` org. We also tell GARM to generate a random secret and install a webhook using that random secret. If you want to use a specific secret, you can use the `--webhook-secret` option, but in that case, you'll have to manually set up the webhook in GitHub. + +The `--pool-balancer-type` option is used to set the pool balancer type. That dictates how GARM will choose in which pool it should create a new runner when consuming recorded queued jobs. If `roundrobin` (default) is used, GARM will cycle through all pools and create a runner in the first pool that has available resources. If `pack` is used, GARM will try to fill up a pool before moving to the next one. The order of the pools is determined by the pool priority. We'll see more about pools in the next section. You should see something like this: ```bash gabriel@rossak:~$ garm-cli repo add \ -> --credentials gabriel \ -> --owner gsamfira \ -> --name scripts \ -> --webhook-secret $SECRET + --name scripts \ + --credentials gabriel_org \ + --install-webhook \ + --random-webhook-secret \ + --owner gsamfira \ + --pool-balancer-type roundrobin +----------------------+--------------------------------------+ | FIELD | VALUE | +----------------------+--------------------------------------+ -| ID | f4900c7c-2ec0-41bd-9eab-d70fe9bd850d | +| ID | 0c91d9fd-2417-45d4-883c-05daeeaa8272 | | Owner | gsamfira | | Name | scripts | -| Credentials | gabriel | -| Pool manager running | false | -| Failure reason | | +| Pool balancer type | roundrobin | +| Credentials | gabriel_app | +| Pool manager running | true | +----------------------+--------------------------------------+ ``` @@ -394,11 +435,11 @@ We can now list the repos: ```bash gabriel@rock:~$ garm-cli repo ls -+--------------------------------------+----------+---------+------------------+------------------+ -| ID | OWNER | NAME | CREDENTIALS NAME | POOL MGR RUNNING | -+--------------------------------------+----------+---------+------------------+------------------+ -| f4900c7c-2ec0-41bd-9eab-d70fe9bd850d | gsamfira | scripts | gabriel | true | -+--------------------------------------+----------+---------+------------------+------------------+ ++--------------------------------------+----------+--------------+------------------+--------------------+------------------+ +| ID | OWNER | NAME | CREDENTIALS NAME | POOL BALANCER TYPE | POOL MGR RUNNING | ++--------------------------------------+----------+--------------+------------------+--------------------+------------------+ +| 0c91d9fd-2417-45d4-883c-05daeeaa8272 | gsamfira | scripts | gabriel | pack | true | ++--------------------------------------+----------+--------------+------------------+--------------------+------------------+ ``` Excellent! Make a note of the ID. We'll need it later when we create a pool. @@ -411,18 +452,18 @@ To create a pool we'll need the repo ID from the previous step (which we have) a ```bash gabriel@rossak:~$ garm-cli provider list -+-----------+------------------------+------+ -| NAME | DESCRIPTION | TYPE | -+-----------+------------------------+------+ -| lxd_local | Local LXD installation | lxd | -+-----------+------------------------+------+ ++-----------+------------------------+-----------+ +| NAME | DESCRIPTION | TYPE | ++-----------+------------------------+-----------+ +| lxd_local | Local LXD installation | external | ++-----------+------------------------+-----------+ ``` Now we can create a pool: ```bash garm-cli pool add \ - --repo f4900c7c-2ec0-41bd-9eab-d70fe9bd850d \ + --repo 0c91d9fd-2417-45d4-883c-05daeeaa8272 \ --enabled true \ --provider-name lxd_local \ --flavor default \ @@ -438,7 +479,7 @@ You should see something like this: ```bash gabriel@rossak:~$ garm-cli pool add \ -> --repo f4900c7c-2ec0-41bd-9eab-d70fe9bd850d \ +> --repo 0c91d9fd-2417-45d4-883c-05daeeaa8272 \ > --enabled true \ > --provider-name lxd_local \ > --flavor default \ @@ -453,6 +494,7 @@ gabriel@rossak:~$ garm-cli pool add \ +--------------------------+--------------------------------------------+ | ID | 344e4a72-2035-4a18-a3d5-87bd3874b56c | | Provider Name | lxd_local | +| Priority | 0 | | Image | ubuntu:22.04 | | Flavor | default | | OS Type | linux | @@ -474,11 +516,11 @@ If we list the pool we should see it: ```bash gabriel@rock:~$ garm-cli pool ls -a -+--------------------------------------+--------------+---------+----------------------------------------+------------------+-------+---------+---------------+ -| ID | IMAGE | FLAVOR | TAGS | BELONGS TO | LEVEL | ENABLED | RUNNER PREFIX | -+--------------------------------------+--------------+---------+----------------------------------------+------------------+-------+---------+---------------+ -| 344e4a72-2035-4a18-a3d5-87bd3874b56c | ubuntu:22.04 | default | self-hosted amd64 Linux ubuntu generic | gsamfira/scripts | repo | true | garm | -+--------------------------------------+--------------+---------+----------------------------------------+------------------+-------+---------+---------------+ ++--------------------------------------+---------------------------+--------------+-----------------------------------------+------------------+-------+---------+---------------+----------+ +| ID | IMAGE | FLAVOR | TAGS | BELONGS TO | LEVEL | ENABLED | RUNNER PREFIX | PRIORITY | ++--------------------------------------+---------------------------+--------------+-----------------------------------------+------------------+-------+---------+---------------+----------+ +| 344e4a72-2035-4a18-a3d5-87bd3874b56c | ubuntu:22.04 | default | self-hosted amd64 Linux ubuntu generic | gsamfira/scripts | repo | true | garm | 0 | ++--------------------------------------+---------------------------+--------------+-----------------------------------------+------------------+-------+---------+---------------+----------+ ``` This pool is enabled, but the `min-idle-runners` option is set to 0. This means that it will not create any lingering runners. It will only create runners when a job is started. If your provider is slow to boot up new instances, you may want to set this to a value higher than 0. @@ -504,6 +546,7 @@ gabriel@rossak:~$ garm-cli pool update 344e4a72-2035-4a18-a3d5-87bd3874b56c --mi +--------------------------+--------------------------------------------+ | ID | 344e4a72-2035-4a18-a3d5-87bd3874b56c | | Provider Name | lxd_local | +| Priority | 0 | | Image | ubuntu:22.04 | | Flavor | default | | OS Type | linux |