|
| 1 | +package sql |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/json" |
| 6 | + "fmt" |
| 7 | + "testing" |
| 8 | + |
| 9 | + commonParams "github.com/cloudbase/garm-provider-common/params" |
| 10 | + |
| 11 | + dbCommon "github.com/cloudbase/garm/database/common" |
| 12 | + "github.com/cloudbase/garm/database/watcher" |
| 13 | + garmTesting "github.com/cloudbase/garm/internal/testing" |
| 14 | + "github.com/cloudbase/garm/params" |
| 15 | + "github.com/stretchr/testify/suite" |
| 16 | +) |
| 17 | + |
| 18 | +type ScaleSetsTestSuite struct { |
| 19 | + suite.Suite |
| 20 | + Store dbCommon.Store |
| 21 | + adminCtx context.Context |
| 22 | + creds params.GithubCredentials |
| 23 | + |
| 24 | + org params.Organization |
| 25 | + repo params.Repository |
| 26 | + enterprise params.Enterprise |
| 27 | + |
| 28 | + orgEntity params.GithubEntity |
| 29 | + repoEntity params.GithubEntity |
| 30 | + enterpriseEntity params.GithubEntity |
| 31 | +} |
| 32 | + |
| 33 | +func (s *ScaleSetsTestSuite) SetupTest() { |
| 34 | + // create testing sqlite database |
| 35 | + ctx := context.Background() |
| 36 | + watcher.InitWatcher(ctx) |
| 37 | + |
| 38 | + db, err := NewSQLDatabase(context.Background(), garmTesting.GetTestSqliteDBConfig(s.T())) |
| 39 | + if err != nil { |
| 40 | + s.FailNow(fmt.Sprintf("failed to create db connection: %s", err)) |
| 41 | + } |
| 42 | + s.Store = db |
| 43 | + |
| 44 | + adminCtx := garmTesting.ImpersonateAdminContext(ctx, db, s.T()) |
| 45 | + s.adminCtx = adminCtx |
| 46 | + |
| 47 | + githubEndpoint := garmTesting.CreateDefaultGithubEndpoint(adminCtx, db, s.T()) |
| 48 | + s.creds = garmTesting.CreateTestGithubCredentials(adminCtx, "new-creds", db, s.T(), githubEndpoint) |
| 49 | + |
| 50 | + // create an organization for testing purposes |
| 51 | + s.org, err = s.Store.CreateOrganization(s.adminCtx, "test-org", s.creds.Name, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) |
| 52 | + if err != nil { |
| 53 | + s.FailNow(fmt.Sprintf("failed to create org: %s", err)) |
| 54 | + } |
| 55 | + |
| 56 | + s.repo, err = s.Store.CreateRepository(s.adminCtx, "test-org", "test-repo", s.creds.Name, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) |
| 57 | + if err != nil { |
| 58 | + s.FailNow(fmt.Sprintf("failed to create repo: %s", err)) |
| 59 | + } |
| 60 | + |
| 61 | + s.enterprise, err = s.Store.CreateEnterprise(s.adminCtx, "test-enterprise", s.creds.Name, "test-webhookSecret", params.PoolBalancerTypeRoundRobin) |
| 62 | + if err != nil { |
| 63 | + s.FailNow(fmt.Sprintf("failed to create enterprise: %s", err)) |
| 64 | + } |
| 65 | + |
| 66 | + s.orgEntity, err = s.org.GetEntity() |
| 67 | + if err != nil { |
| 68 | + s.FailNow(fmt.Sprintf("failed to get org entity: %s", err)) |
| 69 | + } |
| 70 | + |
| 71 | + s.repoEntity, err = s.repo.GetEntity() |
| 72 | + if err != nil { |
| 73 | + s.FailNow(fmt.Sprintf("failed to get repo entity: %s", err)) |
| 74 | + } |
| 75 | + |
| 76 | + s.enterpriseEntity, err = s.enterprise.GetEntity() |
| 77 | + if err != nil { |
| 78 | + s.FailNow(fmt.Sprintf("failed to get enterprise entity: %s", err)) |
| 79 | + } |
| 80 | + |
| 81 | + s.T().Cleanup(func() { |
| 82 | + err := s.Store.DeleteOrganization(s.adminCtx, s.org.ID) |
| 83 | + if err != nil { |
| 84 | + s.FailNow(fmt.Sprintf("failed to delete org: %s", err)) |
| 85 | + } |
| 86 | + err = s.Store.DeleteRepository(s.adminCtx, s.repo.ID) |
| 87 | + if err != nil { |
| 88 | + s.FailNow(fmt.Sprintf("failed to delete repo: %s", err)) |
| 89 | + } |
| 90 | + err = s.Store.DeleteEnterprise(s.adminCtx, s.enterprise.ID) |
| 91 | + if err != nil { |
| 92 | + s.FailNow(fmt.Sprintf("failed to delete enterprise: %s", err)) |
| 93 | + } |
| 94 | + }) |
| 95 | +} |
| 96 | + |
| 97 | +func (s *ScaleSetsTestSuite) callback(old, newSet params.ScaleSet) error { |
| 98 | + s.Require().Equal(old.Name, "test-scaleset") |
| 99 | + s.Require().Equal(newSet.Name, "test-scaleset-updated") |
| 100 | + s.Require().Equal(old.OSType, commonParams.Linux) |
| 101 | + s.Require().Equal(newSet.OSType, commonParams.Windows) |
| 102 | + s.Require().Equal(old.OSArch, commonParams.Amd64) |
| 103 | + s.Require().Equal(newSet.OSArch, commonParams.Arm64) |
| 104 | + s.Require().Equal(old.ExtraSpecs, json.RawMessage(`{"test": 1}`)) |
| 105 | + s.Require().Equal(newSet.ExtraSpecs, json.RawMessage(`{"test": 111}`)) |
| 106 | + s.Require().Equal(old.MaxRunners, uint(10)) |
| 107 | + s.Require().Equal(newSet.MaxRunners, uint(60)) |
| 108 | + s.Require().Equal(old.MinIdleRunners, uint(5)) |
| 109 | + s.Require().Equal(newSet.MinIdleRunners, uint(50)) |
| 110 | + s.Require().Equal(old.Image, "test-image") |
| 111 | + s.Require().Equal(newSet.Image, "new-test-image") |
| 112 | + s.Require().Equal(old.Flavor, "test-flavor") |
| 113 | + s.Require().Equal(newSet.Flavor, "new-test-flavor") |
| 114 | + s.Require().Equal(old.GitHubRunnerGroup, "test-group") |
| 115 | + s.Require().Equal(newSet.GitHubRunnerGroup, "new-test-group") |
| 116 | + s.Require().Equal(old.RunnerPrefix.Prefix, "garm") |
| 117 | + s.Require().Equal(newSet.RunnerPrefix.Prefix, "test-prefix2") |
| 118 | + s.Require().Equal(old.Enabled, false) |
| 119 | + s.Require().Equal(newSet.Enabled, true) |
| 120 | + return nil |
| 121 | +} |
| 122 | + |
| 123 | +func (s *ScaleSetsTestSuite) TestScaleSetOperations() { |
| 124 | + // create a scale set for the organization |
| 125 | + createScaleSetPrams := params.CreateScaleSetParams{ |
| 126 | + Name: "test-scaleset", |
| 127 | + ProviderName: "test-provider", |
| 128 | + MaxRunners: 10, |
| 129 | + MinIdleRunners: 5, |
| 130 | + Image: "test-image", |
| 131 | + Flavor: "test-flavor", |
| 132 | + OSType: commonParams.Linux, |
| 133 | + OSArch: commonParams.Amd64, |
| 134 | + ExtraSpecs: json.RawMessage(`{"test": 1}`), |
| 135 | + GitHubRunnerGroup: "test-group", |
| 136 | + } |
| 137 | + |
| 138 | + var orgScaleSet params.ScaleSet |
| 139 | + var repoScaleSet params.ScaleSet |
| 140 | + var enterpriseScaleSet params.ScaleSet |
| 141 | + var err error |
| 142 | + |
| 143 | + s.T().Run("create org scaleset", func(t *testing.T) { |
| 144 | + orgScaleSet, err = s.Store.CreateEntityScaleSet(s.adminCtx, s.orgEntity, createScaleSetPrams) |
| 145 | + s.Require().NoError(err) |
| 146 | + s.Require().NotNil(orgScaleSet) |
| 147 | + s.Require().Equal(orgScaleSet.Name, createScaleSetPrams.Name) |
| 148 | + s.T().Cleanup(func() { |
| 149 | + err := s.Store.DeleteScaleSetByID(s.adminCtx, orgScaleSet.ID) |
| 150 | + if err != nil { |
| 151 | + s.FailNow(fmt.Sprintf("failed to delete scaleset: %s", err)) |
| 152 | + } |
| 153 | + }) |
| 154 | + |
| 155 | + }) |
| 156 | + |
| 157 | + s.T().Run("create repo scaleset", func(t *testing.T) { |
| 158 | + repoScaleSet, err = s.Store.CreateEntityScaleSet(s.adminCtx, s.repoEntity, createScaleSetPrams) |
| 159 | + s.Require().NoError(err) |
| 160 | + s.Require().NotNil(repoScaleSet) |
| 161 | + s.Require().Equal(repoScaleSet.Name, createScaleSetPrams.Name) |
| 162 | + s.T().Cleanup(func() { |
| 163 | + err := s.Store.DeleteScaleSetByID(s.adminCtx, repoScaleSet.ID) |
| 164 | + if err != nil { |
| 165 | + s.FailNow(fmt.Sprintf("failed to delete scaleset: %s", err)) |
| 166 | + } |
| 167 | + }) |
| 168 | + }) |
| 169 | + |
| 170 | + s.T().Run("create enterprise scaleset", func(t *testing.T) { |
| 171 | + enterpriseScaleSet, err = s.Store.CreateEntityScaleSet(s.adminCtx, s.enterpriseEntity, createScaleSetPrams) |
| 172 | + s.Require().NoError(err) |
| 173 | + s.Require().NotNil(enterpriseScaleSet) |
| 174 | + s.Require().Equal(enterpriseScaleSet.Name, createScaleSetPrams.Name) |
| 175 | + |
| 176 | + s.T().Cleanup(func() { |
| 177 | + err := s.Store.DeleteScaleSetByID(s.adminCtx, enterpriseScaleSet.ID) |
| 178 | + if err != nil { |
| 179 | + s.FailNow(fmt.Sprintf("failed to delete scaleset: %s", err)) |
| 180 | + } |
| 181 | + }) |
| 182 | + }) |
| 183 | + |
| 184 | + s.T().Run("create list all scalesets", func(t *testing.T) { |
| 185 | + allScaleSets, err := s.Store.ListAllScaleSets(s.adminCtx) |
| 186 | + s.Require().NoError(err) |
| 187 | + s.Require().NotEmpty(allScaleSets) |
| 188 | + s.Require().Len(allScaleSets, 3) |
| 189 | + }) |
| 190 | + |
| 191 | + s.T().Run("list repo scalesets", func(t *testing.T) { |
| 192 | + repoScaleSets, err := s.Store.ListEntityScaleSets(s.adminCtx, s.repoEntity) |
| 193 | + s.Require().NoError(err) |
| 194 | + s.Require().NotEmpty(repoScaleSets) |
| 195 | + s.Require().Len(repoScaleSets, 1) |
| 196 | + }) |
| 197 | + |
| 198 | + s.T().Run("list org scalesets", func(t *testing.T) { |
| 199 | + orgScaleSets, err := s.Store.ListEntityScaleSets(s.adminCtx, s.orgEntity) |
| 200 | + s.Require().NoError(err) |
| 201 | + s.Require().NotEmpty(orgScaleSets) |
| 202 | + s.Require().Len(orgScaleSets, 1) |
| 203 | + }) |
| 204 | + |
| 205 | + s.T().Run("list enterprise scalesets", func(t *testing.T) { |
| 206 | + enterpriseScaleSets, err := s.Store.ListEntityScaleSets(s.adminCtx, s.enterpriseEntity) |
| 207 | + s.Require().NoError(err) |
| 208 | + s.Require().NotEmpty(enterpriseScaleSets) |
| 209 | + s.Require().Len(enterpriseScaleSets, 1) |
| 210 | + }) |
| 211 | + |
| 212 | + s.T().Run("get repo scaleset by ID", func(t *testing.T) { |
| 213 | + repoScaleSetByID, err := s.Store.GetScaleSetByID(s.adminCtx, repoScaleSet.ID) |
| 214 | + s.Require().NoError(err) |
| 215 | + s.Require().NotNil(repoScaleSetByID) |
| 216 | + s.Require().Equal(repoScaleSetByID.ID, repoScaleSet.ID) |
| 217 | + s.Require().Equal(repoScaleSetByID.Name, repoScaleSet.Name) |
| 218 | + }) |
| 219 | + |
| 220 | + s.T().Run("get org scaleset by ID", func(t *testing.T) { |
| 221 | + orgScaleSetByID, err := s.Store.GetScaleSetByID(s.adminCtx, orgScaleSet.ID) |
| 222 | + s.Require().NoError(err) |
| 223 | + s.Require().NotNil(orgScaleSetByID) |
| 224 | + s.Require().Equal(orgScaleSetByID.ID, orgScaleSet.ID) |
| 225 | + s.Require().Equal(orgScaleSetByID.Name, orgScaleSet.Name) |
| 226 | + }) |
| 227 | + |
| 228 | + s.T().Run("get enterprise scaleset by ID", func(t *testing.T) { |
| 229 | + enterpriseScaleSetByID, err := s.Store.GetScaleSetByID(s.adminCtx, enterpriseScaleSet.ID) |
| 230 | + s.Require().NoError(err) |
| 231 | + s.Require().NotNil(enterpriseScaleSetByID) |
| 232 | + s.Require().Equal(enterpriseScaleSetByID.ID, enterpriseScaleSet.ID) |
| 233 | + s.Require().Equal(enterpriseScaleSetByID.Name, enterpriseScaleSet.Name) |
| 234 | + }) |
| 235 | + |
| 236 | + s.T().Run("get scaleset by ID not found", func(t *testing.T) { |
| 237 | + _, err = s.Store.GetScaleSetByID(s.adminCtx, 999) |
| 238 | + s.Require().Error(err) |
| 239 | + s.Require().Contains(err.Error(), "not found") |
| 240 | + }) |
| 241 | + |
| 242 | + s.T().Run("Set scale set last message ID and desired count", func(t *testing.T) { |
| 243 | + err = s.Store.SetScaleSetLastMessageID(s.adminCtx, orgScaleSet.ID, 20) |
| 244 | + s.Require().NoError(err) |
| 245 | + err = s.Store.SetScaleSetDesiredRunnerCount(s.adminCtx, orgScaleSet.ID, 5) |
| 246 | + s.Require().NoError(err) |
| 247 | + orgScaleSetByID, err := s.Store.GetScaleSetByID(s.adminCtx, orgScaleSet.ID) |
| 248 | + s.Require().NoError(err) |
| 249 | + s.Require().NotNil(orgScaleSetByID) |
| 250 | + s.Require().Equal(orgScaleSetByID.LastMessageID, int64(20)) |
| 251 | + s.Require().Equal(orgScaleSetByID.DesiredRunnerCount, 5) |
| 252 | + }) |
| 253 | + |
| 254 | + updateParams := params.UpdateScaleSetParams{ |
| 255 | + Name: "test-scaleset-updated", |
| 256 | + RunnerPrefix: params.RunnerPrefix{ |
| 257 | + Prefix: "test-prefix2", |
| 258 | + }, |
| 259 | + OSType: commonParams.Windows, |
| 260 | + OSArch: commonParams.Arm64, |
| 261 | + ExtraSpecs: json.RawMessage(`{"test": 111}`), |
| 262 | + Enabled: garmTesting.Ptr(true), |
| 263 | + MaxRunners: garmTesting.Ptr(uint(60)), |
| 264 | + MinIdleRunners: garmTesting.Ptr(uint(50)), |
| 265 | + Image: "new-test-image", |
| 266 | + Flavor: "new-test-flavor", |
| 267 | + GitHubRunnerGroup: garmTesting.Ptr("new-test-group"), |
| 268 | + } |
| 269 | + |
| 270 | + s.T().Run("update repo scaleset", func(t *testing.T) { |
| 271 | + newRepoScaleSet, err := s.Store.UpdateEntityScaleSet(s.adminCtx, s.repoEntity, repoScaleSet.ID, updateParams, s.callback) |
| 272 | + s.Require().NoError(err) |
| 273 | + s.Require().NotNil(newRepoScaleSet) |
| 274 | + s.Require().NoError(s.callback(repoScaleSet, newRepoScaleSet)) |
| 275 | + }) |
| 276 | + |
| 277 | + s.T().Run("update org scaleset", func(t *testing.T) { |
| 278 | + newOrgScaleSet, err := s.Store.UpdateEntityScaleSet(s.adminCtx, s.orgEntity, orgScaleSet.ID, updateParams, s.callback) |
| 279 | + s.Require().NoError(err) |
| 280 | + s.Require().NotNil(newOrgScaleSet) |
| 281 | + s.Require().NoError(s.callback(orgScaleSet, newOrgScaleSet)) |
| 282 | + }) |
| 283 | + |
| 284 | + s.T().Run("update enterprise scaleset", func(t *testing.T) { |
| 285 | + newEnterpriseScaleSet, err := s.Store.UpdateEntityScaleSet(s.adminCtx, s.enterpriseEntity, enterpriseScaleSet.ID, updateParams, s.callback) |
| 286 | + s.Require().NoError(err) |
| 287 | + s.Require().NotNil(newEnterpriseScaleSet) |
| 288 | + s.Require().NoError(s.callback(enterpriseScaleSet, newEnterpriseScaleSet)) |
| 289 | + }) |
| 290 | + |
| 291 | + s.T().Run("update scaleset not found", func(t *testing.T) { |
| 292 | + _, err = s.Store.UpdateEntityScaleSet(s.adminCtx, s.enterpriseEntity, 99999, updateParams, s.callback) |
| 293 | + s.Require().Error(err) |
| 294 | + s.Require().Contains(err.Error(), "not found") |
| 295 | + }) |
| 296 | + |
| 297 | + s.T().Run("update scaleset with invalid entity", func(t *testing.T) { |
| 298 | + _, err = s.Store.UpdateEntityScaleSet(s.adminCtx, params.GithubEntity{}, enterpriseScaleSet.ID, params.UpdateScaleSetParams{}, nil) |
| 299 | + s.Require().Error(err) |
| 300 | + s.Require().Contains(err.Error(), "missing entity id") |
| 301 | + }) |
| 302 | + |
| 303 | + s.T().Run("Create repo scale set instance", func(t *testing.T) { |
| 304 | + param := params.CreateInstanceParams{ |
| 305 | + Name: "test-instance", |
| 306 | + Status: commonParams.InstancePendingCreate, |
| 307 | + RunnerStatus: params.RunnerPending, |
| 308 | + OSType: commonParams.Linux, |
| 309 | + OSArch: commonParams.Amd64, |
| 310 | + CallbackURL: "http://localhost:8080/callback", |
| 311 | + MetadataURL: "http://localhost:8080/metadata", |
| 312 | + GitHubRunnerGroup: "test-group", |
| 313 | + JitConfiguration: map[string]string{ |
| 314 | + "test": "test", |
| 315 | + }, |
| 316 | + AgentID: 5, |
| 317 | + } |
| 318 | + |
| 319 | + instance, err := s.Store.CreateScaleSetInstance(s.adminCtx, repoScaleSet.ID, param) |
| 320 | + s.Require().NoError(err) |
| 321 | + s.Require().NotNil(instance) |
| 322 | + s.Require().Equal(instance.Name, param.Name) |
| 323 | + s.Require().Equal(instance.Status, param.Status) |
| 324 | + s.Require().Equal(instance.RunnerStatus, param.RunnerStatus) |
| 325 | + s.Require().Equal(instance.OSType, param.OSType) |
| 326 | + s.Require().Equal(instance.OSArch, param.OSArch) |
| 327 | + s.Require().Equal(instance.CallbackURL, param.CallbackURL) |
| 328 | + s.Require().Equal(instance.MetadataURL, param.MetadataURL) |
| 329 | + s.Require().Equal(instance.GitHubRunnerGroup, param.GitHubRunnerGroup) |
| 330 | + s.Require().Equal(instance.JitConfiguration, param.JitConfiguration) |
| 331 | + s.Require().Equal(instance.AgentID, param.AgentID) |
| 332 | + |
| 333 | + s.T().Cleanup(func() { |
| 334 | + err := s.Store.DeleteInstanceByName(s.adminCtx, instance.Name) |
| 335 | + if err != nil { |
| 336 | + s.FailNow(fmt.Sprintf("failed to delete scaleset instance: %s", err)) |
| 337 | + } |
| 338 | + }) |
| 339 | + }) |
| 340 | + |
| 341 | + s.T().Run("List repo scale set instances", func(t *testing.T) { |
| 342 | + instances, err := s.Store.ListScaleSetInstances(s.adminCtx, repoScaleSet.ID) |
| 343 | + s.Require().NoError(err) |
| 344 | + s.Require().NotEmpty(instances) |
| 345 | + s.Require().Len(instances, 1) |
| 346 | + }) |
| 347 | +} |
| 348 | + |
| 349 | +func TestScaleSetsTestSuite(t *testing.T) { |
| 350 | + t.Parallel() |
| 351 | + suite.Run(t, new(ScaleSetsTestSuite)) |
| 352 | +} |
0 commit comments