2022-10-13 13:30:17 +03:00
// 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 sql
import (
"context"
2025-05-02 12:22:04 +00:00
"encoding/json"
2022-10-13 17:18:44 +03:00
"flag"
2022-10-13 13:30:17 +03:00
"fmt"
2022-10-13 17:18:44 +03:00
"regexp"
2022-10-13 13:30:17 +03:00
"testing"
"github.com/stretchr/testify/suite"
2022-10-13 17:18:44 +03:00
"gopkg.in/DATA-DOG/go-sqlmock.v1"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
2024-02-22 16:54:38 +01:00
2025-05-02 12:22:04 +00:00
commonParams "github.com/cloudbase/garm-provider-common/params"
2024-02-22 16:54:38 +01:00
dbCommon "github.com/cloudbase/garm/database/common"
2025-05-02 12:22:04 +00:00
"github.com/cloudbase/garm/database/watcher"
2024-02-22 16:54:38 +01:00
garmTesting "github.com/cloudbase/garm/internal/testing"
"github.com/cloudbase/garm/params"
2022-10-13 13:30:17 +03:00
)
type PoolsTestFixtures struct {
2022-10-13 17:18:44 +03:00
Org params . Organization
Pools [ ] params . Pool
SQLMock sqlmock . Sqlmock
2022-10-13 13:30:17 +03:00
}
type PoolsTestSuite struct {
suite . Suite
2025-05-02 12:22:04 +00:00
Store dbCommon . Store
ctx context . Context
2022-10-13 17:18:44 +03:00
StoreSQLMocked * sqlDatabase
Fixtures * PoolsTestFixtures
2024-04-16 17:05:18 +00:00
adminCtx context . Context
2022-10-13 13:30:17 +03:00
}
2022-10-13 17:18:44 +03:00
func ( s * PoolsTestSuite ) assertSQLMockExpectations ( ) {
err := s . Fixtures . SQLMock . ExpectationsWereMet ( )
if err != nil {
s . FailNow ( fmt . Sprintf ( "failed to meet sqlmock expectations, got error: %v" , err ) )
}
}
2025-05-02 12:22:04 +00:00
func ( s * PoolsTestSuite ) TearDownTest ( ) {
watcher . CloseWatcher ( )
}
2022-10-13 13:30:17 +03:00
func ( s * PoolsTestSuite ) SetupTest ( ) {
// create testing sqlite database
2025-05-02 12:22:04 +00:00
ctx := context . Background ( )
watcher . InitWatcher ( ctx )
2022-10-13 13:30:17 +03:00
db , err := NewSQLDatabase ( context . Background ( ) , garmTesting . GetTestSqliteDBConfig ( s . T ( ) ) )
if err != nil {
s . FailNow ( fmt . Sprintf ( "failed to create db connection: %s" , err ) )
}
s . Store = db
2025-05-02 12:22:04 +00:00
s . ctx = garmTesting . ImpersonateAdminContext ( ctx , s . Store , s . T ( ) )
2022-10-13 13:30:17 +03:00
2024-04-16 17:05:18 +00:00
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 )
2022-10-13 13:30:17 +03:00
// create an organization for testing purposes
2024-04-16 17:05:18 +00:00
org , err := s . Store . CreateOrganization ( s . adminCtx , "test-org" , creds . Name , "test-webhookSecret" , params . PoolBalancerTypeRoundRobin )
2022-10-13 13:30:17 +03:00
if err != nil {
s . FailNow ( fmt . Sprintf ( "failed to create org: %s" , err ) )
}
2024-03-29 18:18:29 +00:00
entity , err := org . GetEntity ( )
s . Require ( ) . Nil ( err )
2022-10-13 13:30:17 +03:00
// create some pool objects in the database, for testing purposes
orgPools := [ ] params . Pool { }
for i := 1 ; i <= 3 ; i ++ {
2024-03-28 18:23:49 +00:00
pool , err := db . CreateEntityPool (
2024-04-16 17:05:18 +00:00
s . adminCtx ,
2024-03-28 18:23:49 +00:00
entity ,
2022-10-13 13:30:17 +03:00
params . CreatePoolParams {
ProviderName : "test-provider" ,
MaxRunners : 4 ,
MinIdleRunners : 2 ,
Image : fmt . Sprintf ( "test-image-%d" , i ) ,
Flavor : "test-flavor" ,
OSType : "linux" ,
2024-05-21 07:27:07 +02:00
Tags : [ ] string { "amd64-linux-runner" } ,
2022-10-13 13:30:17 +03:00
} ,
)
if err != nil {
s . FailNow ( fmt . Sprintf ( "cannot create org pool: %v" , err ) )
}
orgPools = append ( orgPools , pool )
}
2022-10-13 17:18:44 +03:00
// create store with mocked sql connection
sqlDB , sqlMock , err := sqlmock . New ( )
if err != nil {
s . FailNow ( fmt . Sprintf ( "failed to run 'sqlmock.New()', got error: %v" , err ) )
}
s . T ( ) . Cleanup ( func ( ) { sqlDB . Close ( ) } )
mysqlConfig := mysql . Config {
Conn : sqlDB ,
SkipInitializeWithVersion : true ,
}
gormConfig := & gorm . Config { }
2024-02-22 17:20:05 +01:00
if flag . Lookup ( "test.v" ) . Value . String ( ) == falseString {
2022-10-13 17:18:44 +03:00
gormConfig . Logger = logger . Default . LogMode ( logger . Silent )
}
gormConn , err := gorm . Open ( mysql . New ( mysqlConfig ) , gormConfig )
if err != nil {
s . FailNow ( fmt . Sprintf ( "fail to open gorm connection: %v" , err ) )
}
s . StoreSQLMocked = & sqlDatabase {
conn : gormConn ,
}
2022-10-13 13:30:17 +03:00
// setup test fixtures
fixtures := & PoolsTestFixtures {
2022-10-13 17:18:44 +03:00
Org : org ,
Pools : orgPools ,
SQLMock : sqlMock ,
2022-10-13 13:30:17 +03:00
}
s . Fixtures = fixtures
}
func ( s * PoolsTestSuite ) TestListAllPools ( ) {
2024-04-16 17:05:18 +00:00
pools , err := s . Store . ListAllPools ( s . adminCtx )
2022-10-13 13:30:17 +03:00
s . Require ( ) . Nil ( err )
2022-12-04 17:30:27 +00:00
garmTesting . EqualDBEntityID ( s . T ( ) , s . Fixtures . Pools , pools )
2022-10-13 13:30:17 +03:00
}
2022-10-13 17:18:44 +03:00
func ( s * PoolsTestSuite ) TestListAllPoolsDBFetchErr ( ) {
s . Fixtures . SQLMock .
Add pool balancing strategy
This change adds the ability to specify the pool balancing strategy to
use when processing queued jobs. Before this change, GARM would round-robin
through all pools that matched the set of tags requested by queued jobs.
When round-robin (default) is used for an entity (repo, org or enterprise)
and you have 2 pools defined for that entity with a common set of tags that
match 10 jobs (for example), then those jobs would trigger the creation of
a new runner in each of the two pools in turn. Job 1 would go to pool 1,
job 2 would go to pool 2, job 3 to pool 1, job 4 to pool 2 and so on.
When "stack" is used, those same 10 jobs would trigger the creation of a
new runner in the pool with the highest priority, every time.
In both cases, if a pool is full, the next one would be tried automatically.
For the stack case, this would mean that if pool 2 had a priority of 10 and
pool 1 would have a priority of 5, pool 2 would be saturated first, then
pool 1.
Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
2024-03-14 20:04:34 +00:00
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" ) ) .
2022-10-13 17:18:44 +03:00
WillReturnError ( fmt . Errorf ( "mocked fetching all pools error" ) )
2024-04-16 17:05:18 +00:00
_ , err := s . StoreSQLMocked . ListAllPools ( s . adminCtx )
2022-10-13 17:18:44 +03:00
s . assertSQLMockExpectations ( )
s . Require ( ) . NotNil ( err )
s . Require ( ) . Equal ( "fetching all pools: mocked fetching all pools error" , err . Error ( ) )
}
2022-10-13 13:30:17 +03:00
func ( s * PoolsTestSuite ) TestGetPoolByID ( ) {
2024-04-16 17:05:18 +00:00
pool , err := s . Store . GetPoolByID ( s . adminCtx , s . Fixtures . Pools [ 0 ] . ID )
2022-10-13 13:30:17 +03:00
s . Require ( ) . Nil ( err )
s . Require ( ) . Equal ( s . Fixtures . Pools [ 0 ] . ID , pool . ID )
}
func ( s * PoolsTestSuite ) TestGetPoolByIDInvalidPoolID ( ) {
2024-04-16 17:05:18 +00:00
_ , err := s . Store . GetPoolByID ( s . adminCtx , "dummy-pool-id" )
2022-10-13 13:30:17 +03:00
s . Require ( ) . NotNil ( err )
s . Require ( ) . Equal ( "fetching pool by ID: parsing id: invalid request" , err . Error ( ) )
}
func ( s * PoolsTestSuite ) TestDeletePoolByID ( ) {
2024-04-16 17:05:18 +00:00
err := s . Store . DeletePoolByID ( s . adminCtx , s . Fixtures . Pools [ 0 ] . ID )
2022-10-13 13:30:17 +03:00
s . Require ( ) . Nil ( err )
2024-04-16 17:05:18 +00:00
_ , err = s . Store . GetPoolByID ( s . adminCtx , s . Fixtures . Pools [ 0 ] . ID )
2022-10-13 13:30:17 +03:00
s . Require ( ) . Equal ( "fetching pool by ID: not found" , err . Error ( ) )
}
func ( s * PoolsTestSuite ) TestDeletePoolByIDInvalidPoolID ( ) {
2024-04-16 17:05:18 +00:00
err := s . Store . DeletePoolByID ( s . adminCtx , "dummy-pool-id" )
2022-10-13 13:30:17 +03:00
s . Require ( ) . NotNil ( err )
s . Require ( ) . Equal ( "fetching pool by ID: parsing id: invalid request" , err . Error ( ) )
}
2022-10-13 17:18:44 +03:00
func ( s * PoolsTestSuite ) TestDeletePoolByIDDBRemoveErr ( ) {
s . Fixtures . SQLMock .
2024-04-22 13:38:51 +00:00
ExpectQuery ( regexp . QuoteMeta ( "SELECT * FROM `pools` WHERE id = ? AND `pools`.`deleted_at` IS NULL ORDER BY `pools`.`id` LIMIT ?" ) ) .
WithArgs ( s . Fixtures . Pools [ 0 ] . ID , 1 ) .
2022-10-13 17:18:44 +03:00
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" } ) . AddRow ( s . Fixtures . Pools [ 0 ] . ID ) )
s . Fixtures . SQLMock . ExpectBegin ( )
s . Fixtures . SQLMock .
ExpectExec ( regexp . QuoteMeta ( "DELETE FROM `pools` WHERE `pools`.`id` = ?" ) ) .
WillReturnError ( fmt . Errorf ( "mocked removing pool error" ) )
s . Fixtures . SQLMock . ExpectRollback ( )
2024-04-16 17:05:18 +00:00
err := s . StoreSQLMocked . DeletePoolByID ( s . adminCtx , s . Fixtures . Pools [ 0 ] . ID )
2022-10-13 17:18:44 +03:00
s . assertSQLMockExpectations ( )
s . Require ( ) . NotNil ( err )
s . Require ( ) . Equal ( "removing pool: mocked removing pool error" , err . Error ( ) )
}
2025-05-02 12:22:04 +00:00
func ( s * PoolsTestSuite ) TestEntityPoolOperations ( ) {
ep := garmTesting . CreateDefaultGithubEndpoint ( s . ctx , s . Store , s . T ( ) )
creds := garmTesting . CreateTestGithubCredentials ( s . ctx , "test-creds" , s . Store , s . T ( ) , ep )
s . T ( ) . Cleanup ( func ( ) { s . Store . DeleteGithubCredentials ( s . ctx , creds . ID ) } )
2025-05-14 00:34:54 +00:00
repo , err := s . Store . CreateRepository ( s . ctx , "test-owner" , "test-repo" , creds , "test-secret" , params . PoolBalancerTypeRoundRobin )
2025-05-02 12:22:04 +00:00
s . Require ( ) . NoError ( err )
s . Require ( ) . NotEmpty ( repo . ID )
s . T ( ) . Cleanup ( func ( ) { s . Store . DeleteRepository ( s . ctx , repo . ID ) } )
entity , err := repo . GetEntity ( )
s . Require ( ) . NoError ( err )
createPoolParams := params . CreatePoolParams {
ProviderName : "test-provider" ,
Image : "test-image" ,
Flavor : "test-flavor" ,
OSType : commonParams . Linux ,
OSArch : commonParams . Amd64 ,
Tags : [ ] string { "test-tag" } ,
}
pool , err := s . Store . CreateEntityPool ( s . ctx , entity , createPoolParams )
s . Require ( ) . NoError ( err )
s . Require ( ) . NotEmpty ( pool . ID )
s . T ( ) . Cleanup ( func ( ) { s . Store . DeleteEntityPool ( s . ctx , entity , pool . ID ) } )
entityPool , err := s . Store . GetEntityPool ( s . ctx , entity , pool . ID )
s . Require ( ) . NoError ( err )
s . Require ( ) . Equal ( pool . ID , entityPool . ID )
s . Require ( ) . Equal ( pool . ProviderName , entityPool . ProviderName )
updatePoolParams := params . UpdatePoolParams {
Enabled : garmTesting . Ptr ( true ) ,
Flavor : "new-flavor" ,
Image : "new-image" ,
RunnerPrefix : params . RunnerPrefix {
Prefix : "new-prefix" ,
} ,
MaxRunners : garmTesting . Ptr ( uint ( 100 ) ) ,
MinIdleRunners : garmTesting . Ptr ( uint ( 50 ) ) ,
OSType : commonParams . Windows ,
OSArch : commonParams . Amd64 ,
Tags : [ ] string { "new-tag" } ,
RunnerBootstrapTimeout : garmTesting . Ptr ( uint ( 10 ) ) ,
ExtraSpecs : json . RawMessage ( ` { "extra": "specs"} ` ) ,
GitHubRunnerGroup : garmTesting . Ptr ( "new-group" ) ,
Priority : garmTesting . Ptr ( uint ( 1 ) ) ,
}
pool , err = s . Store . UpdateEntityPool ( s . ctx , entity , pool . ID , updatePoolParams )
s . Require ( ) . NoError ( err )
s . Require ( ) . Equal ( * updatePoolParams . Enabled , pool . Enabled )
s . Require ( ) . Equal ( updatePoolParams . Flavor , pool . Flavor )
s . Require ( ) . Equal ( updatePoolParams . Image , pool . Image )
s . Require ( ) . Equal ( updatePoolParams . RunnerPrefix . Prefix , pool . RunnerPrefix . Prefix )
s . Require ( ) . Equal ( * updatePoolParams . MaxRunners , pool . MaxRunners )
s . Require ( ) . Equal ( * updatePoolParams . MinIdleRunners , pool . MinIdleRunners )
s . Require ( ) . Equal ( updatePoolParams . OSType , pool . OSType )
s . Require ( ) . Equal ( updatePoolParams . OSArch , pool . OSArch )
s . Require ( ) . Equal ( * updatePoolParams . RunnerBootstrapTimeout , pool . RunnerBootstrapTimeout )
s . Require ( ) . Equal ( updatePoolParams . ExtraSpecs , pool . ExtraSpecs )
s . Require ( ) . Equal ( * updatePoolParams . GitHubRunnerGroup , pool . GitHubRunnerGroup )
s . Require ( ) . Equal ( * updatePoolParams . Priority , pool . Priority )
entityPools , err := s . Store . ListEntityPools ( s . ctx , entity )
s . Require ( ) . NoError ( err )
s . Require ( ) . Len ( entityPools , 1 )
s . Require ( ) . Equal ( pool . ID , entityPools [ 0 ] . ID )
tagsToMatch := [ ] string { "new-tag" }
pools , err := s . Store . FindPoolsMatchingAllTags ( s . ctx , entity . EntityType , entity . ID , tagsToMatch )
s . Require ( ) . NoError ( err )
s . Require ( ) . Len ( pools , 1 )
s . Require ( ) . Equal ( pool . ID , pools [ 0 ] . ID )
invalidTagsToMatch := [ ] string { "invalid-tag" }
pools , err = s . Store . FindPoolsMatchingAllTags ( s . ctx , entity . EntityType , entity . ID , invalidTagsToMatch )
s . Require ( ) . NoError ( err )
s . Require ( ) . Len ( pools , 0 )
}
func ( s * PoolsTestSuite ) TestListEntityInstances ( ) {
ep := garmTesting . CreateDefaultGithubEndpoint ( s . ctx , s . Store , s . T ( ) )
creds := garmTesting . CreateTestGithubCredentials ( s . ctx , "test-creds" , s . Store , s . T ( ) , ep )
s . T ( ) . Cleanup ( func ( ) { s . Store . DeleteGithubCredentials ( s . ctx , creds . ID ) } )
2025-05-14 00:34:54 +00:00
repo , err := s . Store . CreateRepository ( s . ctx , "test-owner" , "test-repo" , creds , "test-secret" , params . PoolBalancerTypeRoundRobin )
2025-05-02 12:22:04 +00:00
s . Require ( ) . NoError ( err )
s . Require ( ) . NotEmpty ( repo . ID )
s . T ( ) . Cleanup ( func ( ) { s . Store . DeleteRepository ( s . ctx , repo . ID ) } )
entity , err := repo . GetEntity ( )
s . Require ( ) . NoError ( err )
createPoolParams := params . CreatePoolParams {
ProviderName : "test-provider" ,
Image : "test-image" ,
Flavor : "test-flavor" ,
OSType : commonParams . Linux ,
OSArch : commonParams . Amd64 ,
Tags : [ ] string { "test-tag" } ,
}
pool , err := s . Store . CreateEntityPool ( s . ctx , entity , createPoolParams )
s . Require ( ) . NoError ( err )
s . Require ( ) . NotEmpty ( pool . ID )
s . T ( ) . Cleanup ( func ( ) { s . Store . DeleteEntityPool ( s . ctx , entity , pool . ID ) } )
createInstanceParams := params . CreateInstanceParams {
Name : "test-instance" ,
OSType : commonParams . Linux ,
OSArch : commonParams . Amd64 ,
Status : commonParams . InstanceCreating ,
}
instance , err := s . Store . CreateInstance ( s . ctx , pool . ID , createInstanceParams )
s . Require ( ) . NoError ( err )
s . Require ( ) . NotEmpty ( instance . ID )
s . T ( ) . Cleanup ( func ( ) { s . Store . DeleteInstance ( s . ctx , pool . ID , instance . ID ) } )
instances , err := s . Store . ListEntityInstances ( s . ctx , entity )
s . Require ( ) . NoError ( err )
s . Require ( ) . Len ( instances , 1 )
s . Require ( ) . Equal ( instance . ID , instances [ 0 ] . ID )
s . Require ( ) . Equal ( instance . Name , instances [ 0 ] . Name )
2025-05-02 14:24:25 +00:00
s . Require ( ) . Equal ( instance . ProviderName , pool . ProviderName )
2025-05-02 12:22:04 +00:00
}
2022-10-13 13:30:17 +03:00
func TestPoolsTestSuite ( t * testing . T ) {
suite . Run ( t , new ( PoolsTestSuite ) )
}