Add some tests

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
Gabriel Adrian Samfira 2025-05-19 19:45:45 +00:00
parent bb798a288a
commit b2d5609352
9 changed files with 441 additions and 24 deletions

View file

@ -44,8 +44,21 @@ const (
instanceTokenFetched contextFlags = "tokenFetched"
instanceHasJITConfig contextFlags = "hasJITConfig"
instanceParams contextFlags = "instanceParams"
instanceForgeTypeKey contextFlags = "forge_type"
)
func SetInstanceForgeType(ctx context.Context, val string) context.Context {
return context.WithValue(ctx, instanceForgeTypeKey, val)
}
func InstanceForgeType(ctx context.Context) params.EndpointType {
elem := ctx.Value(instanceForgeTypeKey)
if elem == nil {
return ""
}
return elem.(params.EndpointType)
}
func SetInstanceID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, instanceIDKey, id)
}
@ -159,7 +172,7 @@ func InstanceEntity(ctx context.Context) string {
return elem.(string)
}
func PopulateInstanceContext(ctx context.Context, instance params.Instance) context.Context {
func PopulateInstanceContext(ctx context.Context, instance params.Instance, claims *InstanceJWTClaims) context.Context {
ctx = SetInstanceID(ctx, instance.ID)
ctx = SetInstanceName(ctx, instance.Name)
ctx = SetInstancePoolID(ctx, instance.PoolID)
@ -167,6 +180,7 @@ func PopulateInstanceContext(ctx context.Context, instance params.Instance) cont
ctx = SetInstanceTokenFetched(ctx, instance.TokenFetched)
ctx = SetInstanceHasJITConfig(ctx, instance.JitConfiguration)
ctx = SetInstanceParams(ctx, instance)
ctx = SetInstanceForgeType(ctx, claims.ForgeType)
return ctx
}

View file

@ -44,6 +44,7 @@ type InstanceJWTClaims struct {
// Entity is the repo or org name
Entity string `json:"entity"`
CreateAttempt int `json:"create_attempt"`
ForgeType string `json:"forge_type"`
jwt.RegisteredClaims
}
@ -60,7 +61,7 @@ type instanceToken struct {
jwtSecret string
}
func (i *instanceToken) NewInstanceJWTToken(instance params.Instance, entity string, entityType params.ForgeEntityType, ttlMinutes uint) (string, error) {
func (i *instanceToken) NewInstanceJWTToken(instance params.Instance, entity params.ForgeEntity, entityType params.ForgeEntityType, ttlMinutes uint) (string, error) {
// Token expiration is equal to the bootstrap timeout set on the pool plus the polling
// interval garm uses to check for timed out runners. Runners that have not sent their info
// by the end of this interval are most likely failed and will be reaped by garm anyway.
@ -83,7 +84,8 @@ func (i *instanceToken) NewInstanceJWTToken(instance params.Instance, entity str
Name: instance.Name,
PoolID: instance.PoolID,
Scope: entityType,
Entity: entity,
Entity: entity.String(),
ForgeType: string(entity.Credentials.ForgeType),
CreateAttempt: instance.CreateAttempt,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
@ -124,7 +126,7 @@ func (amw *instanceMiddleware) claimsToContext(ctx context.Context, claims *Inst
return ctx, runnerErrors.ErrUnauthorized
}
ctx = PopulateInstanceContext(ctx, instanceInfo)
ctx = PopulateInstanceContext(ctx, instanceInfo, claims)
return ctx, nil
}

View file

@ -26,5 +26,5 @@ type Middleware interface {
}
type InstanceTokenGetter interface {
NewInstanceJWTToken(instance params.Instance, entity string, poolType params.ForgeEntityType, ttlMinutes uint) (string, error)
NewInstanceJWTToken(instance params.Instance, entity params.ForgeEntity, poolType params.ForgeEntityType, ttlMinutes uint) (string, error)
}

400
cache/cache_test.go vendored
View file

@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/suite"
runnerErrors "github.com/cloudbase/garm-provider-common/errors"
commonParams "github.com/cloudbase/garm-provider-common/params"
garmTesting "github.com/cloudbase/garm/internal/testing"
"github.com/cloudbase/garm/params"
@ -35,6 +36,7 @@ func (c *CacheTestSuite) TearDownTest() {
githubToolsCache.mux.Lock()
defer githubToolsCache.mux.Unlock()
githubToolsCache.entities = make(map[string]GithubEntityTools)
giteaCredentialsCache.cache = make(map[uint]params.ForgeCredentials)
credentialsCache.cache = make(map[uint]params.ForgeCredentials)
instanceCache.cache = make(map[string]params.Instance)
entityCache = &EntityCache{
@ -49,7 +51,7 @@ func (c *CacheTestSuite) TestCacheIsInitialized() {
c.Require().NotNil(entityCache)
}
func (c *CacheTestSuite) TestSetCacheWorks() {
func (c *CacheTestSuite) TestSetToolsCacheWorks() {
tools := []commonParams.RunnerApplicationDownload{
{
DownloadURL: garmTesting.Ptr("https://example.com"),
@ -65,6 +67,39 @@ func (c *CacheTestSuite) TestSetCacheWorks() {
c.Require().Equal(tools[0].GetDownloadURL(), cachedTools[0].GetDownloadURL())
}
func (c *CacheTestSuite) TestSetToolsCacheWithError() {
tools := []commonParams.RunnerApplicationDownload{
{
DownloadURL: garmTesting.Ptr("https://example.com"),
},
}
c.Require().NotNil(githubToolsCache)
c.Require().Len(githubToolsCache.entities, 0)
SetGithubToolsCache(c.entity, tools)
entity := githubToolsCache.entities[c.entity.ID]
c.Require().Equal(int64(entity.expiresAt.Sub(entity.updatedAt).Minutes()), int64(60))
c.Require().Len(githubToolsCache.entities, 1)
SetGithubToolsCacheError(c.entity, runnerErrors.ErrNotFound)
cachedTools, err := GetGithubToolsCache(c.entity.ID)
c.Require().Error(err)
c.Require().Nil(cachedTools)
}
func (c *CacheTestSuite) TestSetErrorOnNonExistingCacheEntity() {
entity := params.ForgeEntity{
ID: "non-existing-entity",
}
c.Require().NotNil(githubToolsCache)
c.Require().Len(githubToolsCache.entities, 0)
SetGithubToolsCacheError(entity, runnerErrors.ErrNotFound)
storedEntity, err := GetGithubToolsCache(entity.ID)
c.Require().Error(err)
c.Require().Nil(storedEntity)
}
func (c *CacheTestSuite) TestTimedOutToolsCache() {
tools := []commonParams.RunnerApplicationDownload{
{
@ -273,12 +308,23 @@ func (c *CacheTestSuite) TestSetGetEntityCache() {
c.Require().True(ok)
c.Require().Equal(entity.ID, cachedEntity.ID)
pool := params.Pool{
ID: "pool-1",
}
SetEntityPool(entity.ID, pool)
cachedEntityPools := GetEntityPools("test-entity")
c.Require().Equal(1, len(cachedEntityPools))
entity.Credentials.Description = "test description"
SetEntity(entity)
cachedEntity, ok = GetEntity("test-entity")
c.Require().True(ok)
c.Require().Equal(entity.ID, cachedEntity.ID)
c.Require().Equal(entity.Credentials.Description, cachedEntity.Credentials.Description)
// Make sure we don't clobber pools after updating the entity
cachedEntityPools = GetEntityPools("test-entity")
c.Require().Equal(1, len(cachedEntityPools))
}
func (c *CacheTestSuite) TestReplaceEntityPools() {
@ -623,6 +669,358 @@ func (c *CacheTestSuite) TestGetEntityPool() {
c.Require().Equal(pool.ID, poolFromCache.ID)
}
func (c *CacheTestSuite) TestSetGiteaCredentials() {
credentials := params.ForgeCredentials{
ID: 1,
Description: "test description",
}
SetGiteaCredentials(credentials)
cachedCreds, ok := GetGiteaCredentials(1)
c.Require().True(ok)
c.Require().Equal(credentials.ID, cachedCreds.ID)
cachedCreds.Description = "new description"
SetGiteaCredentials(cachedCreds)
cachedCreds, ok = GetGiteaCredentials(1)
c.Require().True(ok)
c.Require().Equal(credentials.ID, cachedCreds.ID)
c.Require().Equal("new description", cachedCreds.Description)
}
func (c *CacheTestSuite) TestGetAllGiteaCredentials() {
credentials1 := params.ForgeCredentials{
ID: 1,
}
credentials2 := params.ForgeCredentials{
ID: 2,
}
SetGiteaCredentials(credentials1)
SetGiteaCredentials(credentials2)
cachedCreds := GetAllGiteaCredentials()
c.Require().Len(cachedCreds, 2)
c.Require().Contains(cachedCreds, credentials1)
c.Require().Contains(cachedCreds, credentials2)
}
func (c *CacheTestSuite) TestDeleteGiteaCredentials() {
credentials := params.ForgeCredentials{
ID: 1,
}
SetGiteaCredentials(credentials)
cachedCreds, ok := GetGiteaCredentials(1)
c.Require().True(ok)
c.Require().Equal(credentials.ID, cachedCreds.ID)
DeleteGiteaCredentials(1)
cachedCreds, ok = GetGiteaCredentials(1)
c.Require().False(ok)
c.Require().Equal(params.ForgeCredentials{}, cachedCreds)
}
func (c *CacheTestSuite) TestDeleteGiteaCredentialsNotFound() {
credentials := params.ForgeCredentials{
ID: 1,
}
SetGiteaCredentials(credentials)
cachedCreds, ok := GetGiteaCredentials(1)
c.Require().True(ok)
c.Require().Equal(credentials.ID, cachedCreds.ID)
DeleteGiteaCredentials(2)
cachedCreds, ok = GetGiteaCredentials(1)
c.Require().True(ok)
c.Require().Equal(credentials.ID, cachedCreds.ID)
}
func (c *CacheTestSuite) TestUpdateCredentialsInAffectedEntities() {
credentials := params.ForgeCredentials{
ID: 1,
Description: "test description",
}
entity1 := params.ForgeEntity{
ID: "test-entity-1",
EntityType: params.ForgeEntityTypeOrganization,
Name: "test",
Owner: "test",
Credentials: credentials,
}
entity2 := params.ForgeEntity{
ID: "test-entity-2",
EntityType: params.ForgeEntityTypeOrganization,
Name: "test",
Owner: "test",
Credentials: credentials,
}
SetEntity(entity1)
SetEntity(entity2)
cachedEntity1, ok := GetEntity(entity1.ID)
c.Require().True(ok)
c.Require().Equal(entity1.ID, cachedEntity1.ID)
cachedEntity2, ok := GetEntity(entity2.ID)
c.Require().True(ok)
c.Require().Equal(entity2.ID, cachedEntity2.ID)
c.Require().Equal(credentials.ID, cachedEntity1.Credentials.ID)
c.Require().Equal(credentials.ID, cachedEntity2.Credentials.ID)
c.Require().Equal(credentials.Description, cachedEntity1.Credentials.Description)
c.Require().Equal(credentials.Description, cachedEntity2.Credentials.Description)
credentials.Description = "new description"
SetGiteaCredentials(credentials)
cachedEntity1, ok = GetEntity(entity1.ID)
c.Require().True(ok)
c.Require().Equal(entity1.ID, cachedEntity1.ID)
cachedEntity2, ok = GetEntity(entity2.ID)
c.Require().True(ok)
c.Require().Equal(entity2.ID, cachedEntity2.ID)
c.Require().Equal(credentials.ID, cachedEntity1.Credentials.ID)
c.Require().Equal(credentials.ID, cachedEntity2.Credentials.ID)
c.Require().Equal(credentials.Description, cachedEntity1.Credentials.Description)
c.Require().Equal(credentials.Description, cachedEntity2.Credentials.Description)
}
func (c *CacheTestSuite) TestSetGiteaEntity() {
credentials := params.ForgeCredentials{
ID: 1,
Description: "test description",
ForgeType: params.GiteaEndpointType,
}
entity := params.ForgeEntity{
ID: "test-entity",
EntityType: params.ForgeEntityTypeOrganization,
Name: "test",
Owner: "test",
Credentials: credentials,
}
SetGiteaCredentials(credentials)
SetEntity(entity)
cachedEntity, ok := GetEntity(entity.ID)
c.Require().True(ok)
c.Require().Equal(entity.ID, cachedEntity.ID)
c.Require().Equal(credentials.ID, cachedEntity.Credentials.ID)
c.Require().Equal(credentials.Description, cachedEntity.Credentials.Description)
c.Require().Equal(credentials.ForgeType, cachedEntity.Credentials.ForgeType)
}
func (c *CacheTestSuite) TestGetEntitiesUsingCredentials() {
credentials := params.ForgeCredentials{
ID: 1,
Description: "test description",
Name: "test",
ForgeType: params.GithubEndpointType,
}
credentials2 := params.ForgeCredentials{
ID: 2,
Description: "test description2",
Name: "test",
ForgeType: params.GiteaEndpointType,
}
entity1 := params.ForgeEntity{
ID: "test-entity-1",
EntityType: params.ForgeEntityTypeOrganization,
Name: "test",
Owner: "test",
Credentials: credentials,
}
entity2 := params.ForgeEntity{
ID: "test-entity-2",
EntityType: params.ForgeEntityTypeOrganization,
Name: "test",
Owner: "test",
Credentials: credentials,
}
entity3 := params.ForgeEntity{
ID: "test-entity-3",
EntityType: params.ForgeEntityTypeOrganization,
Name: "test",
Owner: "test",
Credentials: credentials2,
}
SetEntity(entity1)
SetEntity(entity2)
SetEntity(entity3)
cachedEntities := GetEntitiesUsingCredentials(credentials)
c.Require().Len(cachedEntities, 2)
c.Require().Contains(cachedEntities, entity1)
c.Require().Contains(cachedEntities, entity2)
cachedEntities = GetEntitiesUsingCredentials(credentials2)
c.Require().Len(cachedEntities, 1)
c.Require().Contains(cachedEntities, entity3)
}
func (c *CacheTestSuite) TestGetallEntities() {
credentials := params.ForgeCredentials{
ID: 1,
Description: "test description",
Name: "test",
ForgeType: params.GithubEndpointType,
}
credentials2 := params.ForgeCredentials{
ID: 2,
Description: "test description2",
Name: "test",
ForgeType: params.GiteaEndpointType,
}
entity1 := params.ForgeEntity{
ID: "test-entity-1",
EntityType: params.ForgeEntityTypeOrganization,
Name: "test",
Owner: "test",
Credentials: credentials,
CreatedAt: time.Now(),
}
entity2 := params.ForgeEntity{
ID: "test-entity-2",
EntityType: params.ForgeEntityTypeOrganization,
Name: "test",
Owner: "test",
Credentials: credentials,
CreatedAt: time.Now().Add(1 * time.Second),
}
entity3 := params.ForgeEntity{
ID: "test-entity-3",
EntityType: params.ForgeEntityTypeOrganization,
Name: "test",
Owner: "test",
Credentials: credentials2,
CreatedAt: time.Now().Add(2 * time.Second),
}
SetEntity(entity1)
SetEntity(entity2)
SetEntity(entity3)
// Sorted by creation date
cachedEntities := GetAllEntities()
c.Require().Len(cachedEntities, 3)
c.Require().Equal(cachedEntities[0], entity1)
c.Require().Equal(cachedEntities[1], entity2)
c.Require().Equal(cachedEntities[2], entity3)
}
func (c *CacheTestSuite) TestGetAllPools() {
entity := params.ForgeEntity{
ID: "test-entity",
EntityType: params.ForgeEntityTypeOrganization,
Name: "test",
Owner: "test",
}
pool1 := params.Pool{
ID: "pool-1",
CreatedAt: time.Now(),
Tags: []params.Tag{
{
Name: "tag1",
},
{
Name: "tag2",
},
},
}
pool2 := params.Pool{
ID: "pool-2",
CreatedAt: time.Now().Add(1 * time.Second),
Tags: []params.Tag{
{
Name: "tag1",
},
{
Name: "tag3",
},
},
}
SetEntity(entity)
SetEntityPool(entity.ID, pool1)
SetEntityPool(entity.ID, pool2)
cachedEntity, ok := GetEntity(entity.ID)
c.Require().True(ok)
c.Require().Equal(entity.ID, cachedEntity.ID)
pools := GetAllPools()
c.Require().Len(pools, 2)
c.Require().Equal(pools[0].ID, pool1.ID)
c.Require().Equal(pools[1].ID, pool2.ID)
}
func (c *CacheTestSuite) TestGetAllScaleSets() {
entity := params.ForgeEntity{
ID: "test-entity",
EntityType: params.ForgeEntityTypeOrganization,
Name: "test",
Owner: "test",
}
scaleSet1 := params.ScaleSet{
ID: 1,
}
scaleSet2 := params.ScaleSet{
ID: 2,
}
SetEntity(entity)
SetEntityScaleSet(entity.ID, scaleSet1)
SetEntityScaleSet(entity.ID, scaleSet2)
cachedEntity, ok := GetEntity(entity.ID)
c.Require().True(ok)
c.Require().Equal(entity.ID, cachedEntity.ID)
scaleSets := GetAllScaleSets()
c.Require().Len(scaleSets, 2)
c.Require().Equal(scaleSets[0].ID, scaleSet1.ID)
c.Require().Equal(scaleSets[1].ID, scaleSet2.ID)
}
func (c *CacheTestSuite) TestGetAllGetAllGithubCredentialsAsMap() {
credentials1 := params.ForgeCredentials{
ID: 1,
}
credentials2 := params.ForgeCredentials{
ID: 2,
}
SetGithubCredentials(credentials1)
SetGithubCredentials(credentials2)
cachedCreds := GetAllGithubCredentialsAsMap()
c.Require().Len(cachedCreds, 2)
c.Require().Contains(cachedCreds, credentials1.ID)
c.Require().Contains(cachedCreds, credentials2.ID)
}
func (c *CacheTestSuite) TestGetAllGiteaCredentialsAsMap() {
credentials1 := params.ForgeCredentials{
ID: 1,
CreatedAt: time.Now(),
}
credentials2 := params.ForgeCredentials{
ID: 2,
CreatedAt: time.Now().Add(1 * time.Second),
}
SetGiteaCredentials(credentials1)
SetGiteaCredentials(credentials2)
cachedCreds := GetAllGiteaCredentialsAsMap()
c.Require().Len(cachedCreds, 2)
c.Require().Contains(cachedCreds, credentials1.ID)
c.Require().Contains(cachedCreds, credentials2.ID)
}
func TestCacheTestSuite(t *testing.T) {
t.Parallel()
suite.Run(t, new(CacheTestSuite))

16
cache/tools_cache.go vendored
View file

@ -26,13 +26,6 @@ type GithubEntityTools struct {
tools []commonParams.RunnerApplicationDownload
}
func (g GithubEntityTools) Error() string {
if g.err != nil {
return g.err.Error()
}
return ""
}
type GithubToolsCache struct {
mux sync.Mutex
// entity IDs are UUID4s. It is highly unlikely they will collide (🤞).
@ -51,7 +44,10 @@ func (g *GithubToolsCache) Get(entityID string) ([]commonParams.RunnerApplicatio
return nil, fmt.Errorf("cache expired for entity %s", entityID)
}
}
return cache.tools, cache.err
if cache.err != nil {
return nil, cache.err
}
return cache.tools, nil
}
return nil, fmt.Errorf("no cache found for entity %s", entityID)
}
@ -101,3 +97,7 @@ func SetGithubToolsCache(entity params.ForgeEntity, tools []commonParams.RunnerA
func GetGithubToolsCache(entityID string) ([]commonParams.RunnerApplicationDownload, error) {
return githubToolsCache.Get(entityID)
}
func SetGithubToolsCacheError(entity params.ForgeEntity, err error) {
githubToolsCache.SetToolsError(entity, err)
}

View file

@ -87,16 +87,17 @@ func WithEntityPoolFilter(ghEntity params.ForgeEntity) dbCommon.PayloadFilterFun
}
}
// WithEntityPoolFilter returns true if the change payload is a pool that belongs to the
// supplied Github entity. This is useful when an entity worker wants to watch for changes
// in pools that belong to it.
// WithEntityScaleSetFilter returns true if the change payload is a scale set that belongs to the
// supplied Github entity.
func WithEntityScaleSetFilter(ghEntity params.ForgeEntity) dbCommon.PayloadFilterFunc {
return func(payload dbCommon.ChangePayload) bool {
forgeType, err := ghEntity.GetForgeType()
if err != nil {
return false
}
if forgeType != params.GiteaEndpointType {
// Gitea does not have scale sets.
if forgeType == params.GiteaEndpointType {
return false
}

View file

@ -869,8 +869,7 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error
jwtValidity := pool.RunnerTimeout()
entity := r.entity.String()
jwtToken, err := r.instanceTokenGetter.NewInstanceJWTToken(instance, entity, pool.PoolType(), jwtValidity)
jwtToken, err := r.instanceTokenGetter.NewInstanceJWTToken(instance, r.entity, pool.PoolType(), jwtValidity)
if err != nil {
return errors.Wrap(err, "fetching instance jwt token")
}

View file

@ -663,17 +663,20 @@ func (r *Runner) DispatchWorkflowJob(hookTargetType, signature string, forgeType
slog.DebugContext(
r.ctx, "got hook for repo",
"repo_owner", util.SanitizeLogEntry(job.Repository.Owner.Login),
"repo_name", util.SanitizeLogEntry(job.Repository.Name))
"repo_name", util.SanitizeLogEntry(job.Repository.Name),
"endpoint", endpoint.Name)
poolManager, err = r.findRepoPoolManager(job.Repository.Owner.Login, job.Repository.Name, endpoint.Name)
case OrganizationHook:
slog.DebugContext(
r.ctx, "got hook for organization",
"organization", util.SanitizeLogEntry(job.GetOrgName(forgeType)))
"organization", util.SanitizeLogEntry(job.GetOrgName(forgeType)),
"endpoint", endpoint.Name)
poolManager, err = r.findOrgPoolManager(job.GetOrgName(forgeType), endpoint.Name)
case EnterpriseHook:
slog.DebugContext(
r.ctx, "got hook for enterprise",
"enterprise", util.SanitizeLogEntry(job.Enterprise.Slug))
"enterprise", util.SanitizeLogEntry(job.Enterprise.Slug),
"endpoint", endpoint.Name)
poolManager, err = r.findEnterprisePoolManager(job.Enterprise.Slug, endpoint.Name)
default:
return runnerErrors.NewBadRequestError("cannot handle hook target type %s", hookTargetType)

View file

@ -144,7 +144,7 @@ func (i *instanceManager) handleCreateInstanceInProvider(instance params.Instanc
}
token, err := i.helper.InstanceTokenGetter().NewInstanceJWTToken(
instance, entity.String(), entity.EntityType, i.scaleSet.RunnerBootstrapTimeout)
instance, entity, entity.EntityType, i.scaleSet.RunnerBootstrapTimeout)
if err != nil {
return fmt.Errorf("creating instance token: %w", err)
}