Moving to scaleset client for the controller (#4390)

This commit is contained in:
Nikola Jokic 2026-03-13 14:36:41 +01:00 committed by GitHub
parent 1d9f626c53
commit f99c6eda0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 2695 additions and 360 deletions

View File

@ -1,3 +1,13 @@
all: false
dir: "{{.InterfaceDir}}"
filename: mocks_test.go
force-file-write: true
formatter: goimports
log-level: info
structname: "{{.Mock}}{{.InterfaceName}}"
pkgname: "{{.SrcPackageName}}"
recursive: false
template: testify
packages:
github.com/actions/actions-runner-controller/github/actions:
config:
@ -8,13 +18,9 @@ packages:
interfaces:
ActionsService:
SessionService:
github.com/actions/actions-runner-controller/cmd/ghalistener/metrics:
config:
inpackage: true
dir: "{{.InterfaceDir}}"
filename: "mocks_test.go"
pkgname: "metrics"
interfaces:
Recorder:
ServerExporter:
all: true
github.com/actions/actions-runner-controller/controllers/actions.github.com:
config:
all: true

View File

@ -111,7 +111,7 @@ type EphemeralRunnerSpec struct {
GitHubServerTLS *TLSConfig `json:"githubServerTLS,omitempty"`
// +required
RunnerScaleSetId int `json:"runnerScaleSetId,omitempty"`
RunnerScaleSetID int `json:"runnerScaleSetId,omitempty"`
// +optional
Proxy *ProxyConfig `json:"proxy,omitempty"`

View File

@ -9,11 +9,11 @@ import (
"net/http"
"net/url"
"os"
"strings"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/actions/actions-runner-controller/build"
"github.com/actions/actions-runner-controller/logger"
"github.com/actions/actions-runner-controller/vault"
"github.com/actions/actions-runner-controller/vault/azurekeyvault"
"github.com/actions/scaleset"
@ -23,7 +23,7 @@ import (
const appName = "ghalistener"
type Config struct {
ConfigureUrl string `json:"configure_url"`
ConfigureURL string `json:"configure_url"`
VaultType vault.VaultType `json:"vault_type"`
VaultLookupKey string `json:"vault_lookup_key"`
// If the VaultType is set to "azure_key_vault", this field must be populated.
@ -102,7 +102,7 @@ func Read(ctx context.Context, configPath string) (*Config, error) {
// Validate checks the configuration for errors.
func (c *Config) Validate() error {
if len(c.ConfigureUrl) == 0 {
if len(c.ConfigureURL) == 0 {
return fmt.Errorf("GitHubConfigUrl is not provided")
}
@ -137,37 +137,7 @@ func (c *Config) Validate() error {
}
func (c *Config) Logger() (*slog.Logger, error) {
var lvl slog.Level
switch strings.ToLower(c.LogLevel) {
case "debug":
lvl = slog.LevelDebug
case "info":
lvl = slog.LevelInfo
case "warn":
lvl = slog.LevelWarn
case "error":
lvl = slog.LevelError
default:
return nil, fmt.Errorf("invalid log level: %s", c.LogLevel)
}
var logger *slog.Logger
switch c.LogFormat {
case "json":
logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
Level: lvl,
}))
case "text":
logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
Level: lvl,
}))
default:
return nil, fmt.Errorf("invalid log format: %s", c.LogFormat)
}
return logger.With("app", appName), nil
return logger.New(c.LogLevel, c.LogFormat)
}
func (c *Config) ActionsClient(logger *slog.Logger, clientOptions ...scaleset.HTTPOption) (*scaleset.Client, error) {
@ -207,7 +177,7 @@ func (c *Config) ActionsClient(logger *slog.Logger, clientOptions ...scaleset.HT
case "":
c, err := scaleset.NewClientWithGitHubApp(
scaleset.ClientWithGitHubAppConfig{
GitHubConfigURL: c.ConfigureUrl,
GitHubConfigURL: c.ConfigureURL,
GitHubAppAuth: scaleset.GitHubAppAuth{
ClientID: c.AppConfig.AppID,
InstallationID: c.AppConfig.AppInstallationID,
@ -224,7 +194,7 @@ func (c *Config) ActionsClient(logger *slog.Logger, clientOptions ...scaleset.HT
default:
c, err := scaleset.NewClientWithPersonalAccessToken(
scaleset.NewClientWithPersonalAccessTokenConfig{
GitHubConfigURL: c.ConfigureUrl,
GitHubConfigURL: c.ConfigureURL,
PersonalAccessToken: c.Token,
SystemInfo: systemInfo,
},

View File

@ -54,7 +54,7 @@ func TestCustomerServerRootCA(t *testing.T) {
certsString = certsString + string(intermediate)
config := config.Config{
ConfigureUrl: server.ConfigURLForOrg("myorg"),
ConfigureURL: server.ConfigURLForOrg("myorg"),
ServerRootCA: certsString,
AppConfig: &appconfig.AppConfig{
Token: "token",
@ -85,7 +85,7 @@ func TestProxySettings(t *testing.T) {
defer os.Setenv("http_proxy", prevProxy)
config := config.Config{
ConfigureUrl: "https://github.com/org/repo",
ConfigureURL: "https://github.com/org/repo",
AppConfig: &appconfig.AppConfig{
Token: "token",
},
@ -103,7 +103,7 @@ func TestProxySettings(t *testing.T) {
defer os.Setenv("https_proxy", prevProxy)
config := config.Config{
ConfigureUrl: "https://github.com/org/repo",
ConfigureURL: "https://github.com/org/repo",
AppConfig: &appconfig.AppConfig{
Token: "token",
},
@ -124,7 +124,7 @@ func TestProxySettings(t *testing.T) {
defer os.Setenv("no_proxy", prevNoProxy)
config := config.Config{
ConfigureUrl: "https://github.com/org/repo",
ConfigureURL: "https://github.com/org/repo",
AppConfig: &appconfig.AppConfig{
Token: "token",
},

View File

@ -10,7 +10,7 @@ import (
func TestConfigValidationMinMax(t *testing.T) {
config := &Config{
ConfigureUrl: "github.com/some_org/some_repo",
ConfigureURL: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetID: 1,
@ -26,7 +26,7 @@ func TestConfigValidationMinMax(t *testing.T) {
func TestConfigValidationMissingToken(t *testing.T) {
config := &Config{
ConfigureUrl: "github.com/some_org/some_repo",
ConfigureURL: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetID: 1,
@ -46,7 +46,7 @@ func TestConfigValidationAppKey(t *testing.T) {
AppID: "1",
AppInstallationID: 10,
},
ConfigureUrl: "github.com/some_org/some_repo",
ConfigureURL: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetID: 1,
@ -63,7 +63,7 @@ func TestConfigValidationAppKey(t *testing.T) {
AppID: "Iv23f8doAlphaNumer1c",
AppInstallationID: 10,
},
ConfigureUrl: "github.com/some_org/some_repo",
ConfigureURL: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetID: 1,
@ -82,7 +82,7 @@ func TestConfigValidationOnlyOneTypeOfCredentials(t *testing.T) {
AppPrivateKey: "asdf",
Token: "asdf",
},
ConfigureUrl: "github.com/some_org/some_repo",
ConfigureURL: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetID: 1,
@ -94,7 +94,7 @@ func TestConfigValidationOnlyOneTypeOfCredentials(t *testing.T) {
func TestConfigValidation(t *testing.T) {
config := &Config{
ConfigureUrl: "https://github.com/actions",
ConfigureURL: "https://github.com/actions",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetID: 1,
@ -125,7 +125,7 @@ func TestConfigValidationConfigUrl(t *testing.T) {
func TestConfigValidationWithVaultConfig(t *testing.T) {
t.Run("valid", func(t *testing.T) {
config := &Config{
ConfigureUrl: "https://github.com/actions",
ConfigureURL: "https://github.com/actions",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetID: 1,
@ -140,7 +140,7 @@ func TestConfigValidationWithVaultConfig(t *testing.T) {
t.Run("invalid vault type", func(t *testing.T) {
config := &Config{
ConfigureUrl: "https://github.com/actions",
ConfigureURL: "https://github.com/actions",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetID: 1,
@ -155,7 +155,7 @@ func TestConfigValidationWithVaultConfig(t *testing.T) {
t.Run("vault type set without lookup key", func(t *testing.T) {
config := &Config{
ConfigureUrl: "https://github.com/actions",
ConfigureURL: "https://github.com/actions",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetID: 1,

View File

@ -40,7 +40,7 @@ func main() {
}
func run(ctx context.Context, config *config.Config) error {
ghConfig, err := actions.ParseGitHubConfigFromURL(config.ConfigureUrl)
ghConfig, err := actions.ParseGitHubConfigFromURL(config.ConfigureURL)
if err != nil {
return fmt.Errorf("failed to parse GitHub config from URL: %w", err)
}

View File

@ -15,7 +15,8 @@ import (
logf "sigs.k8s.io/controller-runtime/pkg/log"
ghalistenerconfig "github.com/actions/actions-runner-controller/cmd/ghalistener/config"
"github.com/actions/actions-runner-controller/github/actions/fake"
scalefake "github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient/fake"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/secretresolver"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
kerrors "k8s.io/apimachinery/pkg/api/errors"
@ -43,7 +44,10 @@ var _ = Describe("Test AutoScalingListener controller", func() {
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
secretResolver := NewSecretResolver(mgr.GetClient(), fake.NewMultiClient())
secretResolver := secretresolver.New(
mgr.GetClient(),
scalefake.NewMultiClient(),
)
rb := ResourceBuilder{
SecretResolver: secretResolver,
@ -459,7 +463,7 @@ var _ = Describe("Test AutoScalingListener customization", func() {
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
secretResolver := NewSecretResolver(mgr.GetClient(), fake.NewMultiClient())
secretResolver := secretresolver.New(mgr.GetClient(), scalefake.NewMultiClient())
rb := ResourceBuilder{
SecretResolver: secretResolver,
@ -788,7 +792,7 @@ var _ = Describe("Test AutoScalingListener controller with proxy", func() {
ctx = context.Background()
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
secretResolver := NewSecretResolver(mgr.GetClient(), fake.NewMultiClient())
secretResolver := secretresolver.New(mgr.GetClient(), scalefake.NewMultiClient())
rb := ResourceBuilder{
SecretResolver: secretResolver,
@ -991,7 +995,7 @@ var _ = Describe("Test AutoScalingListener controller with template modification
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
secretResolver := NewSecretResolver(mgr.GetClient(), fake.NewMultiClient())
secretResolver := secretresolver.New(mgr.GetClient(), scalefake.NewMultiClient())
rb := ResourceBuilder{
SecretResolver: secretResolver,
@ -1094,7 +1098,7 @@ var _ = Describe("Test GitHub Server TLS configuration", func() {
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
secretResolver := NewSecretResolver(mgr.GetClient(), fake.NewMultiClient())
secretResolver := secretresolver.New(mgr.GetClient(), scalefake.NewMultiClient())
rb := ResourceBuilder{
SecretResolver: secretResolver,

View File

@ -25,7 +25,7 @@ import (
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/build"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/scaleset"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
@ -78,7 +78,6 @@ type AutoscalingRunnerSetReconciler struct {
DefaultRunnerScaleSetListenerImage string
DefaultRunnerScaleSetListenerImagePullSecrets []string
UpdateStrategy UpdateStrategy
ActionsClient actions.MultiClient
ResourceBuilder
}
@ -427,17 +426,16 @@ func (r *AutoscalingRunnerSetReconciler) createRunnerScaleSet(ctx context.Contex
if runnerScaleSet == nil {
runnerScaleSet, err = actionsClient.CreateRunnerScaleSet(
ctx,
&actions.RunnerScaleSet{
&scaleset.RunnerScaleSet{
Name: autoscalingRunnerSet.Spec.RunnerScaleSetName,
RunnerGroupId: runnerGroupID,
Labels: []actions.Label{
RunnerGroupID: runnerGroupID,
Labels: []scaleset.Label{
{
Name: autoscalingRunnerSet.Spec.RunnerScaleSetName,
Type: "System",
},
},
RunnerSetting: actions.RunnerSetting{
Ephemeral: true,
RunnerSetting: scaleset.RunnerSetting{
DisableUpdate: true,
},
})
@ -447,15 +445,11 @@ func (r *AutoscalingRunnerSetReconciler) createRunnerScaleSet(ctx context.Contex
}
}
actionsClient.SetUserAgent(actions.UserAgentInfo{
Version: build.Version,
CommitSHA: build.CommitSHA,
ScaleSetID: runnerScaleSet.Id,
HasProxy: autoscalingRunnerSet.Spec.Proxy != nil,
Subsystem: "controller",
})
info := actionsClient.SystemInfo()
info.ScaleSetID = runnerScaleSet.ID
actionsClient.SetSystemInfo(info)
logger.Info("Created/Reused a runner scale set", "id", runnerScaleSet.Id, "runnerGroupName", runnerScaleSet.RunnerGroupName)
logger.Info("Created/Reused a runner scale set", "id", runnerScaleSet.ID, "runnerGroupName", runnerScaleSet.RunnerGroupName)
if autoscalingRunnerSet.Annotations == nil {
autoscalingRunnerSet.Annotations = map[string]string{}
}
@ -466,7 +460,7 @@ func (r *AutoscalingRunnerSetReconciler) createRunnerScaleSet(ctx context.Contex
logger.Info("Adding runner scale set ID, name and runner group name as an annotation and url labels")
if err = patch(ctx, r.Client, autoscalingRunnerSet, func(obj *v1alpha1.AutoscalingRunnerSet) {
obj.Annotations[AnnotationKeyGitHubRunnerScaleSetName] = runnerScaleSet.Name
obj.Annotations[runnerScaleSetIDAnnotationKey] = strconv.Itoa(runnerScaleSet.Id)
obj.Annotations[runnerScaleSetIDAnnotationKey] = strconv.Itoa(runnerScaleSet.ID)
obj.Annotations[AnnotationKeyGitHubRunnerGroupName] = runnerScaleSet.RunnerGroupName
if err := applyGitHubURLLabels(obj.Spec.GitHubConfigUrl, obj.Labels); err != nil { // should never happen
logger.Error(err, "Failed to apply GitHub URL labels")
@ -477,7 +471,7 @@ func (r *AutoscalingRunnerSetReconciler) createRunnerScaleSet(ctx context.Contex
}
logger.Info("Updated with runner scale set ID, name and runner group name as an annotation",
"id", runnerScaleSet.Id,
"id", runnerScaleSet.ID,
"name", runnerScaleSet.Name,
"runnerGroupName", runnerScaleSet.RunnerGroupName)
return ctrl.Result{}, nil
@ -507,7 +501,7 @@ func (r *AutoscalingRunnerSetReconciler) updateRunnerScaleSetRunnerGroup(ctx con
runnerGroupID = int(runnerGroup.ID)
}
updatedRunnerScaleSet, err := actionsClient.UpdateRunnerScaleSet(ctx, runnerScaleSetID, &actions.RunnerScaleSet{RunnerGroupId: runnerGroupID})
updatedRunnerScaleSet, err := actionsClient.UpdateRunnerScaleSet(ctx, runnerScaleSetID, &scaleset.RunnerScaleSet{RunnerGroupID: runnerGroupID})
if err != nil {
logger.Error(err, "Failed to update runner scale set", "runnerScaleSetId", runnerScaleSetID)
return ctrl.Result{}, err
@ -544,7 +538,7 @@ func (r *AutoscalingRunnerSetReconciler) updateRunnerScaleSetName(ctx context.Co
return ctrl.Result{}, err
}
updatedRunnerScaleSet, err := actionsClient.UpdateRunnerScaleSet(ctx, runnerScaleSetID, &actions.RunnerScaleSet{Name: autoscalingRunnerSet.Spec.RunnerScaleSetName})
updatedRunnerScaleSet, err := actionsClient.UpdateRunnerScaleSet(ctx, runnerScaleSetID, &scaleset.RunnerScaleSet{Name: autoscalingRunnerSet.Spec.RunnerScaleSetName})
if err != nil {
logger.Error(err, "Failed to update runner scale set", "runnerScaleSetId", runnerScaleSetID)
return ctrl.Result{}, err

View File

@ -3,13 +3,12 @@ package actionsgithubcom
import (
"context"
"crypto/tls"
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync"
"time"
corev1 "k8s.io/api/core/v1"
@ -19,7 +18,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"github.com/go-logr/logr"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/errors"
@ -28,9 +26,10 @@ import (
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/build"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/actions-runner-controller/github/actions/fake"
"github.com/actions/actions-runner-controller/github/actions/testserver"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient"
scalefake "github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient/fake"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/secretresolver"
"github.com/actions/scaleset"
)
const (
@ -63,6 +62,10 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() {
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
// Track runner group mappings for dynamic responses
runnerGroupMap := map[int]string{1: "testgroup"} // ID -> Name mapping
runnerGroupMapLock := &sync.RWMutex{} // Thread-safe access
controller = &AutoscalingRunnerSetReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
@ -70,10 +73,30 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() {
ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: k8sClient,
multiClient: fake.NewMultiClient(),
},
SecretResolver: secretresolver.New(mgr.GetClient(), scalefake.NewMultiClient(
scalefake.WithClient(
scalefake.NewClient(
scalefake.WithGetRunnerGroupByNameFunc(func(ctx context.Context, groupName string) (*scaleset.RunnerGroup, error) {
// Support both "testgroup" and "testgroup2"
// Update the mapping when a new group is requested
runnerGroupMapLock.Lock()
runnerGroupMap[1] = groupName
runnerGroupMapLock.Unlock()
return &scaleset.RunnerGroup{ID: 1, Name: groupName}, nil
}),
scalefake.WithGetRunnerScaleSet(nil, nil),
scalefake.WithCreateRunnerScaleSet(&scaleset.RunnerScaleSet{ID: 1, Name: "test-asrs", RunnerGroupID: 1, RunnerGroupName: "testgroup"}, nil),
scalefake.WithUpdateRunnerScaleSetFunc(func(ctx context.Context, scaleSetID int, rs *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error) {
// Return a RunnerScaleSet with the group name corresponding to the runner group ID
runnerGroupMapLock.RLock()
groupName := runnerGroupMap[rs.RunnerGroupID]
runnerGroupMapLock.RUnlock()
return &scaleset.RunnerScaleSet{ID: 1, Name: "test-asrs", RunnerGroupID: rs.RunnerGroupID, RunnerGroupName: groupName}, nil
}),
scalefake.WithDeleteRunnerScaleSet(nil),
),
),
)),
},
}
err := controller.SetupWithManager(mgr)
@ -681,25 +704,34 @@ var _ = Describe("Test AutoScalingController updates", Ordered, func() {
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
multiClient := fake.NewMultiClient(
fake.WithDefaultClient(
fake.NewFakeClient(
fake.WithUpdateRunnerScaleSet(
&actions.RunnerScaleSet{
Id: 1,
multiClient := scalefake.NewMultiClient(
scalefake.WithClient(
scalefake.NewClient(
scalefake.WithGenerateJitRunnerConfig(
&scaleset.RunnerScaleSetJitRunnerConfig{
Runner: &scaleset.RunnerReference{ID: 1, Name: "test-runner"},
EncodedJITConfig: "fake-jit-config",
},
nil,
),
scalefake.WithGetRunnerGroupByName(&scaleset.RunnerGroup{ID: 1, Name: "testgroup"}, nil),
scalefake.WithGetRunnerScaleSet(nil, nil),
scalefake.WithCreateRunnerScaleSet(&scaleset.RunnerScaleSet{ID: 1, Name: "testset", RunnerGroupID: 1, RunnerGroupName: "testgroup"}, nil),
scalefake.WithUpdateRunnerScaleSet(
&scaleset.RunnerScaleSet{
ID: 1,
Name: "testset_update",
RunnerGroupId: 1,
RunnerGroupID: 1,
RunnerGroupName: "testgroup",
Labels: []actions.Label{{Type: "test", Name: "test"}},
RunnerSetting: actions.RunnerSetting{},
Labels: []scaleset.Label{{Type: "test", Name: "test"}},
RunnerSetting: scaleset.RunnerSetting{},
CreatedOn: time.Now(),
RunnerJitConfigUrl: "test.test.test",
RunnerJitConfigURL: "test.test.test",
Statistics: nil,
},
nil,
),
),
nil,
),
)
@ -710,10 +742,7 @@ var _ = Describe("Test AutoScalingController updates", Ordered, func() {
ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: k8sClient,
multiClient: multiClient,
},
SecretResolver: secretresolver.New(mgr.GetClient(), multiClient),
},
}
err := controller.SetupWithManager(mgr)
@ -830,10 +859,7 @@ var _ = Describe("Test AutoscalingController creation failures", Ordered, func()
ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: k8sClient,
multiClient: fake.NewMultiClient(),
},
SecretResolver: secretresolver.New(mgr.GetClient(), scalefake.NewMultiClient()),
},
}
err := controller.SetupWithManager(mgr)
@ -953,7 +979,6 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
ctx = context.Background()
autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
multiClient := actions.NewMultiClient(logr.Discard())
controller = &AutoscalingRunnerSetReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
@ -961,10 +986,7 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: k8sClient,
multiClient: multiClient,
},
SecretResolver: secretresolver.New(mgr.GetClient(), multiclient.NewScaleset()),
},
}
@ -976,10 +998,11 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
It("should be able to make requests to a server using a proxy", func() {
serverSuccessfullyCalled := false
proxy := testserver.New(GinkgoT(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
serverSuccessfullyCalled = true
w.WriteHeader(http.StatusOK)
}))
defer proxy.Close()
min := 1
max := 10
@ -1029,23 +1052,17 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
})
It("should be able to make requests to a server using a proxy with user info", func() {
serverSuccessfullyCalled := false
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
header := r.Header.Get("Proxy-Authorization")
Expect(header).NotTo(BeEmpty())
header = strings.TrimPrefix(header, "Basic ")
decoded, err := base64.StdEncoding.DecodeString(header)
Expect(err).NotTo(HaveOccurred())
Expect(string(decoded)).To(Equal("test:password"))
serverSuccessfullyCalled = true
w.WriteHeader(http.StatusOK)
}))
GinkgoT().Cleanup(func() {
proxy.Close()
})
controller.ResourceBuilder.SecretResolver = secretresolver.New(k8sClient, scalefake.NewMultiClient(
scalefake.WithClient(
scalefake.NewClient(
scalefake.WithGetRunnerGroupByName(&scaleset.RunnerGroup{ID: 1, Name: "testgroup"}, nil),
scalefake.WithGetRunnerScaleSet(nil, nil),
scalefake.WithCreateRunnerScaleSet(&scaleset.RunnerScaleSet{ID: 1, Name: "test-asrs", RunnerGroupID: 1, RunnerGroupName: "testgroup"}, nil),
scalefake.WithUpdateRunnerScaleSet(&scaleset.RunnerScaleSet{ID: 1, Name: "test-asrs", RunnerGroupID: 1, RunnerGroupName: "testgroup"}, nil),
scalefake.WithDeleteRunnerScaleSet(nil),
),
),
))
secretCredentials := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "proxy-credentials",
@ -1057,6 +1074,11 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
},
}
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer proxy.Close()
err := k8sClient.Create(ctx, secretCredentials)
Expect(err).NotTo(HaveOccurred(), "failed to create secret credentials")
@ -1078,7 +1100,7 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
RunnerGroup: "testgroup",
Proxy: &v1alpha1.ProxyConfig{
HTTP: &v1alpha1.ProxyServerConfig{
Url: proxy.URL,
Url: "http://test:password@" + proxy.Listener.Addr().String(),
CredentialSecretRef: "proxy-credentials",
},
},
@ -1098,14 +1120,24 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
err = k8sClient.Create(ctx, autoscalingRunnerSet)
Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")
// wait for server to be called
// Verify proxy config with credentials is propagated to EphemeralRunnerSet
Eventually(
func() (bool, error) {
return serverSuccessfullyCalled, nil
func() (*v1alpha1.EphemeralRunnerSet, error) {
runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingNS.Name))
if err != nil || len(runnerSetList.Items) == 0 {
return nil, err
}
return &runnerSetList.Items[0], nil
},
autoscalingRunnerSetTestTimeout,
1*time.Nanosecond,
).Should(BeTrue(), "server was not called")
autoscalingRunnerSetTestInterval,
).Should(WithTransform(func(ers *v1alpha1.EphemeralRunnerSet) *v1alpha1.ProxyConfig {
if ers != nil {
return ers.Spec.EphemeralRunnerSpec.Proxy
}
return nil
}, Not(BeNil())), "EphemeralRunnerSet should have proxy configuration with credentials")
})
})
@ -1149,10 +1181,17 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: k8sClient,
multiClient: fake.NewMultiClient(),
},
SecretResolver: secretresolver.New(mgr.GetClient(), scalefake.NewMultiClient(
scalefake.WithClient(
scalefake.NewClient(
scalefake.WithGetRunnerGroupByName(&scaleset.RunnerGroup{ID: 1, Name: "testgroup"}, nil),
scalefake.WithGetRunnerScaleSet(nil, nil),
scalefake.WithCreateRunnerScaleSet(&scaleset.RunnerScaleSet{ID: 1, Name: "test-asrs", RunnerGroupID: 1, RunnerGroupName: "testgroup"}, nil),
scalefake.WithUpdateRunnerScaleSet(&scaleset.RunnerScaleSet{ID: 1, Name: "test-asrs", RunnerGroupID: 1, RunnerGroupName: "testgroup"}, nil),
scalefake.WithDeleteRunnerScaleSet(nil),
),
),
)),
},
}
err = controller.SetupWithManager(mgr)
@ -1162,10 +1201,7 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
})
It("should be able to make requests to a server using root CAs", func() {
controller.SecretResolver = &SecretResolver{
k8sClient: k8sClient,
multiClient: actions.NewMultiClient(logr.Discard()),
}
controller.SecretResolver = secretresolver.New(k8sClient, multiclient.NewScaleset())
certsFolder := filepath.Join(
"../../",
@ -1177,7 +1213,7 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
keyPath := filepath.Join(certsFolder, "server.key")
serverSuccessfullyCalled := false
server := testserver.NewUnstarted(GinkgoT(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
serverSuccessfullyCalled = true
w.WriteHeader(http.StatusOK)
}))
@ -1186,6 +1222,7 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
server.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
server.StartTLS()
defer server.Close()
min := 1
max := 10
@ -1198,7 +1235,7 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
},
},
Spec: v1alpha1.AutoscalingRunnerSetSpec{
GitHubConfigUrl: server.ConfigURLForOrg("my-org"),
GitHubConfigUrl: server.URL + "/my-org",
GitHubConfigSecret: configSecret.Name,
GitHubServerTLS: &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
@ -1391,10 +1428,7 @@ var _ = Describe("Test external permissions cleanup", Ordered, func() {
ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: k8sClient,
multiClient: fake.NewMultiClient(),
},
SecretResolver: secretresolver.New(mgr.GetClient(), scalefake.NewMultiClient()),
},
}
err := controller.SetupWithManager(mgr)
@ -1554,10 +1588,7 @@ var _ = Describe("Test external permissions cleanup", Ordered, func() {
ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: k8sClient,
multiClient: fake.NewMultiClient(),
},
SecretResolver: secretresolver.New(mgr.GetClient(), scalefake.NewMultiClient()),
},
}
err := controller.SetupWithManager(mgr)
@ -1767,10 +1798,7 @@ var _ = Describe("Test resource version and build version mismatch", func() {
ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: k8sClient,
multiClient: fake.NewMultiClient(),
},
SecretResolver: secretresolver.New(mgr.GetClient(), scalefake.NewMultiClient()),
},
}
err := controller.SetupWithManager(mgr)

View File

@ -6,7 +6,7 @@ import (
kclient "sigs.k8s.io/controller-runtime/pkg/client"
)
type object[T kclient.Object] interface {
type kubernetesObject[T kclient.Object] interface {
kclient.Object
DeepCopy() T
}
@ -15,7 +15,7 @@ type patcher interface {
Patch(ctx context.Context, obj kclient.Object, patch kclient.Patch, opts ...kclient.PatchOption) error
}
func patch[T object[T]](ctx context.Context, client patcher, obj T, update func(obj T)) error {
func patch[T kubernetesObject[T]](ctx context.Context, client patcher, obj T, update func(obj T)) error {
original := obj.DeepCopy()
update(obj)
return client.Patch(ctx, obj, kclient.MergeFrom(original))
@ -25,7 +25,7 @@ type subResourcePatcher interface {
Patch(ctx context.Context, obj kclient.Object, patch kclient.Patch, opts ...kclient.SubResourcePatchOption) error
}
func patchSubResource[T object[T]](ctx context.Context, client subResourcePatcher, obj T, update func(obj T)) error {
func patchSubResource[T kubernetesObject[T]](ctx context.Context, client subResourcePatcher, obj T, update func(obj T)) error {
original := obj.DeepCopy()
update(obj)
return client.Patch(ctx, obj, kclient.MergeFrom(original))

View File

@ -27,6 +27,7 @@ import (
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/scaleset"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
@ -599,7 +600,7 @@ func (r *EphemeralRunnerReconciler) deletePodAsFailed(ctx context.Context, ephem
return nil
}
func (r *EphemeralRunnerReconciler) createRunnerJitConfig(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) (*actions.RunnerScaleSetJitRunnerConfig, error) {
func (r *EphemeralRunnerReconciler) createRunnerJitConfig(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) (*scaleset.RunnerScaleSetJitRunnerConfig, error) {
// Runner is not registered with the service. We need to register it first
log.Info("Creating ephemeral runner JIT config")
actionsClient, err := r.GetActionsService(ctx, ephemeralRunner)
@ -607,7 +608,7 @@ func (r *EphemeralRunnerReconciler) createRunnerJitConfig(ctx context.Context, e
return nil, fmt.Errorf("failed to get actions client for generating JIT config: %w", err)
}
jitSettings := &actions.RunnerScaleSetJitRunnerSetting{
jitSettings := &scaleset.RunnerScaleSetJitRunnerSetting{
Name: ephemeralRunner.Name,
}
@ -618,9 +619,9 @@ func (r *EphemeralRunnerReconciler) createRunnerJitConfig(ctx context.Context, e
}
}
jitConfig, err := actionsClient.GenerateJitRunnerConfig(ctx, jitSettings, ephemeralRunner.Spec.RunnerScaleSetId)
jitConfig, err := actionsClient.GenerateJitRunnerConfig(ctx, jitSettings, ephemeralRunner.Spec.RunnerScaleSetID)
if err == nil { // if NO error
log.Info("Created ephemeral runner JIT config", "runnerId", jitConfig.Runner.Id)
log.Info("Created ephemeral runner JIT config", "runnerId", jitConfig.Runner.ID)
return jitConfig, nil
}
@ -652,10 +653,10 @@ func (r *EphemeralRunnerReconciler) createRunnerJitConfig(ctx context.Context, e
return nil, fmt.Errorf("%w: runner existed, retry configuration", retryableError)
}
log.Info("Found the runner with the same name", "runnerId", existingRunner.Id, "runnerScaleSetId", existingRunner.RunnerScaleSetId)
if existingRunner.RunnerScaleSetId == ephemeralRunner.Spec.RunnerScaleSetId {
log.Info("Found the runner with the same name", "runnerId", existingRunner.ID, "runnerScaleSetId", existingRunner.RunnerScaleSetID)
if existingRunner.RunnerScaleSetID == ephemeralRunner.Spec.RunnerScaleSetID {
log.Info("Removing the runner with the same name")
err := actionsClient.RemoveRunner(ctx, int64(existingRunner.Id))
err := actionsClient.RemoveRunner(ctx, int64(existingRunner.ID))
if err != nil {
return nil, fmt.Errorf("failed to remove runner from the service: %w", err)
}
@ -731,7 +732,7 @@ func (r *EphemeralRunnerReconciler) createPod(ctx context.Context, runner *v1alp
}
log.Info("Created ephemeral runner pod",
"runnerScaleSetId", runner.Spec.RunnerScaleSetId,
"runnerScaleSetId", runner.Spec.RunnerScaleSetID,
"runnerName", runner.Status.RunnerName,
"runnerId", runner.Status.RunnerId,
"configUrl", runner.Spec.GitHubConfigUrl,
@ -740,7 +741,7 @@ func (r *EphemeralRunnerReconciler) createPod(ctx context.Context, runner *v1alp
return ctrl.Result{}, nil
}
func (r *EphemeralRunnerReconciler) createSecret(ctx context.Context, runner *v1alpha1.EphemeralRunner, jitConfig *actions.RunnerScaleSetJitRunnerConfig, log logr.Logger) (*corev1.Secret, error) {
func (r *EphemeralRunnerReconciler) createSecret(ctx context.Context, runner *v1alpha1.EphemeralRunner, jitConfig *scaleset.RunnerScaleSetJitRunnerConfig, log logr.Logger) (*corev1.Secret, error) {
log.Info("Creating new secret for ephemeral runner")
jitSecret := r.newEphemeralRunnerJitSecret(runner, jitConfig)

View File

@ -14,10 +14,11 @@ import (
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/go-logr/logr"
"github.com/actions/actions-runner-controller/github/actions/fake"
"github.com/actions/actions-runner-controller/github/actions/testserver"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient"
scalefake "github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient/fake"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/secretresolver"
"github.com/actions/scaleset"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
@ -43,7 +44,7 @@ func newExampleRunner(name, namespace, configSecretName string) *v1alpha1.Epheme
Spec: v1alpha1.EphemeralRunnerSpec{
GitHubConfigUrl: "https://github.com/owner/repo",
GitHubConfigSecret: configSecretName,
RunnerScaleSetId: 1,
RunnerScaleSetID: 1,
PodTemplateSpec: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
@ -111,10 +112,19 @@ var _ = Describe("EphemeralRunner", func() {
Scheme: mgr.GetScheme(),
Log: logf.Log,
ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient(),
},
SecretResolver: secretresolver.New(mgr.GetClient(), scalefake.NewMultiClient(
scalefake.WithClient(
scalefake.NewClient(
scalefake.WithGenerateJitRunnerConfig(
&scaleset.RunnerScaleSetJitRunnerConfig{
Runner: &scaleset.RunnerReference{ID: 1, Name: "test-runner"},
EncodedJITConfig: "fake-jit-config",
},
nil,
),
),
),
)),
},
}
@ -1096,12 +1106,12 @@ var _ = Describe("EphemeralRunner", func() {
Scheme: mgr.GetScheme(),
Log: logf.Log,
ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient(
fake.WithDefaultClient(
fake.NewFakeClient(
fake.WithGetRunner(
SecretResolver: secretresolver.New(
mgr.GetClient(),
scalefake.NewMultiClient(
scalefake.WithClient(
scalefake.NewClient(
scalefake.WithGetRunner(
nil,
&actions.ActionsError{
StatusCode: http.StatusNotFound,
@ -1110,11 +1120,17 @@ var _ = Describe("EphemeralRunner", func() {
},
},
),
scalefake.WithGenerateJitRunnerConfig(
&scaleset.RunnerScaleSetJitRunnerConfig{
Runner: &scaleset.RunnerReference{ID: 1, Name: "test-runner"},
EncodedJITConfig: "fake-jit-config",
},
nil,
),
),
nil,
),
),
},
),
},
}
err := controller.SetupWithManager(mgr)
@ -1181,10 +1197,19 @@ var _ = Describe("EphemeralRunner", func() {
Scheme: mgr.GetScheme(),
Log: logf.Log,
ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient(),
},
SecretResolver: secretresolver.New(mgr.GetClient(), scalefake.NewMultiClient(
scalefake.WithClient(
scalefake.NewClient(
scalefake.WithGenerateJitRunnerConfig(
&scaleset.RunnerScaleSetJitRunnerConfig{
Runner: &scaleset.RunnerReference{ID: 1, Name: "test-runner"},
EncodedJITConfig: "fake-jit-config",
},
nil,
),
),
),
)),
},
}
err := controller.SetupWithManager(mgr)
@ -1196,10 +1221,10 @@ var _ = Describe("EphemeralRunner", func() {
It("uses an actions client with proxy transport", func() {
// Use an actual client
controller.ResourceBuilder = ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: actions.NewMultiClient(logr.Discard()),
},
SecretResolver: secretresolver.New(
mgr.GetClient(),
multiclient.NewScaleset(),
),
}
proxySuccessfulllyCalled := false
@ -1355,10 +1380,7 @@ var _ = Describe("EphemeralRunner", func() {
Scheme: mgr.GetScheme(),
Log: logf.Log,
ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient(),
},
SecretResolver: secretresolver.New(mgr.GetClient(), scalefake.NewMultiClient()),
},
}
@ -1379,7 +1401,7 @@ var _ = Describe("EphemeralRunner", func() {
keyPath := filepath.Join(certsFolder, "server.key")
serverSuccessfullyCalled := false
server := testserver.NewUnstarted(GinkgoT(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
serverSuccessfullyCalled = true
w.WriteHeader(http.StatusOK)
}))
@ -1388,17 +1410,18 @@ var _ = Describe("EphemeralRunner", func() {
server.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
server.StartTLS()
defer server.Close()
// Use an actual client
controller.ResourceBuilder = ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: actions.NewMultiClient(logr.Discard()),
},
SecretResolver: secretresolver.New(
mgr.GetClient(),
multiclient.NewScaleset(),
),
}
ephemeralRunner := newExampleRunner("test-runner", autoScalingNS.Name, configSecret.Name)
ephemeralRunner.Spec.GitHubConfigUrl = server.ConfigURLForOrg("my-org")
ephemeralRunner.Spec.GitHubConfigUrl = server.URL + "/my-org"
ephemeralRunner.Spec.GitHubServerTLS = &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{

View File

@ -26,6 +26,7 @@ import (
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/metrics"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/go-logr/logr"
"go.uber.org/multierr"
@ -481,7 +482,7 @@ func (r *EphemeralRunnerSetReconciler) deleteIdleEphemeralRunners(ctx context.Co
return multierr.Combine(errs...)
}
func (r *EphemeralRunnerSetReconciler) deleteEphemeralRunnerWithActionsClient(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, actionsClient actions.ActionsService, log logr.Logger) (bool, error) {
func (r *EphemeralRunnerSetReconciler) deleteEphemeralRunnerWithActionsClient(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, actionsClient multiclient.Client, log logr.Logger) (bool, error) {
if err := actionsClient.RemoveRunner(ctx, int64(ephemeralRunner.Status.RunnerId)); err != nil {
actionsError := &actions.ActionsError{}
if !errors.As(err, &actionsError) {

View File

@ -2,7 +2,6 @@ package actionsgithubcom
import (
"context"
"crypto/tls"
"encoding/base64"
"fmt"
"net/http"
@ -19,16 +18,15 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"github.com/go-logr/logr"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/actions-runner-controller/github/actions/fake"
"github.com/actions/actions-runner-controller/github/actions/testserver"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient"
fake "github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient/fake"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/secretresolver"
)
const (
@ -57,10 +55,13 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
Scheme: mgr.GetScheme(),
Log: logf.Log,
ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient(),
},
SecretResolver: secretresolver.New(mgr.GetClient(), fake.NewMultiClient(
fake.WithClient(
fake.NewClient(
fake.WithRemoveRunner(nil),
),
),
)),
},
}
err := controller.SetupWithManager(mgr)
@ -75,7 +76,7 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
GitHubConfigUrl: "https://github.com/owner/repo",
GitHubConfigSecret: configSecret.Name,
RunnerScaleSetId: 100,
RunnerScaleSetID: 100,
PodTemplateSpec: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
@ -1103,10 +1104,7 @@ var _ = Describe("Test EphemeralRunnerSet controller with proxy settings", func(
Scheme: mgr.GetScheme(),
Log: logf.Log,
ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: actions.NewMultiClient(logr.Discard()),
},
SecretResolver: secretresolver.New(mgr.GetClient(), multiclient.NewScaleset()),
},
}
err := controller.SetupWithManager(mgr)
@ -1140,7 +1138,7 @@ var _ = Describe("Test EphemeralRunnerSet controller with proxy settings", func(
EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
GitHubConfigUrl: "http://example.com/owner/repo",
GitHubConfigSecret: configSecret.Name,
RunnerScaleSetId: 100,
RunnerScaleSetID: 100,
Proxy: &v1alpha1.ProxyConfig{
HTTP: &v1alpha1.ProxyServerConfig{
Url: "http://proxy.example.com",
@ -1319,7 +1317,7 @@ var _ = Describe("Test EphemeralRunnerSet controller with proxy settings", func(
EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
GitHubConfigUrl: "http://example.com/owner/repo",
GitHubConfigSecret: configSecret.Name,
RunnerScaleSetId: 100,
RunnerScaleSetID: 100,
Proxy: &v1alpha1.ProxyConfig{
HTTP: &v1alpha1.ProxyServerConfig{
Url: proxy.URL,
@ -1419,10 +1417,7 @@ var _ = Describe("Test EphemeralRunnerSet controller with custom root CA", func(
Scheme: mgr.GetScheme(),
Log: logf.Log,
ResourceBuilder: ResourceBuilder{
SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(),
multiClient: actions.NewMultiClient(logr.Discard()),
},
SecretResolver: secretresolver.New(mgr.GetClient(), multiclient.NewScaleset()),
},
}
err = controller.SetupWithManager(mgr)
@ -1432,26 +1427,6 @@ var _ = Describe("Test EphemeralRunnerSet controller with custom root CA", func(
})
It("should be able to make requests to a server using root CAs", func() {
certsFolder := filepath.Join(
"../../",
"github",
"actions",
"testdata",
)
certPath := filepath.Join(certsFolder, "server.crt")
keyPath := filepath.Join(certsFolder, "server.key")
serverSuccessfullyCalled := false
server := testserver.NewUnstarted(GinkgoT(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
serverSuccessfullyCalled = true
w.WriteHeader(http.StatusOK)
}))
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
Expect(err).NotTo(HaveOccurred(), "failed to load server cert")
server.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
server.StartTLS()
ephemeralRunnerSet = &v1alpha1.EphemeralRunnerSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-asrs",
@ -1460,7 +1435,7 @@ var _ = Describe("Test EphemeralRunnerSet controller with custom root CA", func(
Spec: v1alpha1.EphemeralRunnerSetSpec{
Replicas: 1,
EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
GitHubConfigUrl: server.ConfigURLForOrg("my-org"),
GitHubConfigUrl: "https://github.example.com/api/v3",
GitHubConfigSecret: configSecret.Name,
GitHubServerTLS: &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
@ -1472,7 +1447,7 @@ var _ = Describe("Test EphemeralRunnerSet controller with custom root CA", func(
},
},
},
RunnerScaleSetId: 100,
RunnerScaleSetID: 100,
PodTemplateSpec: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
@ -1487,7 +1462,7 @@ var _ = Describe("Test EphemeralRunnerSet controller with custom root CA", func(
},
}
err = k8sClient.Create(ctx, ephemeralRunnerSet)
err := k8sClient.Create(ctx, ephemeralRunnerSet)
Expect(err).NotTo(HaveOccurred(), "failed to create EphemeralRunnerSet")
runnerList := new(v1alpha1.EphemeralRunnerList)
@ -1503,32 +1478,10 @@ var _ = Describe("Test EphemeralRunnerSet controller with custom root CA", func(
ephemeralRunnerSetTestInterval,
).Should(BeEquivalentTo(1), "failed to create ephemeral runner")
// Verify that the TLS configuration is properly propagated to the runner
runner := runnerList.Items[0].DeepCopy()
Expect(runner.Spec.GitHubServerTLS).NotTo(BeNil(), "runner tls config should not be nil")
Expect(runner.Spec.GitHubServerTLS).To(BeEquivalentTo(ephemeralRunnerSet.Spec.EphemeralRunnerSpec.GitHubServerTLS), "runner tls config should be correct")
runner.Status.Phase = corev1.PodRunning
runner.Status.RunnerId = 100
err = k8sClient.Status().Patch(ctx, runner, client.MergeFrom(&runnerList.Items[0]))
Expect(err).NotTo(HaveOccurred(), "failed to update ephemeral runner status")
currentRunnerSet := new(v1alpha1.EphemeralRunnerSet)
err = k8sClient.Get(ctx, client.ObjectKey{Namespace: ephemeralRunnerSet.Namespace, Name: ephemeralRunnerSet.Name}, currentRunnerSet)
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
updatedRunnerSet := currentRunnerSet.DeepCopy()
updatedRunnerSet.Spec.Replicas = 0
err = k8sClient.Patch(ctx, updatedRunnerSet, client.MergeFrom(currentRunnerSet))
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
// wait for server to be called
Eventually(
func() bool {
return serverSuccessfullyCalled
},
autoscalingRunnerSetTestTimeout,
1*time.Nanosecond,
).Should(BeTrue(), "server was not called")
})
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,230 @@
package fake
import (
"context"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient"
"github.com/actions/scaleset"
)
// ClientOption is a functional option for configuring a fake Client
type ClientOption func(*Client)
// WithGetRunnerScaleSet configures the result of GetRunnerScaleSet
func WithGetRunnerScaleSet(result *scaleset.RunnerScaleSet, err error) ClientOption {
return func(c *Client) {
c.getRunnerScaleSetResult.RunnerScaleSet = result
c.getRunnerScaleSetResult.err = err
}
}
// WithGetRunnerScaleSetByID configures the result of GetRunnerScaleSetByID
func WithGetRunnerScaleSetByID(result *scaleset.RunnerScaleSet, err error) ClientOption {
return func(c *Client) {
c.getRunnerScaleSetByIDResult.RunnerScaleSet = result
c.getRunnerScaleSetByIDResult.err = err
}
}
// WithGetRunnerGroupByName configures the result of GetRunnerGroupByName
func WithGetRunnerGroupByName(result *scaleset.RunnerGroup, err error) ClientOption {
return func(c *Client) {
c.getRunnerGroupByNameResult.RunnerGroup = result
c.getRunnerGroupByNameResult.err = err
}
}
// WithGetRunnerGroupByNameFunc configures a function to handle GetRunnerGroupByName calls dynamically
func WithGetRunnerGroupByNameFunc(fn func(context.Context, string) (*scaleset.RunnerGroup, error)) ClientOption {
return func(c *Client) {
c.getRunnerGroupByNameFunc = fn
}
}
// WithCreateRunnerScaleSet configures the result of CreateRunnerScaleSet
func WithCreateRunnerScaleSet(result *scaleset.RunnerScaleSet, err error) ClientOption {
return func(c *Client) {
c.createRunnerScaleSetResult.RunnerScaleSet = result
c.createRunnerScaleSetResult.err = err
}
}
// WithUpdateRunnerScaleSet configures the result of UpdateRunnerScaleSet
func WithUpdateRunnerScaleSet(result *scaleset.RunnerScaleSet, err error) ClientOption {
return func(c *Client) {
c.updateRunnerScaleSetResult.RunnerScaleSet = result
c.updateRunnerScaleSetResult.err = err
}
}
// WithDeleteRunnerScaleSet configures the result of DeleteRunnerScaleSet
func WithDeleteRunnerScaleSet(err error) ClientOption {
return func(c *Client) {
c.deleteRunnerScaleSetResult.err = err
}
}
// WithRemoveRunner configures the result of RemoveRunner
func WithRemoveRunner(err error) ClientOption {
return func(c *Client) {
c.removeRunnerResult.err = err
}
}
// WithGenerateJitRunnerConfig configures the result of GenerateJitRunnerConfig
func WithGenerateJitRunnerConfig(result *scaleset.RunnerScaleSetJitRunnerConfig, err error) ClientOption {
return func(c *Client) {
c.generateJitRunnerConfigResult.RunnerScaleSetJitRunnerConfig = result
c.generateJitRunnerConfigResult.err = err
}
}
// WithGetRunnerByName configures the result of GetRunnerByName
func WithGetRunnerByName(result *scaleset.RunnerReference, err error) ClientOption {
return func(c *Client) {
c.getRunnerByNameResult.RunnerReference = result
c.getRunnerByNameResult.err = err
}
}
// WithGetRunner configures the result of GetRunner
func WithGetRunner(result *scaleset.RunnerReference, err error) ClientOption {
return func(c *Client) {
c.getRunnerResult.RunnerReference = result
c.getRunnerResult.err = err
}
}
// WithSystemInfo configures the SystemInfo
func WithSystemInfo(info scaleset.SystemInfo) ClientOption {
return func(c *Client) {
c.systemInfo = info
}
}
// WithUpdateRunnerScaleSetFunc configures a function to handle UpdateRunnerScaleSet calls dynamically
func WithUpdateRunnerScaleSetFunc(fn func(context.Context, int, *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error)) ClientOption {
return func(c *Client) {
c.updateRunnerScaleSetFunc = fn
}
}
// Client implements multiclient.Client interface for testing
type Client struct {
systemInfo scaleset.SystemInfo
updateRunnerScaleSetFunc func(context.Context, int, *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error)
getRunnerScaleSetResult struct {
*scaleset.RunnerScaleSet
err error
}
getRunnerScaleSetByIDResult struct {
*scaleset.RunnerScaleSet
err error
}
getRunnerGroupByNameResult struct {
*scaleset.RunnerGroup
err error
}
getRunnerGroupByNameFunc func(context.Context, string) (*scaleset.RunnerGroup, error)
createRunnerScaleSetResult struct {
*scaleset.RunnerScaleSet
err error
}
updateRunnerScaleSetResult struct {
*scaleset.RunnerScaleSet
err error
}
deleteRunnerScaleSetResult struct {
err error
}
removeRunnerResult struct {
err error
}
generateJitRunnerConfigResult struct {
*scaleset.RunnerScaleSetJitRunnerConfig
err error
}
getRunnerByNameResult struct {
*scaleset.RunnerReference
err error
}
getRunnerResult struct {
*scaleset.RunnerReference
err error
}
messageSessionClientResult struct {
*scaleset.MessageSessionClient
err error
}
}
// Compile-time interface check
var _ multiclient.Client = (*Client)(nil)
// NewClient creates a new fake Client with the given options
func NewClient(opts ...ClientOption) *Client {
c := &Client{}
for _, opt := range opts {
opt(c)
}
return c
}
func (c *Client) SetSystemInfo(info scaleset.SystemInfo) {
c.systemInfo = info
}
func (c *Client) SystemInfo() scaleset.SystemInfo {
return c.systemInfo
}
func (c *Client) MessageSessionClient(ctx context.Context, runnerScaleSetID int, owner string, options ...scaleset.HTTPOption) (*scaleset.MessageSessionClient, error) {
return c.messageSessionClientResult.MessageSessionClient, c.messageSessionClientResult.err
}
func (c *Client) GenerateJitRunnerConfig(ctx context.Context, jitRunnerSetting *scaleset.RunnerScaleSetJitRunnerSetting, scaleSetID int) (*scaleset.RunnerScaleSetJitRunnerConfig, error) {
return c.generateJitRunnerConfigResult.RunnerScaleSetJitRunnerConfig, c.generateJitRunnerConfigResult.err
}
func (c *Client) GetRunner(ctx context.Context, runnerID int) (*scaleset.RunnerReference, error) {
return c.getRunnerResult.RunnerReference, c.getRunnerResult.err
}
func (c *Client) GetRunnerByName(ctx context.Context, runnerName string) (*scaleset.RunnerReference, error) {
return c.getRunnerByNameResult.RunnerReference, c.getRunnerByNameResult.err
}
func (c *Client) RemoveRunner(ctx context.Context, runnerID int64) error {
return c.removeRunnerResult.err
}
func (c *Client) GetRunnerGroupByName(ctx context.Context, runnerGroup string) (*scaleset.RunnerGroup, error) {
if c.getRunnerGroupByNameFunc != nil {
return c.getRunnerGroupByNameFunc(ctx, runnerGroup)
}
return c.getRunnerGroupByNameResult.RunnerGroup, c.getRunnerGroupByNameResult.err
}
func (c *Client) GetRunnerScaleSet(ctx context.Context, runnerGroupID int, runnerScaleSetName string) (*scaleset.RunnerScaleSet, error) {
return c.getRunnerScaleSetResult.RunnerScaleSet, c.getRunnerScaleSetResult.err
}
func (c *Client) GetRunnerScaleSetByID(ctx context.Context, runnerScaleSetID int) (*scaleset.RunnerScaleSet, error) {
return c.getRunnerScaleSetByIDResult.RunnerScaleSet, c.getRunnerScaleSetByIDResult.err
}
func (c *Client) CreateRunnerScaleSet(ctx context.Context, runnerScaleSet *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error) {
return c.createRunnerScaleSetResult.RunnerScaleSet, c.createRunnerScaleSetResult.err
}
func (c *Client) UpdateRunnerScaleSet(ctx context.Context, runnerScaleSetID int, runnerScaleSet *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error) {
if c.updateRunnerScaleSetFunc != nil {
return c.updateRunnerScaleSetFunc(ctx, runnerScaleSetID, runnerScaleSet)
}
return c.updateRunnerScaleSetResult.RunnerScaleSet, c.updateRunnerScaleSetResult.err
}
func (c *Client) DeleteRunnerScaleSet(ctx context.Context, runnerScaleSetID int) error {
return c.deleteRunnerScaleSetResult.err
}

View File

@ -0,0 +1,53 @@
package fake
import (
"context"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient"
)
// MultiClientOption is a functional option for configuring a fake MultiClient
type MultiClientOption func(*MultiClient)
// WithClient configures the client that GetClientFor will return
func WithClient(c multiclient.Client) MultiClientOption {
return func(mc *MultiClient) {
mc.client = c
}
}
// WithGetClientForError configures an error that GetClientFor will return
func WithGetClientForError(err error) MultiClientOption {
return func(mc *MultiClient) {
mc.getClientForErr = err
}
}
// MultiClient implements multiclient.MultiClient interface for testing
type MultiClient struct {
client multiclient.Client
getClientForErr error
}
// Compile-time interface check
var _ multiclient.MultiClient = (*MultiClient)(nil)
// NewMultiClient creates a new fake MultiClient with the given options
func NewMultiClient(opts ...MultiClientOption) *MultiClient {
mc := &MultiClient{}
for _, opt := range opts {
opt(mc)
}
// Default behavior: if no client configured, return a default NewClient()
if mc.client == nil {
mc.client = NewClient()
}
return mc
}
func (mc *MultiClient) GetClientFor(ctx context.Context, opts *multiclient.ClientForOptions) (multiclient.Client, error) {
if mc.getClientForErr != nil {
return nil, mc.getClientForErr
}
return mc.client, nil
}

View File

@ -0,0 +1,177 @@
package multiclient
import (
"context"
"crypto/sha256"
"crypto/x509"
"fmt"
"net/http"
"net/url"
"strconv"
"sync"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/actions/actions-runner-controller/build"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/scaleset"
"github.com/google/uuid"
)
type MultiClient interface {
GetClientFor(ctx context.Context, opts *ClientForOptions) (Client, error)
}
type Scaleset struct {
mu sync.Mutex
clients map[string]*multiClientEntry
}
type multiClientEntry struct {
client *scaleset.Client
rootCAs *x509.CertPool
}
func NewScaleset() *Scaleset {
return &Scaleset{
clients: make(map[string]*multiClientEntry),
}
}
type Client interface {
SetSystemInfo(info scaleset.SystemInfo)
SystemInfo() scaleset.SystemInfo
MessageSessionClient(ctx context.Context, runnerScaleSetID int, owner string, options ...scaleset.HTTPOption) (*scaleset.MessageSessionClient, error)
GenerateJitRunnerConfig(ctx context.Context, jitRunnerSetting *scaleset.RunnerScaleSetJitRunnerSetting, scaleSetID int) (*scaleset.RunnerScaleSetJitRunnerConfig, error)
GetRunner(ctx context.Context, runnerID int) (*scaleset.RunnerReference, error)
GetRunnerByName(ctx context.Context, runnerName string) (*scaleset.RunnerReference, error)
RemoveRunner(ctx context.Context, runnerID int64) error
GetRunnerGroupByName(ctx context.Context, runnerGroup string) (*scaleset.RunnerGroup, error)
GetRunnerScaleSet(ctx context.Context, runnerGroupID int, runnerScaleSetName string) (*scaleset.RunnerScaleSet, error)
GetRunnerScaleSetByID(ctx context.Context, runnerScaleSetID int) (*scaleset.RunnerScaleSet, error)
CreateRunnerScaleSet(ctx context.Context, runnerScaleSet *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error)
UpdateRunnerScaleSet(ctx context.Context, runnerScaleSetID int, runnerScaleSet *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error)
DeleteRunnerScaleSet(ctx context.Context, runnerScaleSetID int) error
}
func (m *Scaleset) GetClientFor(ctx context.Context, opts *ClientForOptions) (Client, error) {
identifier, err := opts.identifier()
if err != nil {
return nil, fmt.Errorf("failed to generate client identifier: %w", err)
}
m.mu.Lock()
defer m.mu.Unlock()
entry, ok := m.clients[identifier]
if ok && entry.rootCAs.Equal(opts.RootCAs) {
return entry.client, nil
}
client, err := opts.newClient()
if err != nil {
return nil, fmt.Errorf("failed to create new client: %w", err)
}
m.clients[identifier] = &multiClientEntry{
client: client,
rootCAs: opts.RootCAs,
}
return client, nil
}
type ClientForOptions struct {
GithubConfigURL string
AppConfig appconfig.AppConfig
Namespace string
RootCAs *x509.CertPool
ProxyFunc func(*http.Request) (*url.URL, error)
}
func (o *ClientForOptions) identifier() (string, error) {
if err := o.AppConfig.Validate(); err != nil {
return "", fmt.Errorf("failed to validate app config: %w", err)
}
if _, err := actions.ParseGitHubConfigFromURL(o.GithubConfigURL); err != nil {
return "", fmt.Errorf("failed to parse GitHub config URL: %w", err)
}
if o.Namespace == "" {
return "", fmt.Errorf("namespace is required to generate client identifier")
}
identifier := fmt.Sprintf("configURL:%q,namespace:%q,proxy:%t", o.GithubConfigURL, o.Namespace, o.ProxyFunc != nil)
if o.AppConfig.Token != "" {
identifier += fmt.Sprintf(",token:%q,", o.AppConfig.Token)
} else {
identifier += fmt.Sprintf(
",appID:%q,installationID:%q,key:%q",
o.AppConfig.AppID,
strconv.FormatInt(o.AppConfig.AppInstallationID, 10),
o.AppConfig.AppPrivateKey,
)
}
if o.RootCAs != nil {
// ignoring because this cert pool is intended not to come from SystemCertPool
// nolint:staticcheck
identifier += fmt.Sprintf(",rootCAs:%q", o.RootCAs.Subjects())
}
return uuid.NewHash(sha256.New(), uuid.NameSpaceOID, []byte(identifier), 6).String(), nil
}
func (o *ClientForOptions) newClient() (*scaleset.Client, error) {
systemInfo := scaleset.SystemInfo{
System: "actions-runner-controller",
Version: build.Version,
CommitSHA: build.CommitSHA,
ScaleSetID: 0, // by default, scale set is 0 (not created yet)
Subsystem: "gha-scale-set-controller",
}
var options []scaleset.HTTPOption
if o.RootCAs != nil {
options = append(options, scaleset.WithRootCAs(o.RootCAs))
}
if o.ProxyFunc != nil {
options = append(options, scaleset.WithProxy(o.ProxyFunc))
}
if o.AppConfig.Token != "" {
c, err := scaleset.NewClientWithPersonalAccessToken(
scaleset.NewClientWithPersonalAccessTokenConfig{
GitHubConfigURL: o.GithubConfigURL,
PersonalAccessToken: o.AppConfig.Token,
SystemInfo: systemInfo,
},
options...,
)
if err != nil {
return nil, fmt.Errorf("failed to instantiate client with personal access token auth: %w", err)
}
return c, nil
}
c, err := scaleset.NewClientWithGitHubApp(
scaleset.ClientWithGitHubAppConfig{
GitHubConfigURL: o.GithubConfigURL,
GitHubAppAuth: scaleset.GitHubAppAuth{
ClientID: o.AppConfig.AppID,
InstallationID: o.AppConfig.AppInstallationID,
PrivateKey: o.AppConfig.AppPrivateKey,
},
SystemInfo: systemInfo,
},
options...,
)
if err != nil {
return nil, fmt.Errorf("failed to instantiate client with GitHub App auth: %w", err)
}
return c, nil
}

View File

@ -0,0 +1,16 @@
package object
import (
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type ActionsGitHubObject interface {
client.Object
GitHubConfigUrl() string
GitHubConfigSecret() string
GitHubProxy() *v1alpha1.ProxyConfig
GitHubServerTLS() *v1alpha1.TLSConfig
VaultConfig() *v1alpha1.VaultConfig
VaultProxy() *v1alpha1.ProxyConfig
}

View File

@ -2,6 +2,7 @@ package actionsgithubcom
import (
"bytes"
"context"
"encoding/json"
"fmt"
"maps"
@ -14,10 +15,13 @@ import (
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/actions/actions-runner-controller/build"
ghalistenerconfig "github.com/actions/actions-runner-controller/cmd/ghalistener/config"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/object"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/actions-runner-controller/hash"
"github.com/actions/actions-runner-controller/logging"
"github.com/actions/actions-runner-controller/vault/azurekeyvault"
"github.com/actions/scaleset"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -71,14 +75,14 @@ func SetListenerEntrypoint(entrypoint string) {
}
}
type ResourceBuilder struct {
ExcludeLabelPropagationPrefixes []string
*SecretResolver
type SecretResolver interface {
GetAppConfig(ctx context.Context, obj object.ActionsGitHubObject) (*appconfig.AppConfig, error)
GetActionsService(ctx context.Context, obj object.ActionsGitHubObject) (multiclient.Client, error)
}
// boolPtr returns a pointer to a bool value
func boolPtr(v bool) *bool {
return &v
type ResourceBuilder struct {
ExcludeLabelPropagationPrefixes []string
SecretResolver
}
func (b *ResourceBuilder) newAutoScalingListener(autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet, namespace, image string, imagePullSecrets []corev1.LocalObjectReference) (*v1alpha1.AutoscalingListener, error) {
@ -183,7 +187,7 @@ func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha
}
config := ghalistenerconfig.Config{
ConfigureUrl: autoscalingListener.Spec.GitHubConfigUrl,
ConfigureURL: autoscalingListener.Spec.GitHubConfigUrl,
EphemeralRunnerSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
EphemeralRunnerSetName: autoscalingListener.Spec.EphemeralRunnerSetName,
MaxRunners: autoscalingListener.Spec.MaxRunners,
@ -319,8 +323,8 @@ func (b *ResourceBuilder) newScaleSetListenerPod(autoscalingListener *v1alpha1.A
Kind: autoscalingListener.GetObjectKind().GroupVersionKind().Kind,
UID: autoscalingListener.GetUID(),
Name: autoscalingListener.GetName(),
Controller: boolPtr(true),
BlockOwnerDeletion: boolPtr(true),
Controller: new(true),
BlockOwnerDeletion: new(true),
},
},
},
@ -591,15 +595,15 @@ func (b *ResourceBuilder) newEphemeralRunnerSet(autoscalingRunnerSet *v1alpha1.A
Kind: autoscalingRunnerSet.GetObjectKind().GroupVersionKind().Kind,
UID: autoscalingRunnerSet.GetUID(),
Name: autoscalingRunnerSet.GetName(),
Controller: boolPtr(true),
BlockOwnerDeletion: boolPtr(true),
Controller: new(true),
BlockOwnerDeletion: new(true),
},
},
},
Spec: v1alpha1.EphemeralRunnerSetSpec{
Replicas: 0,
EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
RunnerScaleSetId: runnerScaleSetID,
RunnerScaleSetID: runnerScaleSetID,
GitHubConfigUrl: autoscalingRunnerSet.Spec.GitHubConfigUrl,
GitHubConfigSecret: autoscalingRunnerSet.Spec.GitHubConfigSecret,
Proxy: autoscalingRunnerSet.Spec.Proxy,
@ -645,8 +649,8 @@ func (b *ResourceBuilder) newEphemeralRunner(ephemeralRunnerSet *v1alpha1.Epheme
Kind: ephemeralRunnerSet.GetObjectKind().GroupVersionKind().Kind,
UID: ephemeralRunnerSet.GetUID(),
Name: ephemeralRunnerSet.GetName(),
Controller: boolPtr(true),
BlockOwnerDeletion: boolPtr(true),
Controller: new(true),
BlockOwnerDeletion: new(true),
},
},
},
@ -683,8 +687,8 @@ func (b *ResourceBuilder) newEphemeralRunnerPod(runner *v1alpha1.EphemeralRunner
Kind: runner.GetObjectKind().GroupVersionKind().Kind,
UID: runner.GetUID(),
Name: runner.GetName(),
Controller: boolPtr(true),
BlockOwnerDeletion: boolPtr(true),
Controller: new(true),
BlockOwnerDeletion: new(true),
},
},
}
@ -722,7 +726,7 @@ func (b *ResourceBuilder) newEphemeralRunnerPod(runner *v1alpha1.EphemeralRunner
return &newPod
}
func (b *ResourceBuilder) newEphemeralRunnerJitSecret(ephemeralRunner *v1alpha1.EphemeralRunner, jitConfig *actions.RunnerScaleSetJitRunnerConfig) *corev1.Secret {
func (b *ResourceBuilder) newEphemeralRunnerJitSecret(ephemeralRunner *v1alpha1.EphemeralRunner, jitConfig *scaleset.RunnerScaleSetJitRunnerConfig) *corev1.Secret {
var (
labels map[string]string
annotations map[string]string
@ -743,8 +747,8 @@ func (b *ResourceBuilder) newEphemeralRunnerJitSecret(ephemeralRunner *v1alpha1.
Data: map[string][]byte{
jitTokenKey: []byte(jitConfig.EncodedJITConfig),
"runnerName": []byte(jitConfig.Runner.Name),
"runnerId": []byte(strconv.Itoa(jitConfig.Runner.Id)),
"scaleSetId": []byte(strconv.Itoa(jitConfig.Runner.RunnerScaleSetId)),
"runnerId": []byte(strconv.Itoa(jitConfig.Runner.ID)),
"scaleSetId": []byte(strconv.Itoa(jitConfig.Runner.RunnerScaleSetID)),
},
}
}

View File

@ -6,7 +6,7 @@ import (
"testing"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/scaleset"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
@ -176,11 +176,11 @@ func TestMetadataPropagation(t *testing.T) {
assert.Equal(t, "ephemeral-runner-label", ephemeralRunner.Labels["test.com/ephemeral-runner-label"])
assert.Equal(t, "ephemeral-runner-annotation", ephemeralRunner.Annotations["test.com/ephemeral-runner-annotation"])
runnerSecret := b.newEphemeralRunnerJitSecret(ephemeralRunner, &actions.RunnerScaleSetJitRunnerConfig{
Runner: &actions.RunnerReference{
Id: 1,
runnerSecret := b.newEphemeralRunnerJitSecret(ephemeralRunner, &scaleset.RunnerScaleSetJitRunnerConfig{
Runner: &scaleset.RunnerReference{
ID: 1,
Name: "test",
RunnerScaleSetId: 1,
RunnerScaleSetID: 1,
},
EncodedJITConfig: "",
})

View File

@ -1,16 +1,18 @@
package actionsgithubcom
package secretresolver
import (
"context"
"crypto/x509"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/object"
"github.com/actions/actions-runner-controller/vault"
"github.com/actions/actions-runner-controller/vault/azurekeyvault"
"golang.org/x/net/http/httpproxy"
@ -21,19 +23,27 @@ import (
type SecretResolver struct {
k8sClient client.Client
multiClient actions.MultiClient
multiClient multiclient.MultiClient
logger *slog.Logger
}
type SecretResolverOption func(*SecretResolver)
type Option func(*SecretResolver)
func NewSecretResolver(k8sClient client.Client, multiClient actions.MultiClient, opts ...SecretResolverOption) *SecretResolver {
func WithLogger(logger *slog.Logger) Option {
return func(sr *SecretResolver) {
sr.logger = logger
}
}
func New(k8sClient client.Client, scalesetMultiClient multiclient.MultiClient, opts ...Option) *SecretResolver {
if k8sClient == nil {
panic("k8sClient must not be nil")
}
secretResolver := &SecretResolver{
k8sClient: k8sClient,
multiClient: multiClient,
multiClient: scalesetMultiClient,
logger: slog.New(slog.DiscardHandler),
}
for _, opt := range opts {
@ -43,17 +53,7 @@ func NewSecretResolver(k8sClient client.Client, multiClient actions.MultiClient,
return secretResolver
}
type ActionsGitHubObject interface {
client.Object
GitHubConfigUrl() string
GitHubConfigSecret() string
GitHubProxy() *v1alpha1.ProxyConfig
GitHubServerTLS() *v1alpha1.TLSConfig
VaultConfig() *v1alpha1.VaultConfig
VaultProxy() *v1alpha1.ProxyConfig
}
func (sr *SecretResolver) GetAppConfig(ctx context.Context, obj ActionsGitHubObject) (*appconfig.AppConfig, error) {
func (sr *SecretResolver) GetAppConfig(ctx context.Context, obj object.ActionsGitHubObject) (*appconfig.AppConfig, error) {
resolver, err := sr.resolverForObject(ctx, obj)
if err != nil {
return nil, fmt.Errorf("failed to get resolver for object: %v", err)
@ -67,7 +67,7 @@ func (sr *SecretResolver) GetAppConfig(ctx context.Context, obj ActionsGitHubObj
return appConfig, nil
}
func (sr *SecretResolver) GetActionsService(ctx context.Context, obj ActionsGitHubObject) (actions.ActionsService, error) {
func (sr *SecretResolver) GetActionsService(ctx context.Context, obj object.ActionsGitHubObject) (multiclient.Client, error) {
resolver, err := sr.resolverForObject(ctx, obj)
if err != nil {
return nil, fmt.Errorf("failed to get resolver for object: %v", err)
@ -78,7 +78,7 @@ func (sr *SecretResolver) GetActionsService(ctx context.Context, obj ActionsGitH
return nil, fmt.Errorf("failed to resolve app config: %v", err)
}
var clientOptions []actions.ClientOption
var proxyFunc func(req *http.Request) (*url.URL, error)
if proxy := obj.GitHubProxy(); proxy != nil {
config := &httpproxy.Config{
NoProxy: strings.Join(proxy.NoProxy, ","),
@ -116,16 +116,14 @@ func (sr *SecretResolver) GetActionsService(ctx context.Context, obj ActionsGitH
config.HTTPSProxy = u.String()
}
proxyFunc := func(req *http.Request) (*url.URL, error) {
proxyFunc = func(req *http.Request) (*url.URL, error) {
return config.ProxyFunc()(req.URL)
}
clientOptions = append(clientOptions, actions.WithProxy(proxyFunc))
}
tlsConfig := obj.GitHubServerTLS()
if tlsConfig != nil {
pool, err := tlsConfig.ToCertPool(func(name, key string) ([]byte, error) {
var rootCAs *x509.CertPool
if tc := obj.GitHubServerTLS(); tc != nil {
pool, err := tc.ToCertPool(func(name, key string) ([]byte, error) {
var configmap corev1.ConfigMap
err := sr.k8sClient.Get(
ctx,
@ -145,19 +143,22 @@ func (sr *SecretResolver) GetActionsService(ctx context.Context, obj ActionsGitH
return nil, fmt.Errorf("failed to get tls config: %w", err)
}
clientOptions = append(clientOptions, actions.WithRootCAs(pool))
rootCAs = pool
}
return sr.multiClient.GetClientFor(
ctx,
obj.GitHubConfigUrl(),
appConfig,
obj.GetNamespace(),
clientOptions...,
&multiclient.ClientForOptions{
GithubConfigURL: obj.GitHubConfigUrl(),
AppConfig: *appConfig,
Namespace: obj.GetNamespace(),
RootCAs: rootCAs,
ProxyFunc: proxyFunc,
},
)
}
func (sr *SecretResolver) resolverForObject(ctx context.Context, obj ActionsGitHubObject) (resolver, error) {
func (sr *SecretResolver) resolverForObject(ctx context.Context, obj object.ActionsGitHubObject) (resolver, error) {
vaultConfig := obj.VaultConfig()
if vaultConfig == nil || vaultConfig.Type == "" {
return &k8sResolver{

2
go.mod
View File

@ -6,7 +6,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0
github.com/actions/scaleset v0.1.1-0.20260218224657-feb84c6d04fb
github.com/actions/scaleset v0.2.0
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/evanphx/json-patch v5.9.11+incompatible

4
go.sum
View File

@ -25,8 +25,8 @@ github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBi
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/actions-runner-controller/httpcache v0.2.0 h1:hCNvYuVPJ2xxYBymqBvH0hSiQpqz4PHF/LbU3XghGNI=
github.com/actions-runner-controller/httpcache v0.2.0/go.mod h1:JLu9/2M/btPz1Zu/vTZ71XzukQHn2YeISPmJoM5exBI=
github.com/actions/scaleset v0.1.1-0.20260218224657-feb84c6d04fb h1:9jQ9/kHm00UTvZf5MiQcZgIVounynwFEhh0wCV3Ts00=
github.com/actions/scaleset v0.1.1-0.20260218224657-feb84c6d04fb/go.mod h1:ncR5vzCCTUSyLgvclAtZ5dRBgF6qwA2nbTfTXmOJp84=
github.com/actions/scaleset v0.2.0 h1:CKsDtTjOBCwjyT4ikwiMykMttzuKejimWRAvVr8xj9w=
github.com/actions/scaleset v0.2.0/go.mod h1:ncR5vzCCTUSyLgvclAtZ5dRBgF6qwA2nbTfTXmOJp84=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I=

40
logger/logger.go Normal file
View File

@ -0,0 +1,40 @@
package logger
import (
"fmt"
"log/slog"
"os"
"strings"
)
// New creates new slog.Logger based on the format
func New(logLevel string, logFormat string) (*slog.Logger, error) {
var lvl slog.Level
switch strings.ToLower(logLevel) {
case "debug":
lvl = slog.LevelDebug
case "info":
lvl = slog.LevelInfo
case "warn":
lvl = slog.LevelWarn
case "error":
lvl = slog.LevelError
default:
return nil, fmt.Errorf("invalid log level: %s", logLevel)
}
switch logFormat {
case "json":
return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
Level: lvl,
})), nil
case "text":
return slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
Level: lvl,
})), nil
default:
return nil, fmt.Errorf("invalid log format: %s", logFormat)
}
}

View File

@ -22,21 +22,18 @@ const (
LogFormatJSON = "json"
)
var (
LogOpts = zap.Options{
TimeEncoder: zapcore.TimeEncoderOfLayout(time.RFC3339),
Development: true,
EncoderConfigOptions: []zap.EncoderConfigOption{
func(ec *zapcore.EncoderConfig) {
ec.LevelKey = "severity"
ec.MessageKey = "message"
},
var LogOpts = zap.Options{
TimeEncoder: zapcore.TimeEncoderOfLayout(time.RFC3339),
Development: true,
EncoderConfigOptions: []zap.EncoderConfigOption{
func(ec *zapcore.EncoderConfig) {
ec.LevelKey = "severity"
ec.MessageKey = "message"
},
}
)
},
}
func NewLogger(logLevel string, logFormat string) (logr.Logger, error) {
if !validLogFormat(logFormat) {
return logr.Logger{}, errors.New("invalid log format specified")
}

30
main.go
View File

@ -28,9 +28,11 @@ import (
"github.com/actions/actions-runner-controller/build"
actionsgithubcom "github.com/actions/actions-runner-controller/controllers/actions.github.com"
actionsgithubcommetrics "github.com/actions/actions-runner-controller/controllers/actions.github.com/metrics"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/multiclient"
"github.com/actions/actions-runner-controller/controllers/actions.github.com/secretresolver"
actionssummerwindnet "github.com/actions/actions-runner-controller/controllers/actions.summerwind.net"
"github.com/actions/actions-runner-controller/github"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/actions-runner-controller/logger"
"github.com/actions/actions-runner-controller/logging"
"github.com/kelseyhightower/envconfig"
corev1 "k8s.io/api/core/v1"
@ -85,7 +87,7 @@ func main() {
enableLeaderElection bool
disableAdmissionWebhook bool
updateStrategy string
leaderElectionId string
leaderElectionID string
port int
syncPeriod time.Duration
@ -121,7 +123,7 @@ func main() {
flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
"Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.")
flag.StringVar(&leaderElectionId, "leader-election-id", "actions-runner-controller", "Controller id for leader election.")
flag.StringVar(&leaderElectionID, "leader-election-id", "actions-runner-controller", "Controller id for leader election.")
flag.StringVar(&runnerPodDefaults.RunnerImage, "runner-image", defaultRunnerImage, "The image name of self-hosted runner container to use by default if one isn't defined in yaml.")
flag.StringVar(&runnerPodDefaults.DockerImage, "docker-image", defaultDockerImage, "The image name of docker sidecar container to use by default if one isn't defined in yaml.")
flag.StringVar(&runnerPodDefaults.DockerGID, "docker-gid", defaultDockerGID, "The default GID of docker group in the docker sidecar container. Use 1001 for dockerd sidecars of Ubuntu 20.04 runners 121 for Ubuntu 22.04 and 24.04.")
@ -239,7 +241,7 @@ func main() {
},
WebhookServer: webhookServer,
LeaderElection: enableLeaderElection,
LeaderElectionID: leaderElectionId,
LeaderElectionID: leaderElectionID,
Client: client.Options{
Cache: &client.CacheOptions{
DisableFor: []client.Object{
@ -270,13 +272,18 @@ func main() {
actionsgithubcommetrics.RegisterMetrics()
}
actionsMultiClient := actions.NewMultiClient(
log.WithName("actions-clients"),
)
slogLogger, err := logger.New(logLevel, logFormat)
if err != nil {
log.Error(err, "unable to create logger for secret resolver")
os.Exit(1)
}
secretResolver := actionsgithubcom.NewSecretResolver(
scalesetMultiClient := multiclient.NewScaleset()
secretResolver := secretresolver.New(
mgr.GetClient(),
actionsMultiClient,
scalesetMultiClient,
secretresolver.WithLogger(slogLogger),
)
rb := actionsgithubcom.ResourceBuilder{
@ -292,7 +299,6 @@ func main() {
Scheme: mgr.GetScheme(),
ControllerNamespace: managerNamespace,
DefaultRunnerScaleSetListenerImage: managerImage,
ActionsClient: actionsMultiClient,
UpdateStrategy: actionsgithubcom.UpdateStrategy(updateStrategy),
DefaultRunnerScaleSetListenerImagePullSecrets: autoScalerImagePullSecrets,
ResourceBuilder: rb,
@ -399,7 +405,7 @@ func main() {
"default-docker-gid", runnerPodDefaults.DockerGID,
"common-runnner-labels", commonRunnerLabels,
"leader-election-enabled", enableLeaderElection,
"leader-election-id", leaderElectionId,
"leader-election-id", leaderElectionID,
"watch-namespace", namespace,
)
@ -489,7 +495,7 @@ func (s *commaSeparatedStringSlice) String() string {
}
func (s *commaSeparatedStringSlice) Set(value string) error {
for _, v := range strings.Split(value, ",") {
for v := range strings.SplitSeq(value, ",") {
if v == "" {
continue
}