Add some tests, move some code around

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
Gabriel Adrian Samfira 2026-01-23 07:05:50 +00:00 committed by Gabriel
parent 42cfd1b3c6
commit c29e8d4459
10 changed files with 868 additions and 79 deletions

View file

@ -245,7 +245,17 @@ func main() {
log.Fatal(err)
}
cacheWorker := cache.NewWorker(ctx, db)
instanceTokenGetter, err := auth.NewInstanceTokenGetter(cfg.JWTAuth.Secret)
if err != nil {
log.Fatalf("failed to create instance token getter: %+v", err)
}
runner, err := runner.NewRunner(ctx, *cfg, db, instanceTokenGetter)
if err != nil {
log.Fatalf("failed to create controller: %+v", err)
}
cacheWorker := cache.NewWorker(ctx, db, runner)
if err := cacheWorker.Start(); err != nil {
log.Fatalf("failed to start cache worker: %+v", err)
}
@ -263,11 +273,6 @@ func main() {
log.Fatalf("failed to start entity controller: %+v", err)
}
instanceTokenGetter, err := auth.NewInstanceTokenGetter(cfg.JWTAuth.Secret)
if err != nil {
log.Fatalf("failed to create instance token getter: %+v", err)
}
providerWorker, err := provider.NewWorker(ctx, db, providers, instanceTokenGetter)
if err != nil {
log.Fatalf("failed to create provider worker: %+v", err)
@ -276,11 +281,6 @@ func main() {
log.Fatalf("failed to start provider worker: %+v", err)
}
runner, err := runner.NewRunner(ctx, *cfg, db, instanceTokenGetter)
if err != nil {
log.Fatalf("failed to create controller: %+v", err)
}
// If there are many repos/pools, this may take a long time.
if err := runner.Start(); err != nil {
log.Fatal(err)

View file

@ -58,7 +58,7 @@ type ControllerInfo struct {
MetadataURL string
// WebhookBaseURL is the base URL used to construct the controller webhook URL.
WebhookBaseURL string
// AgentURL is the websocket enabled URL whenre garm agents connect to.
// AgentURL is the websocket enabled URL where garm agents connect to.
AgentURL string
// GARMAgentReleasesURL is the URL from which GARM can sync garm-agent binaries. Alternatively
// the user can manually upload binaries.
@ -488,7 +488,7 @@ type FileObject struct {
// Size is the file size in bytes
Size int64 `gorm:"type:integer"`
// SHA256 is the sha256 checksum (hex encoded)
SHA256 string `gorm:"type:text"`
SHA256 string `gorm:"type:text;index:idx_fo_chksum"`
// Tags is a JSON array of tags
TagsList []FileObjectTag `gorm:"foreignKey:FileObjectID;constraint:OnDelete:CASCADE"`
// Content is a foreign key to a different table where the blob is actually stored.

View file

@ -14,7 +14,11 @@
package params
import "time"
import (
"context"
"io"
"time"
)
// EntityGetter is implemented by all github entities (repositories, organizations and enterprises).
// It defines the GetEntity() function which returns a github entity.
@ -33,3 +37,9 @@ type CreationDateGetter interface {
type ForgeCredentialsGetter interface {
GetForgeCredentials() ForgeCredentials
}
type GARMToolsManager interface {
ListAllGARMTools(ctx context.Context) ([]GARMAgentTool, error)
CreateGARMTool(ctx context.Context, param CreateGARMToolParams, reader io.Reader) (FileObject, error)
DeleteGarmTool(ctx context.Context, osType, osArch string) error
}

View file

@ -33,7 +33,7 @@ var (
garmAgentOSArchARM64Tag = "os_arch=arm64"
)
func (r *Runner) ListGARMTools(ctx context.Context) ([]params.GARMAgentTool, error) {
func (r *Runner) ListAllGARMTools(ctx context.Context) ([]params.GARMAgentTool, error) {
if !auth.IsAdmin(ctx) {
return nil, runnerErrors.ErrUnauthorized
}

View file

@ -375,19 +375,19 @@ func (s *GARMToolsTestSuite) TestDeleteGarmToolDeletesAllVersions() {
s.Equal(uint64(0), results.TotalCount)
}
func (s *GARMToolsTestSuite) TestListGARMToolsUnauthorized() {
_, err := s.Runner.ListGARMTools(s.UnauthorizedContext)
func (s *GARMToolsTestSuite) TestListAllGARMToolsUnauthorized() {
_, err := s.Runner.ListAllGARMTools(s.UnauthorizedContext)
s.Require().Error(err)
s.Equal(runnerErrors.ErrUnauthorized, err)
}
func (s *GARMToolsTestSuite) TestListGARMToolsEmpty() {
tools, err := s.Runner.ListGARMTools(s.AdminContext)
func (s *GARMToolsTestSuite) TestListAllGARMToolsEmpty() {
tools, err := s.Runner.ListAllGARMTools(s.AdminContext)
s.Require().NoError(err)
s.Empty(tools)
}
func (s *GARMToolsTestSuite) TestListGARMToolsSinglePlatform() {
func (s *GARMToolsTestSuite) TestListAllGARMToolsSinglePlatform() {
// Create one tool
param := params.CreateGARMToolParams{
Name: "garm-agent-linux-amd64",
@ -401,14 +401,14 @@ func (s *GARMToolsTestSuite) TestListGARMToolsSinglePlatform() {
_, err := s.Runner.CreateGARMTool(s.AdminContext, param, reader)
s.Require().NoError(err)
tools, err := s.Runner.ListGARMTools(s.AdminContext)
tools, err := s.Runner.ListAllGARMTools(s.AdminContext)
s.Require().NoError(err)
s.Len(tools, 1)
s.Equal("linux", string(tools[0].OSType))
s.Equal("amd64", string(tools[0].OSArch))
}
func (s *GARMToolsTestSuite) TestListGARMToolsMultiplePlatforms() {
func (s *GARMToolsTestSuite) TestListAllGARMToolsMultiplePlatforms() {
// Create tools for all supported platforms
platforms := []struct {
osType commonParams.OSType
@ -434,7 +434,7 @@ func (s *GARMToolsTestSuite) TestListGARMToolsMultiplePlatforms() {
s.Require().NoError(err)
}
tools, err := s.Runner.ListGARMTools(s.AdminContext)
tools, err := s.Runner.ListAllGARMTools(s.AdminContext)
s.Require().NoError(err)
s.Len(tools, 4)

View file

@ -218,6 +218,12 @@ func (r *Runner) GetInstanceMetadata(ctx context.Context) (params.InstanceMetada
return params.InstanceMetadata{}, fmt.Errorf("failed to get entity: %w", err)
}
switch dbEntity.Credentials.ForgeType {
case params.GiteaEndpointType, params.GithubEndpointType:
default:
return params.InstanceMetadata{}, runnerErrors.NewUnprocessableError("invalid forge type: %s", dbEntity.Credentials.ForgeType)
}
ret := params.InstanceMetadata{
RunnerName: instance.Name,
RunnerLabels: getLabelsForInstance(instance),
@ -236,17 +242,22 @@ func (r *Runner) GetInstanceMetadata(ctx context.Context) (params.InstanceMetada
if dbEntity.AgentMode {
agentTools, err := r.GetGARMTools(ctx, 0, 25)
if err != nil {
return params.InstanceMetadata{}, fmt.Errorf("failed to find garm agent tools: %w", err)
if !errors.Is(err, runnerErrors.ErrNotFound) {
return params.InstanceMetadata{}, fmt.Errorf("failed to find garm agent tools: %w", err)
}
slog.ErrorContext(ctx, "failed to find agent tools", "error", err)
}
if agentTools.TotalCount == 0 {
return params.InstanceMetadata{}, runnerErrors.NewConflictError("agent mode is enabled, but agent tools not available")
if agentTools.TotalCount > 0 {
ret.AgentTools = &agentTools.Results[0]
agentToken, err := r.GetAgentJWTToken(r.ctx, instance.Name)
if err != nil {
return params.InstanceMetadata{}, fmt.Errorf("failed to get agent token: %w", err)
}
ret.AgentToken = agentToken
} else {
slog.WarnContext(ctx, "agent mode enabled but no tools found", "runner_name", instance.Name, "pool_id", instance.PoolID)
ret.AgentMode = false
}
ret.AgentTools = &agentTools.Results[0]
agentToken, err := r.GetAgentJWTToken(r.ctx, instance.Name)
if err != nil {
return params.InstanceMetadata{}, fmt.Errorf("failed to get agent token: %w", err)
}
ret.AgentToken = agentToken
}
if len(dbEntity.Credentials.Endpoint.CACertBundle) > 0 {
@ -273,12 +284,6 @@ func (r *Runner) GetInstanceMetadata(ctx context.Context) (params.InstanceMetada
}
ret.RunnerTools = filtered
switch dbEntity.Credentials.ForgeType {
case params.GiteaEndpointType:
case params.GithubEndpointType:
default:
return params.InstanceMetadata{}, fmt.Errorf("invalid forge type: %s", dbEntity.Credentials.ForgeType)
}
return ret, nil
}

View file

@ -15,8 +15,10 @@
package runner
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
@ -28,6 +30,7 @@ import (
runnerErrors "github.com/cloudbase/garm-provider-common/errors"
commonParams "github.com/cloudbase/garm-provider-common/params"
"github.com/cloudbase/garm/auth"
"github.com/cloudbase/garm/cache"
"github.com/cloudbase/garm/database"
dbCommon "github.com/cloudbase/garm/database/common"
garmTesting "github.com/cloudbase/garm/internal/testing"
@ -37,6 +40,17 @@ import (
runnerMocks "github.com/cloudbase/garm/runner/mocks"
)
// mockTokenGetter is a simple mock implementation of auth.InstanceTokenGetter
type mockTokenGetter struct{}
func (m *mockTokenGetter) NewInstanceJWTToken(instance params.Instance, entity params.ForgeEntity, ttlMinutes uint) (string, error) {
return "mock-instance-jwt-token", nil
}
func (m *mockTokenGetter) NewAgentJWTToken(instance params.Instance, entity params.ForgeEntity) (string, error) {
return "mock-agent-jwt-token", nil
}
type MetadataTestFixtures struct {
AdminContext context.Context
Store dbCommon.Store
@ -149,12 +163,13 @@ func (s *MetadataTestSuite) SetupTest() {
s.Fixtures = fixtures
// setup test runner
// setup test runner with mock token getter
runner := &Runner{
providers: fixtures.Providers,
ctx: fixtures.AdminContext,
store: fixtures.Store,
poolManagerCtrl: fixtures.PoolMgrCtrlMock,
tokenGetter: &mockTokenGetter{},
}
s.Runner = runner
@ -285,9 +300,24 @@ func (s *MetadataTestSuite) TestGetLabelsForInstance() {
}
func (s *MetadataTestSuite) TestGetRunnerInstallScript() {
// This test requires complex cache setup for github tools
// Skipping for now as it would require significant test infrastructure
s.T().Skip("Skipping install script test - requires github tools cache setup")
// Set up github tools cache for the entity
tools := []commonParams.RunnerApplicationDownload{
{
OS: garmTesting.Ptr("linux"),
Architecture: garmTesting.Ptr("x64"),
DownloadURL: garmTesting.Ptr("https://example.com/actions-runner-linux-x64-2.0.0.tar.gz"),
Filename: garmTesting.Ptr("actions-runner-linux-x64-2.0.0.tar.gz"),
SHA256Checksum: garmTesting.Ptr("abc123"),
},
}
cache.SetGithubToolsCache(s.Fixtures.TestEntity, tools)
script, err := s.Runner.GetRunnerInstallScript(s.instanceCtx)
s.Require().Nil(err)
s.Require().NotEmpty(script)
// Should contain the template content
s.Require().Contains(string(script), "Installing runner")
}
func (s *MetadataTestSuite) TestGetRunnerInstallScriptUnauthorized() {
@ -298,12 +328,107 @@ func (s *MetadataTestSuite) TestGetRunnerInstallScriptUnauthorized() {
}
func (s *MetadataTestSuite) TestGetRunnerInstallScriptInvalidState() {
// Set up cache even for invalid state to ensure it's the state check that fails
tools := []commonParams.RunnerApplicationDownload{
{
OS: garmTesting.Ptr("linux"),
Architecture: garmTesting.Ptr("x64"),
DownloadURL: garmTesting.Ptr("https://example.com/actions-runner.tar.gz"),
Filename: garmTesting.Ptr("actions-runner.tar.gz"),
SHA256Checksum: garmTesting.Ptr("abc123"),
},
}
cache.SetGithubToolsCache(s.Fixtures.TestEntity, tools)
_, err := s.Runner.GetRunnerInstallScript(s.invalidInstanceCtx)
s.Require().NotNil(err)
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
}
func (s *MetadataTestSuite) TestGetRunnerInstallScriptNoToolsInCache() {
// Don't set up cache - should fail with tools not found error
_, err := s.Runner.GetRunnerInstallScript(s.instanceCtx)
s.Require().NotNil(err)
s.Require().Contains(err.Error(), "failed to get tools")
}
func (s *MetadataTestSuite) TestGetRunnerInstallScriptWithExtraSpecs() {
// Set up github tools cache
tools := []commonParams.RunnerApplicationDownload{
{
OS: garmTesting.Ptr("linux"),
Architecture: garmTesting.Ptr("x64"),
DownloadURL: garmTesting.Ptr("https://example.com/actions-runner.tar.gz"),
Filename: garmTesting.Ptr("actions-runner.tar.gz"),
SHA256Checksum: garmTesting.Ptr("abc123"),
},
}
cache.SetGithubToolsCache(s.Fixtures.TestEntity, tools)
// Update pool with extra specs containing custom context
extraSpecs := json.RawMessage(`{"extra_context": {"custom_var": "custom_value"}}`)
pool, err := s.Fixtures.Store.UpdateEntityPool(s.adminCtx, s.Fixtures.TestEntity, s.Fixtures.TestPool.ID, params.UpdatePoolParams{
ExtraSpecs: extraSpecs,
})
s.Require().NoError(err)
s.Require().NotNil(pool)
script, err := s.Runner.GetRunnerInstallScript(s.instanceCtx)
s.Require().Nil(err)
s.Require().NotEmpty(script)
}
func (s *MetadataTestSuite) TestGetRunnerInstallScriptNoTemplate() {
// Set up github tools cache
tools := []commonParams.RunnerApplicationDownload{
{
OS: garmTesting.Ptr("linux"),
Architecture: garmTesting.Ptr("x64"),
DownloadURL: garmTesting.Ptr("https://example.com/actions-runner.tar.gz"),
Filename: garmTesting.Ptr("actions-runner.tar.gz"),
SHA256Checksum: garmTesting.Ptr("abc123"),
},
}
cache.SetGithubToolsCache(s.Fixtures.TestEntity, tools)
// Create a new pool without a template
poolNoTemplate, err := s.Fixtures.Store.CreateEntityPool(s.adminCtx, s.Fixtures.TestEntity, params.CreatePoolParams{
ProviderName: "test-provider",
MaxRunners: 2,
MinIdleRunners: 1,
Image: "ubuntu:22.04",
Flavor: "medium",
OSType: commonParams.Linux,
OSArch: commonParams.Amd64,
Tags: []string{"linux", "amd64"},
RunnerBootstrapTimeout: 10,
// No TemplateID specified
})
s.Require().NoError(err)
// Create instance with this pool
instance, err := s.Fixtures.Store.CreateInstance(s.adminCtx, poolNoTemplate.ID, params.CreateInstanceParams{
Name: "test-instance-no-template",
OSType: commonParams.Linux,
OSArch: commonParams.Amd64,
})
s.Require().NoError(err)
// Create context for this instance
ctx := auth.SetInstanceParams(context.Background(), instance)
ctx = auth.SetInstanceRunnerStatus(ctx, params.RunnerPending)
ctx = auth.SetInstanceEntity(ctx, s.Fixtures.TestEntity)
ctx = auth.SetInstanceAuthToken(ctx, "test-auth-token")
_, err = s.Runner.GetRunnerInstallScript(ctx)
s.Require().NotNil(err)
s.Require().Contains(err.Error(), "no template associated")
}
func (s *MetadataTestSuite) TestGenerateSystemdUnitFile() {
tests := []struct {
name string
@ -492,6 +617,636 @@ func (s *MetadataTestSuite) TestGetRootCertificateBundleInvalidBundle() {
s.Require().Empty(bundle.RootCertificates)
}
func (s *MetadataTestSuite) TestGetInstanceMetadata() {
// Set up github tools cache for the entity
tools := []commonParams.RunnerApplicationDownload{
{
OS: garmTesting.Ptr("linux"),
Architecture: garmTesting.Ptr("x64"),
DownloadURL: garmTesting.Ptr("https://example.com/actions-runner-linux-x64-2.0.0.tar.gz"),
Filename: garmTesting.Ptr("actions-runner-linux-x64-2.0.0.tar.gz"),
SHA256Checksum: garmTesting.Ptr("abc123"),
},
}
cache.SetGithubToolsCache(s.Fixtures.TestEntity, tools)
metadata, err := s.Runner.GetInstanceMetadata(s.instanceCtx)
s.Require().Nil(err)
s.Require().NotEmpty(metadata.RunnerName)
s.Require().Equal(s.Fixtures.TestInstance.Name, metadata.RunnerName)
s.Require().Equal(params.GithubEndpointType, metadata.ForgeType)
s.Require().False(metadata.JITEnabled)
s.Require().False(metadata.AgentMode)
s.Require().NotNil(metadata.RunnerTools)
// Metadata access details are populated from instance
s.Require().NotNil(metadata.MetadataAccess)
}
func (s *MetadataTestSuite) TestGetInstanceMetadataUnauthorized() {
_, err := s.Runner.GetInstanceMetadata(s.unauthorizedCtx)
s.Require().NotNil(err)
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
}
func (s *MetadataTestSuite) TestGetInstanceMetadataInvalidState() {
_, err := s.Runner.GetInstanceMetadata(s.invalidInstanceCtx)
s.Require().NotNil(err)
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
}
func (s *MetadataTestSuite) TestGetInstanceMetadataNoPoolOrScaleSet() {
// Create instance without pool or scale set
instanceNoPool := s.Fixtures.TestInstance
instanceNoPool.PoolID = ""
instanceNoPool.ScaleSetID = 0
ctx := auth.SetInstanceParams(context.Background(), instanceNoPool)
ctx = auth.SetInstanceRunnerStatus(ctx, params.RunnerPending)
_, err := s.Runner.GetInstanceMetadata(ctx)
s.Require().NotNil(err)
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
}
func (s *MetadataTestSuite) TestGetInstanceMetadataWithJIT() {
// Set up github tools cache
tools := []commonParams.RunnerApplicationDownload{
{
OS: garmTesting.Ptr("linux"),
Architecture: garmTesting.Ptr("x64"),
DownloadURL: garmTesting.Ptr("https://example.com/runner.tar.gz"),
Filename: garmTesting.Ptr("runner.tar.gz"),
SHA256Checksum: garmTesting.Ptr("abc123"),
},
}
cache.SetGithubToolsCache(s.Fixtures.TestEntity, tools)
metadata, err := s.Runner.GetInstanceMetadata(s.jitInstanceCtx)
s.Require().Nil(err)
s.Require().True(metadata.JITEnabled)
s.Require().NotNil(metadata.RunnerTools)
}
func (s *MetadataTestSuite) TestGetInstanceMetadataWithExtraSpecs() {
// Set up github tools cache
tools := []commonParams.RunnerApplicationDownload{
{
OS: garmTesting.Ptr("linux"),
Architecture: garmTesting.Ptr("x64"),
DownloadURL: garmTesting.Ptr("https://example.com/runner.tar.gz"),
Filename: garmTesting.Ptr("runner.tar.gz"),
SHA256Checksum: garmTesting.Ptr("abc123"),
},
}
cache.SetGithubToolsCache(s.Fixtures.TestEntity, tools)
// Update pool with extra specs
extraSpecs := json.RawMessage(`{"custom_key": "custom_value", "another_key": 123}`)
_, err := s.Fixtures.Store.UpdateEntityPool(s.adminCtx, s.Fixtures.TestEntity, s.Fixtures.TestPool.ID, params.UpdatePoolParams{
ExtraSpecs: extraSpecs,
})
s.Require().NoError(err)
metadata, err := s.Runner.GetInstanceMetadata(s.instanceCtx)
s.Require().Nil(err)
s.Require().NotNil(metadata.ExtraSpecs)
s.Require().Contains(metadata.ExtraSpecs, "custom_key")
s.Require().Equal("custom_value", metadata.ExtraSpecs["custom_key"])
}
func (s *MetadataTestSuite) TestGetInstanceMetadataBasicFields() {
// Test that all basic metadata fields are populated correctly
tools := []commonParams.RunnerApplicationDownload{
{
OS: garmTesting.Ptr("linux"),
Architecture: garmTesting.Ptr("x64"),
DownloadURL: garmTesting.Ptr("https://example.com/runner.tar.gz"),
Filename: garmTesting.Ptr("runner.tar.gz"),
SHA256Checksum: garmTesting.Ptr("abc123"),
},
}
cache.SetGithubToolsCache(s.Fixtures.TestEntity, tools)
metadata, err := s.Runner.GetInstanceMetadata(s.instanceCtx)
s.Require().Nil(err)
s.Require().Equal(s.Fixtures.TestInstance.Name, metadata.RunnerName)
s.Require().NotEmpty(metadata.RunnerRegistrationURL)
s.Require().False(metadata.AgentShellEnabled)
}
func (s *MetadataTestSuite) TestGetInstanceMetadataWithAgentModeNoTools() {
// Set up runner tools cache
tools := []commonParams.RunnerApplicationDownload{
{
OS: garmTesting.Ptr("linux"),
Architecture: garmTesting.Ptr("x64"),
DownloadURL: garmTesting.Ptr("https://example.com/runner.tar.gz"),
Filename: garmTesting.Ptr("runner.tar.gz"),
SHA256Checksum: garmTesting.Ptr("abc123"),
},
}
cache.SetGithubToolsCache(s.Fixtures.TestEntity, tools)
// Enable agent mode on the organization
agentMode := true
_, err := s.Fixtures.Store.UpdateOrganization(s.adminCtx, s.Fixtures.TestEntity.ID, params.UpdateEntityParams{
AgentMode: &agentMode,
})
s.Require().NoError(err)
metadata, err := s.Runner.GetInstanceMetadata(s.instanceCtx)
s.Require().Nil(err)
// AgentMode should be disabled because no agent tools are available
s.Require().False(metadata.AgentMode, "AgentMode should be false when no agent tools available")
s.Require().Nil(metadata.AgentTools)
s.Require().Empty(metadata.AgentToken)
}
func (s *MetadataTestSuite) TestGetInstanceMetadataAgentModeDisabledByDefault() {
// Test that agent mode is disabled by default
tools := []commonParams.RunnerApplicationDownload{
{
OS: garmTesting.Ptr("linux"),
Architecture: garmTesting.Ptr("x64"),
DownloadURL: garmTesting.Ptr("https://example.com/runner.tar.gz"),
Filename: garmTesting.Ptr("runner.tar.gz"),
SHA256Checksum: garmTesting.Ptr("abc123"),
},
}
cache.SetGithubToolsCache(s.Fixtures.TestEntity, tools)
metadata, err := s.Runner.GetInstanceMetadata(s.instanceCtx)
s.Require().Nil(err)
s.Require().False(metadata.AgentMode)
s.Require().Nil(metadata.AgentTools)
s.Require().Empty(metadata.AgentToken)
}
func (s *MetadataTestSuite) TestGetInstanceMetadataWithAgentModeToolsCountZero() {
// Set up runner tools cache
tools := []commonParams.RunnerApplicationDownload{
{
OS: garmTesting.Ptr("linux"),
Architecture: garmTesting.Ptr("x64"),
DownloadURL: garmTesting.Ptr("https://example.com/runner.tar.gz"),
Filename: garmTesting.Ptr("runner.tar.gz"),
SHA256Checksum: garmTesting.Ptr("abc123"),
},
}
cache.SetGithubToolsCache(s.Fixtures.TestEntity, tools)
// Enable agent mode on the organization
agentMode := true
_, err := s.Fixtures.Store.UpdateOrganization(s.adminCtx, s.Fixtures.TestEntity.ID, params.UpdateEntityParams{
AgentMode: &agentMode,
})
s.Require().NoError(err)
// GetGARMTools will search for files with category=garm-agent tag
// Since no such files exist, it returns TotalCount=0
metadata, err := s.Runner.GetInstanceMetadata(s.instanceCtx)
s.Require().Nil(err)
// AgentMode should be disabled because TotalCount is 0
s.Require().False(metadata.AgentMode, "AgentMode should be false when GetGARMTools returns TotalCount=0")
s.Require().Nil(metadata.AgentTools)
s.Require().Empty(metadata.AgentToken)
}
func (s *MetadataTestSuite) TestGetInstanceMetadataWithAgentModeGetToolsReturnsNotFoundError() {
// Set up runner tools cache
tools := []commonParams.RunnerApplicationDownload{
{
OS: garmTesting.Ptr("linux"),
Architecture: garmTesting.Ptr("x64"),
DownloadURL: garmTesting.Ptr("https://example.com/runner.tar.gz"),
Filename: garmTesting.Ptr("runner.tar.gz"),
SHA256Checksum: garmTesting.Ptr("abc123"),
},
}
cache.SetGithubToolsCache(s.Fixtures.TestEntity, tools)
// Enable agent mode on the organization
agentMode := true
_, err := s.Fixtures.Store.UpdateOrganization(s.adminCtx, s.Fixtures.TestEntity.ID, params.UpdateEntityParams{
AgentMode: &agentMode,
})
s.Require().NoError(err)
// When GetGARMTools returns ErrNotFound (which happens when TotalCount=0 and no files found),
// it should log the error but continue and disable AgentMode
metadata, err := s.Runner.GetInstanceMetadata(s.instanceCtx)
s.Require().Nil(err)
// Should continue execution and disable AgentMode
s.Require().False(metadata.AgentMode)
s.Require().Nil(metadata.AgentTools)
s.Require().Empty(metadata.AgentToken)
}
func (s *MetadataTestSuite) TestGetInstanceMetadataWithAgentModeAndToolsAvailable() {
// Set up runner tools cache for GitHub runner
runnerTools := []commonParams.RunnerApplicationDownload{
{
OS: garmTesting.Ptr("linux"),
Architecture: garmTesting.Ptr("x64"),
DownloadURL: garmTesting.Ptr("https://example.com/runner.tar.gz"),
Filename: garmTesting.Ptr("runner.tar.gz"),
SHA256Checksum: garmTesting.Ptr("abc123"),
},
}
cache.SetGithubToolsCache(s.Fixtures.TestEntity, runnerTools)
// Enable agent mode on the organization
agentMode := true
_, err := s.Fixtures.Store.UpdateOrganization(s.adminCtx, s.Fixtures.TestEntity.ID, params.UpdateEntityParams{
AgentMode: &agentMode,
})
s.Require().NoError(err)
// Create GARM agent tools using the CreateGARMTool API
agentBinary := []byte("fake garm agent binary content")
agentToolParam := params.CreateGARMToolParams{
Name: "garm-agent-linux-amd64",
Description: "GARM agent for Linux AMD64",
Size: int64(len(agentBinary)),
OSType: commonParams.Linux,
OSArch: commonParams.Amd64,
Version: "v1.0.0",
}
agentTool, err := s.Runner.CreateGARMTool(s.adminCtx, agentToolParam, bytes.NewReader(agentBinary))
s.Require().NoError(err)
s.Require().NotNil(agentTool)
// Now GetInstanceMetadata should succeed with AgentMode enabled
metadata, err := s.Runner.GetInstanceMetadata(s.instanceCtx)
s.Require().Nil(err)
// AgentMode should remain enabled because tools are available
s.Require().True(metadata.AgentMode, "AgentMode should be true when agent tools are available")
// Agent tools should be populated
s.Require().NotNil(metadata.AgentTools, "AgentTools should be populated")
s.Require().Equal(agentTool.ID, metadata.AgentTools.ID)
s.Require().Equal("garm-agent-linux-amd64", metadata.AgentTools.Name)
s.Require().Equal(commonParams.OSType("linux"), metadata.AgentTools.OSType)
s.Require().Equal(commonParams.OSArch("amd64"), metadata.AgentTools.OSArch)
s.Require().Equal("v1.0.0", metadata.AgentTools.Version)
s.Require().NotEmpty(metadata.AgentTools.DownloadURL)
// Agent token should be generated
s.Require().NotEmpty(metadata.AgentToken, "AgentToken should be generated when tools available")
}
func (s *MetadataTestSuite) TestFileObjectToGARMTool() {
tests := []struct {
name string
fileObject params.FileObject
downloadURL string
wantErr bool
errMsg string
}{
{
name: "Valid file object with all tags",
fileObject: params.FileObject{
ID: 1,
Name: "garm-agent-linux-amd64",
Size: 1024,
SHA256: "abc123",
Description: "GARM agent for Linux AMD64",
FileType: "binary",
Tags: []string{"version=1.0.0", "os_type=linux", "os_arch=amd64"},
},
downloadURL: "http://example.com/download",
wantErr: false,
},
{
name: "Missing version tag",
fileObject: params.FileObject{
ID: 2,
Name: "garm-agent",
Tags: []string{"os_type=linux", "os_arch=amd64"},
},
downloadURL: "http://example.com/download",
wantErr: true,
errMsg: "missing version",
},
{
name: "Missing os_type tag",
fileObject: params.FileObject{
ID: 3,
Name: "garm-agent",
Tags: []string{"version=1.0.0", "os_arch=amd64"},
},
downloadURL: "http://example.com/download",
wantErr: true,
errMsg: "missing os_type",
},
{
name: "Missing os_arch tag",
fileObject: params.FileObject{
ID: 4,
Name: "garm-agent",
Tags: []string{"version=1.0.0", "os_type=linux"},
},
downloadURL: "http://example.com/download",
wantErr: true,
errMsg: "missing os_arch",
},
}
for _, tt := range tests {
s.Run(tt.name, func() {
result, err := fileObjectToGARMTool(tt.fileObject, tt.downloadURL)
if tt.wantErr {
s.Require().NotNil(err)
s.Require().Contains(err.Error(), tt.errMsg)
} else {
s.Require().Nil(err)
s.Require().Equal(tt.fileObject.ID, result.ID)
s.Require().Equal(tt.fileObject.Name, result.Name)
s.Require().Equal(tt.fileObject.Size, result.Size)
s.Require().Equal(tt.fileObject.SHA256, result.SHA256SUM)
s.Require().Equal(tt.downloadURL, result.DownloadURL)
}
})
}
}
func (s *MetadataTestSuite) TestGetGARMTools() {
// GetGARMTools requires file objects in database
// This is tested in file_store_test.go and garm_tools_test.go
// Here we just test the authorization paths
_, err := s.Runner.GetGARMTools(s.instanceCtx, 0, 25)
// Should not error on authorization (might have no results)
s.Require().NoError(err)
}
func (s *MetadataTestSuite) TestGetGARMToolsUnauthorized() {
_, err := s.Runner.GetGARMTools(s.unauthorizedCtx, 0, 25)
s.Require().NotNil(err)
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
}
func (s *MetadataTestSuite) TestGetGARMToolsInvalidState() {
_, err := s.Runner.GetGARMTools(s.invalidInstanceCtx, 0, 25)
s.Require().NotNil(err)
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
}
func (s *MetadataTestSuite) TestShowGARMToolsUnauthorized() {
_, err := s.Runner.ShowGARMTools(s.unauthorizedCtx, 1)
s.Require().NotNil(err)
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
}
func (s *MetadataTestSuite) TestShowGARMToolsInvalidState() {
_, err := s.Runner.ShowGARMTools(s.invalidInstanceCtx, 1)
s.Require().NotNil(err)
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
}
func (s *MetadataTestSuite) TestGetGARMToolsReadHandlerUnauthorized() {
_, err := s.Runner.GetGARMToolsReadHandler(s.unauthorizedCtx, 1)
s.Require().NotNil(err)
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
}
func (s *MetadataTestSuite) TestValidateInstanceState() {
tests := []struct {
name string
ctx context.Context
wantErr bool
}{
{
name: "Valid pending instance",
ctx: s.instanceCtx,
wantErr: false,
},
{
name: "Valid installing instance",
ctx: func() context.Context {
ctx := auth.SetInstanceParams(context.Background(), s.Fixtures.TestInstance)
ctx = auth.SetInstanceRunnerStatus(ctx, params.RunnerInstalling)
return ctx
}(),
wantErr: false,
},
{
name: "Invalid state - active",
ctx: s.invalidInstanceCtx,
wantErr: true,
},
{
name: "Unauthorized - no instance",
ctx: s.unauthorizedCtx,
wantErr: true,
},
}
for _, tt := range tests {
s.Run(tt.name, func() {
instance, err := validateInstanceState(tt.ctx)
if tt.wantErr {
s.Require().NotNil(err)
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
} else {
s.Require().Nil(err)
s.Require().NotEmpty(instance.Name)
}
})
}
}
func (s *MetadataTestSuite) TestGetJITConfigFileInvalidState() {
ctx := auth.SetInstanceParams(context.Background(), s.Fixtures.TestInstance)
ctx = auth.SetInstanceRunnerStatus(ctx, params.RunnerActive)
jitInstance := s.Fixtures.TestInstance
jitInstance.JitConfiguration = map[string]string{
".runner": base64.StdEncoding.EncodeToString([]byte("runner config")),
}
ctx = auth.SetInstanceParams(ctx, jitInstance)
ctx = auth.SetInstanceHasJITConfig(ctx, jitInstance.JitConfiguration)
_, err := s.Runner.GetJITConfigFile(ctx, ".runner")
s.Require().NotNil(err)
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
}
func (s *MetadataTestSuite) TestGetJITConfigFileInvalidBase64() {
jitInstance := s.Fixtures.TestInstance
jitInstance.JitConfiguration = map[string]string{
".runner": "invalid-base64!!!",
}
ctx := auth.SetInstanceParams(context.Background(), jitInstance)
ctx = auth.SetInstanceRunnerStatus(ctx, params.RunnerPending)
ctx = auth.SetInstanceHasJITConfig(ctx, jitInstance.JitConfiguration)
_, err := s.Runner.GetJITConfigFile(ctx, ".runner")
s.Require().NotNil(err)
s.Require().Contains(err.Error(), "error decoding file contents")
}
func (s *MetadataTestSuite) TestGetJITConfigFileMultipleFiles() {
jitInstance := s.Fixtures.TestInstance
jitInstance.JitConfiguration = map[string]string{
".runner": base64.StdEncoding.EncodeToString([]byte("runner config")),
".credentials": base64.StdEncoding.EncodeToString([]byte("credentials config")),
".env": base64.StdEncoding.EncodeToString([]byte("env config")),
}
ctx := auth.SetInstanceParams(context.Background(), jitInstance)
ctx = auth.SetInstanceRunnerStatus(ctx, params.RunnerPending)
ctx = auth.SetInstanceHasJITConfig(ctx, jitInstance.JitConfiguration)
// Test each file can be retrieved
runnerContent, err := s.Runner.GetJITConfigFile(ctx, ".runner")
s.Require().Nil(err)
s.Require().Equal("runner config", string(runnerContent))
credContent, err := s.Runner.GetJITConfigFile(ctx, ".credentials")
s.Require().Nil(err)
s.Require().Equal("credentials config", string(credContent))
envContent, err := s.Runner.GetJITConfigFile(ctx, ".env")
s.Require().Nil(err)
s.Require().Equal("env config", string(envContent))
}
func (s *MetadataTestSuite) TestGenerateSystemdUnitFileGiteaWithDefaultUser() {
entity := s.Fixtures.TestEntity
entity.Credentials.ForgeType = params.GiteaEndpointType
ctx := auth.SetInstanceEntity(context.Background(), entity)
unitFile, err := s.Runner.GenerateSystemdUnitFile(ctx, "")
s.Require().Nil(err)
s.Require().NotEmpty(unitFile)
s.Require().Contains(string(unitFile), "Act Runner")
s.Require().Contains(string(unitFile), "act_runner daemon --once")
s.Require().Contains(string(unitFile), "Restart=always")
}
func (s *MetadataTestSuite) TestGetLabelsForInstanceWithCache() {
// This test would require setting up the cache properly
// For now, we test that it doesn't panic with empty cache
instance := s.Fixtures.TestInstance
labels := getLabelsForInstance(instance)
s.Require().NotNil(labels)
}
func (s *MetadataTestSuite) TestGetLabelsForInstanceWithScaleSetAndJIT() {
// Test instance with both scale set and JIT config
// JIT should take precedence
instance := s.Fixtures.TestInstance
instance.ScaleSetID = 123
instance.JitConfiguration = map[string]string{"test": "config"}
labels := getLabelsForInstance(instance)
s.Require().Empty(labels)
}
func (s *MetadataTestSuite) TestGetServiceNameForEntityAllTypes() {
tests := []struct {
name string
entityType params.ForgeEntityType
owner string
repoName string
expected string
wantErr bool
}{
{
name: "Enterprise",
entityType: params.ForgeEntityTypeEnterprise,
owner: "my-enterprise",
expected: "actions.runner.my-enterprise",
wantErr: false,
},
{
name: "Organization",
entityType: params.ForgeEntityTypeOrganization,
owner: "my-org",
expected: "actions.runner.my-org",
wantErr: false,
},
{
name: "Repository",
entityType: params.ForgeEntityTypeRepository,
owner: "my-owner",
repoName: "my-repo",
expected: "actions.runner.my-owner.my-repo",
wantErr: false,
},
{
name: "Invalid type",
entityType: "invalid-type",
owner: "test",
wantErr: true,
},
}
for _, tt := range tests {
s.Run(tt.name, func() {
entity := params.ForgeEntity{
EntityType: tt.entityType,
Owner: tt.owner,
Name: tt.repoName,
}
serviceName, err := s.Runner.getServiceNameForEntity(entity)
if tt.wantErr {
s.Require().Error(err)
} else {
s.Require().NoError(err)
s.Require().Equal(tt.expected, serviceName)
}
})
}
}
func (s *MetadataTestSuite) TestFileObjectToGARMToolWithOptionalFields() {
fileObject := params.FileObject{
ID: 10,
Name: "test-agent",
Size: 2048,
SHA256: "def456",
Description: "Test description",
FileType: "executable",
Tags: []string{
"version=2.0.0",
"os_type=windows",
"os_arch=arm64",
"extra_tag=value",
},
}
result, err := fileObjectToGARMTool(fileObject, "http://test.com/dl")
s.Require().NoError(err)
s.Require().Equal(uint(10), result.ID)
s.Require().Equal("test-agent", result.Name)
s.Require().Equal(int64(2048), result.Size)
s.Require().Equal("def456", result.SHA256SUM)
s.Require().Equal("Test description", result.Description)
s.Require().Equal("executable", result.FileType)
s.Require().Equal("2.0.0", result.Version)
s.Require().Equal(commonParams.OSType("windows"), result.OSType)
s.Require().Equal(commonParams.OSArch("arm64"), result.OSArch)
s.Require().Equal("http://test.com/dl", result.DownloadURL)
}
func TestMetadataTestSuite(t *testing.T) {
suite.Run(t, new(MetadataTestSuite))
}

View file

@ -30,18 +30,19 @@ import (
"github.com/cloudbase/garm/util/github"
)
func NewWorker(ctx context.Context, store common.Store) *Worker {
func NewWorker(ctx context.Context, store common.Store, toolsManager params.GARMToolsManager) *Worker {
consumerID := "cache"
ctx = garmUtil.WithSlogContext(
ctx,
slog.Any("worker", consumerID))
return &Worker{
ctx: ctx,
store: store,
consumerID: consumerID,
toolsWorkes: make(map[string]*toolsUpdater),
quit: make(chan struct{}),
ctx: ctx,
store: store,
garmToolsManager: toolsManager,
consumerID: consumerID,
toolsWorkes: make(map[string]*toolsUpdater),
quit: make(chan struct{}),
}
}
@ -49,9 +50,10 @@ type Worker struct {
ctx context.Context
consumerID string
consumer common.Consumer
store common.Store
toolsWorkes map[string]*toolsUpdater
consumer common.Consumer
store common.Store
garmToolsManager params.GARMToolsManager
toolsWorkes map[string]*toolsUpdater
mux sync.Mutex
running bool

View file

@ -14,6 +14,12 @@
package cache
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
"time"
)
@ -23,6 +29,7 @@ type GitHubReleaseAsset struct {
Size uint `json:"size"`
DownloadCount uint `json:"download_count"`
CreatedAt time.Time `json:"created_at"`
Digest string `json:"digest"`
DownloadURL string `json:"browser_download_url"`
}
@ -34,26 +41,44 @@ type GitHubRelease struct {
Assets []GitHubReleaseAsset `json:"assets"`
}
// func getGithubReleaseFromURL(_ context.Context, releasesEndpoint string) (GitHubRelease, error) {
// resp, err := http.Get(releasesEndpoint) // nolint
// if err != nil {
// return GitHubRelease{}, fmt.Errorf("failed to fetch URL %s: %w", releasesEndpoint, err)
// }
// defer resp.Body.Close()
// data, err := io.ReadAll(resp.Body)
// if err != nil {
// return GitHubRelease{}, fmt.Errorf("failed to read response from URL %s: %w", releasesEndpoint, err)
// }
type GitHubReleases []GitHubRelease
// var tools GitHubRelease
// err = json.Unmarshal(data, &tools)
// if err != nil {
// return GitHubRelease{}, fmt.Errorf("failed to unmarshal response from URL %s: %w", releasesEndpoint, err)
// }
func getLatestGithubReleaseFromURL(_ context.Context, releasesEndpoint string) (GitHubRelease, error) {
resp, err := http.Get(releasesEndpoint)
if err != nil {
return GitHubRelease{}, fmt.Errorf("failed to fetch URL %s: %w", releasesEndpoint, err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return GitHubRelease{}, fmt.Errorf("failed to read response from URL %s: %w", releasesEndpoint, err)
}
// if len(tools.Assets) == 0 {
// return GitHubRelease{}, fmt.Errorf("no tools found from URL %s", releasesEndpoint)
// }
var tools GitHubReleases
err = json.Unmarshal(data, &tools)
if err != nil {
return GitHubRelease{}, fmt.Errorf("failed to unmarshal response from URL %s: %w", releasesEndpoint, err)
}
// return tools, nil
// }
if len(tools) == 0 {
return GitHubRelease{}, fmt.Errorf("no tools found from URL %s", releasesEndpoint)
}
if len(tools[0].Assets) == 0 {
return GitHubRelease{}, fmt.Errorf("no downloadable assets found from URL %s", releasesEndpoint)
}
return tools[0], nil
}
type garmToolsSync struct {
ctx context.Context
mux sync.Mutex
running bool
quit chan struct{}
}
func (g *garmToolsSync) loop() {
}

View file

@ -163,14 +163,6 @@ func (t *toolsUpdater) sleepWithCancel(sleepTime time.Duration) (canceled bool)
func (t *toolsUpdater) giteaUpdateLoop() {
defer t.Stop()
// add some jitter. When spinning up multiple entities, we add
// jitter to prevent stampeeding herd.
randInt, err := rand.Int(rand.Reader, big.NewInt(3000))
if err != nil {
randInt = big.NewInt(0)
}
t.sleepWithCancel(time.Duration(randInt.Int64()) * time.Millisecond)
// add some jitter
timerJitter, err := rand.Int(rand.Reader, big.NewInt(120))
if err != nil {