Refactor secret resolver implementation, add vault type and modify autoscaling listener controller to search for app config instead of secret

This commit is contained in:
Nikola Jokic 2025-06-05 09:15:46 +02:00
parent 74ed8e6838
commit 379f466d8f
No known key found for this signature in database
GPG Key ID: E4104494F9B8DDF6
14 changed files with 181 additions and 82 deletions

View File

@ -20,10 +20,13 @@ import (
) )
type Config struct { type Config struct {
ConfigureUrl string `json:"configure_url"` ConfigureUrl string `json:"configure_url"`
VaultType string `json:"vault_type"` VaultType vault.VaultType `json:"vault_type"`
VaultLookupKey string `json:"vault_lookup_key"` VaultLookupKey string `json:"vault_lookup_key"`
appconfig.AppConfig // AppConfig contains the GitHub App configuration.
// It is initially set to nil if VaultType is set.
// Otherwise, it is populated with the GitHub App credentials from the GitHub secret.
*appconfig.AppConfig
EphemeralRunnerSetNamespace string `json:"ephemeral_runner_set_namespace"` EphemeralRunnerSetNamespace string `json:"ephemeral_runner_set_namespace"`
EphemeralRunnerSetName string `json:"ephemeral_runner_set_name"` EphemeralRunnerSetName string `json:"ephemeral_runner_set_name"`
MaxRunners int `json:"max_runners"` MaxRunners int `json:"max_runners"`
@ -82,7 +85,7 @@ func Read(ctx context.Context, configPath string) (*Config, error) {
return nil, fmt.Errorf("failed to read app config from string: %v", err) return nil, fmt.Errorf("failed to read app config from string: %v", err)
} }
config.AppConfig = *appConfig config.AppConfig = appConfig
if err := config.Validate(); err != nil { if err := config.Validate(); err != nil {
return nil, fmt.Errorf("config validation failed: %w", err) return nil, fmt.Errorf("config validation failed: %w", err)
@ -109,8 +112,14 @@ func (c *Config) Validate() error {
return fmt.Errorf(`MinRunners "%d" cannot be greater than MaxRunners "%d"`, c.MinRunners, c.MaxRunners) return fmt.Errorf(`MinRunners "%d" cannot be greater than MaxRunners "%d"`, c.MinRunners, c.MaxRunners)
} }
if err := c.AppConfig.Validate(); err != nil { if c.VaultType != "" && c.VaultLookupKey == "" {
return fmt.Errorf("AppConfig validation failed: %w", err) return fmt.Errorf("VaultLookupKey is required when VaultType is set to %q", c.VaultType)
}
if c.VaultType != "" && c.VaultLookupKey == "" {
if err := c.AppConfig.Validate(); err != nil {
return fmt.Errorf("AppConfig validation failed: %w", err)
}
} }
return nil return nil

View File

@ -54,7 +54,7 @@ func TestCustomerServerRootCA(t *testing.T) {
config := config.Config{ config := config.Config{
ConfigureUrl: server.ConfigURLForOrg("myorg"), ConfigureUrl: server.ConfigURLForOrg("myorg"),
ServerRootCA: certsString, ServerRootCA: certsString,
AppConfig: appconfig.AppConfig{ AppConfig: &appconfig.AppConfig{
Token: "token", Token: "token",
}, },
} }
@ -83,7 +83,7 @@ func TestProxySettings(t *testing.T) {
config := config.Config{ config := config.Config{
ConfigureUrl: "https://github.com/org/repo", ConfigureUrl: "https://github.com/org/repo",
AppConfig: appconfig.AppConfig{ AppConfig: &appconfig.AppConfig{
Token: "token", Token: "token",
}, },
} }
@ -115,7 +115,7 @@ func TestProxySettings(t *testing.T) {
config := config.Config{ config := config.Config{
ConfigureUrl: "https://github.com/org/repo", ConfigureUrl: "https://github.com/org/repo",
AppConfig: appconfig.AppConfig{ AppConfig: &appconfig.AppConfig{
Token: "token", Token: "token",
}, },
} }
@ -152,7 +152,7 @@ func TestProxySettings(t *testing.T) {
config := config.Config{ config := config.Config{
ConfigureUrl: "https://github.com/org/repo", ConfigureUrl: "https://github.com/org/repo",
AppConfig: appconfig.AppConfig{ AppConfig: &appconfig.AppConfig{
Token: "token", Token: "token",
}, },
} }

View File

@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig" "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/actions/actions-runner-controller/vault"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -16,7 +17,7 @@ func TestConfigValidationMinMax(t *testing.T) {
RunnerScaleSetId: 1, RunnerScaleSetId: 1,
MinRunners: 5, MinRunners: 5,
MaxRunners: 2, MaxRunners: 2,
AppConfig: appconfig.AppConfig{ AppConfig: &appconfig.AppConfig{
Token: "token", Token: "token",
}, },
} }
@ -42,7 +43,7 @@ func TestConfigValidationAppKey(t *testing.T) {
t.Run("app id integer", func(t *testing.T) { t.Run("app id integer", func(t *testing.T) {
t.Parallel() t.Parallel()
config := &Config{ config := &Config{
AppConfig: appconfig.AppConfig{ AppConfig: &appconfig.AppConfig{
AppID: "1", AppID: "1",
AppInstallationID: 10, AppInstallationID: 10,
}, },
@ -59,7 +60,7 @@ func TestConfigValidationAppKey(t *testing.T) {
t.Run("app id as client id", func(t *testing.T) { t.Run("app id as client id", func(t *testing.T) {
t.Parallel() t.Parallel()
config := &Config{ config := &Config{
AppConfig: appconfig.AppConfig{ AppConfig: &appconfig.AppConfig{
AppID: "Iv23f8doAlphaNumer1c", AppID: "Iv23f8doAlphaNumer1c",
AppInstallationID: 10, AppInstallationID: 10,
}, },
@ -76,7 +77,7 @@ func TestConfigValidationAppKey(t *testing.T) {
func TestConfigValidationOnlyOneTypeOfCredentials(t *testing.T) { func TestConfigValidationOnlyOneTypeOfCredentials(t *testing.T) {
config := &Config{ config := &Config{
AppConfig: appconfig.AppConfig{ AppConfig: &appconfig.AppConfig{
AppID: "1", AppID: "1",
AppInstallationID: 10, AppInstallationID: 10,
AppPrivateKey: "asdf", AppPrivateKey: "asdf",
@ -100,7 +101,7 @@ func TestConfigValidation(t *testing.T) {
RunnerScaleSetId: 1, RunnerScaleSetId: 1,
MinRunners: 1, MinRunners: 1,
MaxRunners: 5, MaxRunners: 5,
AppConfig: appconfig.AppConfig{ AppConfig: &appconfig.AppConfig{
Token: "asdf", Token: "asdf",
}, },
} }
@ -121,3 +122,50 @@ func TestConfigValidationConfigUrl(t *testing.T) {
assert.ErrorContains(t, err, "GitHubConfigUrl is not provided", "Expected error about missing ConfigureUrl") assert.ErrorContains(t, err, "GitHubConfigUrl is not provided", "Expected error about missing ConfigureUrl")
} }
func TestConfigValidationWithVaultConfig(t *testing.T) {
t.Run("valid", func(t *testing.T) {
config := &Config{
ConfigureUrl: "https://github.com/actions",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
MinRunners: 1,
MaxRunners: 5,
VaultType: vault.VaultTypeAzureKeyVault,
VaultLookupKey: "testkey",
}
err := config.Validate()
assert.NoError(t, err, "Expected no error for valid vault type")
})
t.Run("invalid vault type", func(t *testing.T) {
config := &Config{
ConfigureUrl: "https://github.com/actions",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
MinRunners: 1,
MaxRunners: 5,
VaultType: vault.VaultType("invalid_vault_type"),
VaultLookupKey: "testkey",
}
err := config.Validate()
assert.ErrorContains(t, err, `unknown vault type: "invalid_vault_type"`, "Expected error for invalid vault type")
})
t.Run("vault type set without lookup key", func(t *testing.T) {
config := &Config{
ConfigureUrl: "https://github.com/actions",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
MinRunners: 1,
MaxRunners: 5,
VaultType: vault.VaultTypeAzureKeyVault,
VaultLookupKey: "",
}
err := config.Validate()
assert.ErrorContains(t, err, `vault type set to "invalid_vault_type", but lookup key is empty`, "Expected error for vault type without lookup key")
})
}

View File

@ -32,6 +32,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/reconcile"
v1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1" v1alpha1 "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/controllers/actions.github.com/metrics" "github.com/actions/actions-runner-controller/controllers/actions.github.com/metrics"
"github.com/actions/actions-runner-controller/github/actions" "github.com/actions/actions-runner-controller/github/actions"
hash "github.com/actions/actions-runner-controller/hash" hash "github.com/actions/actions-runner-controller/hash"
@ -128,12 +129,16 @@ func (r *AutoscalingListenerReconciler) Reconcile(ctx context.Context, req ctrl.
return ctrl.Result{}, err return ctrl.Result{}, err
} }
// Check if the GitHub config secret exists appConfig, err := r.GetAppConfig(ctx, &autoscalingRunnerSet)
secret := new(corev1.Secret) if err != nil {
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, Name: autoscalingListener.Spec.GitHubConfigSecret}, secret); err != nil { log.Error(
log.Error(err, "Failed to find GitHub config secret.", err,
"namespace", autoscalingListener.Spec.AutoscalingRunnerSetNamespace, "Failed to get app config for AutoscalingRunnerSet.",
"name", autoscalingListener.Spec.GitHubConfigSecret) "namespace",
autoscalingRunnerSet.Namespace,
"name",
autoscalingRunnerSet.GitHubConfigSecret,
)
return ctrl.Result{}, err return ctrl.Result{}, err
} }
@ -218,7 +223,7 @@ func (r *AutoscalingListenerReconciler) Reconcile(ctx context.Context, req ctrl.
// Create a listener pod in the controller namespace // Create a listener pod in the controller namespace
log.Info("Creating a listener pod") log.Info("Creating a listener pod")
return r.createListenerPod(ctx, &autoscalingRunnerSet, autoscalingListener, serviceAccount, secret, log) return r.createListenerPod(ctx, &autoscalingRunnerSet, autoscalingListener, serviceAccount, appConfig, log)
} }
cs := listenerContainerStatus(listenerPod) cs := listenerContainerStatus(listenerPod)
@ -377,7 +382,7 @@ func (r *AutoscalingListenerReconciler) createServiceAccountForListener(ctx cont
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
func (r *AutoscalingListenerReconciler) createListenerPod(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, autoscalingListener *v1alpha1.AutoscalingListener, serviceAccount *corev1.ServiceAccount, secret *corev1.Secret, logger logr.Logger) (ctrl.Result, error) { func (r *AutoscalingListenerReconciler) createListenerPod(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, autoscalingListener *v1alpha1.AutoscalingListener, serviceAccount *corev1.ServiceAccount, appConfig *appconfig.AppConfig, logger logr.Logger) (ctrl.Result, error) {
var envs []corev1.EnvVar var envs []corev1.EnvVar
if autoscalingListener.Spec.Proxy != nil { if autoscalingListener.Spec.Proxy != nil {
httpURL := corev1.EnvVar{ httpURL := corev1.EnvVar{
@ -446,7 +451,7 @@ func (r *AutoscalingListenerReconciler) createListenerPod(ctx context.Context, a
logger.Info("Creating listener config secret") logger.Info("Creating listener config secret")
podConfig, err := r.newScaleSetListenerConfig(autoscalingListener, secret, metricsConfig, cert) podConfig, err := r.newScaleSetListenerConfig(autoscalingListener, appConfig, metricsConfig, cert)
if err != nil { if err != nil {
logger.Error(err, "Failed to build listener config secret") logger.Error(err, "Failed to build listener config secret")
return ctrl.Result{}, err return ctrl.Result{}, err

View File

@ -394,7 +394,7 @@ func (r *AutoscalingRunnerSetReconciler) removeFinalizersFromDependentResources(
func (r *AutoscalingRunnerSetReconciler) createRunnerScaleSet(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, logger logr.Logger) (ctrl.Result, error) { func (r *AutoscalingRunnerSetReconciler) createRunnerScaleSet(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, logger logr.Logger) (ctrl.Result, error) {
logger.Info("Creating a new runner scale set") logger.Info("Creating a new runner scale set")
actionsClient, err := r.ActionsClientPool.Get(ctx, autoscalingRunnerSet) actionsClient, err := r.SecretResolver.GetActionsService(ctx, autoscalingRunnerSet)
if len(autoscalingRunnerSet.Spec.RunnerScaleSetName) == 0 { if len(autoscalingRunnerSet.Spec.RunnerScaleSetName) == 0 {
autoscalingRunnerSet.Spec.RunnerScaleSetName = autoscalingRunnerSet.Name autoscalingRunnerSet.Spec.RunnerScaleSetName = autoscalingRunnerSet.Name
} }
@ -490,7 +490,7 @@ func (r *AutoscalingRunnerSetReconciler) updateRunnerScaleSetRunnerGroup(ctx con
return ctrl.Result{}, err return ctrl.Result{}, err
} }
actionsClient, err := r.ActionsClientPool.Get(ctx, autoscalingRunnerSet) actionsClient, err := r.SecretResolver.GetActionsService(ctx, autoscalingRunnerSet)
if err != nil { if err != nil {
logger.Error(err, "Failed to initialize Actions service client for updating a existing runner scale set") logger.Error(err, "Failed to initialize Actions service client for updating a existing runner scale set")
return ctrl.Result{}, err return ctrl.Result{}, err
@ -538,7 +538,7 @@ func (r *AutoscalingRunnerSetReconciler) updateRunnerScaleSetName(ctx context.Co
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
actionsClient, err := r.ActionsClientPool.Get(ctx, autoscalingRunnerSet) actionsClient, err := r.SecretResolver.GetActionsService(ctx, autoscalingRunnerSet)
if err != nil { if err != nil {
logger.Error(err, "Failed to initialize Actions service client for updating a existing runner scale set") logger.Error(err, "Failed to initialize Actions service client for updating a existing runner scale set")
return ctrl.Result{}, err return ctrl.Result{}, err
@ -589,7 +589,7 @@ func (r *AutoscalingRunnerSetReconciler) deleteRunnerScaleSet(ctx context.Contex
return nil return nil
} }
actionsClient, err := r.ActionsClientPool.Get(ctx, autoscalingRunnerSet) actionsClient, err := r.SecretResolver.GetActionsService(ctx, autoscalingRunnerSet)
if err != nil { if err != nil {
logger.Error(err, "Failed to initialize Actions service client for updating a existing runner scale set") logger.Error(err, "Failed to initialize Actions service client for updating a existing runner scale set")
return err return err

View File

@ -71,7 +71,7 @@ var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() {
ControllerNamespace: autoscalingNS.Name, ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ResourceBuilder: ResourceBuilder{ ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: k8sClient, k8sClient: k8sClient,
multiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },
@ -711,7 +711,7 @@ var _ = Describe("Test AutoScalingController updates", Ordered, func() {
ControllerNamespace: autoscalingNS.Name, ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ResourceBuilder: ResourceBuilder{ ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: k8sClient, k8sClient: k8sClient,
multiClient: multiClient, multiClient: multiClient,
}, },
@ -831,7 +831,7 @@ var _ = Describe("Test AutoscalingController creation failures", Ordered, func()
ControllerNamespace: autoscalingNS.Name, ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ResourceBuilder: ResourceBuilder{ ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: k8sClient, k8sClient: k8sClient,
multiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },
@ -962,7 +962,7 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
ControllerNamespace: autoscalingNS.Name, ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ResourceBuilder: ResourceBuilder{ ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: k8sClient, k8sClient: k8sClient,
multiClient: multiClient, multiClient: multiClient,
}, },
@ -1150,7 +1150,7 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
ControllerNamespace: autoscalingNS.Name, ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ResourceBuilder: ResourceBuilder{ ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: k8sClient, k8sClient: k8sClient,
multiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },
@ -1163,7 +1163,7 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
}) })
It("should be able to make requests to a server using root CAs", func() { It("should be able to make requests to a server using root CAs", func() {
controller.ActionsClientPool = &ActionsClientPool{ controller.SecretResolver = &SecretResolver{
k8sClient: k8sClient, k8sClient: k8sClient,
multiClient: actions.NewMultiClient(logr.Discard()), multiClient: actions.NewMultiClient(logr.Discard()),
} }
@ -1392,7 +1392,7 @@ var _ = Describe("Test external permissions cleanup", Ordered, func() {
ControllerNamespace: autoscalingNS.Name, ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ResourceBuilder: ResourceBuilder{ ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: k8sClient, k8sClient: k8sClient,
multiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },
@ -1555,7 +1555,7 @@ var _ = Describe("Test external permissions cleanup", Ordered, func() {
ControllerNamespace: autoscalingNS.Name, ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ResourceBuilder: ResourceBuilder{ ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: k8sClient, k8sClient: k8sClient,
multiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },
@ -1768,7 +1768,7 @@ var _ = Describe("Test resource version and build version mismatch", func() {
ControllerNamespace: autoscalingNS.Name, ControllerNamespace: autoscalingNS.Name,
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc", DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
ResourceBuilder: ResourceBuilder{ ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: k8sClient, k8sClient: k8sClient,
multiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },

View File

@ -528,7 +528,7 @@ func (r *EphemeralRunnerReconciler) deletePodAsFailed(ctx context.Context, ephem
func (r *EphemeralRunnerReconciler) updateStatusWithRunnerConfig(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) (*ctrl.Result, error) { func (r *EphemeralRunnerReconciler) updateStatusWithRunnerConfig(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) (*ctrl.Result, error) {
// Runner is not registered with the service. We need to register it first // Runner is not registered with the service. We need to register it first
log.Info("Creating ephemeral runner JIT config") log.Info("Creating ephemeral runner JIT config")
actionsClient, err := r.ActionsClientPool.Get(ctx, ephemeralRunner) actionsClient, err := r.SecretResolver.GetActionsService(ctx, ephemeralRunner)
if err != nil { if err != nil {
return &ctrl.Result{}, fmt.Errorf("failed to get actions client for generating JIT config: %w", err) return &ctrl.Result{}, fmt.Errorf("failed to get actions client for generating JIT config: %w", err)
} }
@ -755,7 +755,7 @@ func (r *EphemeralRunnerReconciler) updateRunStatusFromPod(ctx context.Context,
// runnerRegisteredWithService checks if the runner is still registered with the service // runnerRegisteredWithService checks if the runner is still registered with the service
// Returns found=false and err=nil if ephemeral runner does not exist in GitHub service and should be deleted // Returns found=false and err=nil if ephemeral runner does not exist in GitHub service and should be deleted
func (r EphemeralRunnerReconciler) runnerRegisteredWithService(ctx context.Context, runner *v1alpha1.EphemeralRunner, log logr.Logger) (found bool, err error) { func (r EphemeralRunnerReconciler) runnerRegisteredWithService(ctx context.Context, runner *v1alpha1.EphemeralRunner, log logr.Logger) (found bool, err error) {
actionsClient, err := r.ActionsClientPool.Get(ctx, runner) actionsClient, err := r.SecretResolver.GetActionsService(ctx, runner)
if err != nil { if err != nil {
return false, fmt.Errorf("failed to get Actions client for ScaleSet: %w", err) return false, fmt.Errorf("failed to get Actions client for ScaleSet: %w", err)
} }
@ -782,7 +782,7 @@ func (r EphemeralRunnerReconciler) runnerRegisteredWithService(ctx context.Conte
} }
func (r *EphemeralRunnerReconciler) deleteRunnerFromService(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) error { func (r *EphemeralRunnerReconciler) deleteRunnerFromService(ctx context.Context, ephemeralRunner *v1alpha1.EphemeralRunner, log logr.Logger) error {
client, err := r.ActionsClientPool.Get(ctx, ephemeralRunner) client, err := r.SecretResolver.GetActionsService(ctx, ephemeralRunner)
if err != nil { if err != nil {
return fmt.Errorf("failed to get actions client for runner: %w", err) return fmt.Errorf("failed to get actions client for runner: %w", err)
} }

View File

@ -111,7 +111,7 @@ var _ = Describe("EphemeralRunner", func() {
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ResourceBuilder: ResourceBuilder{ ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(), k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },
@ -795,7 +795,7 @@ var _ = Describe("EphemeralRunner", func() {
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ResourceBuilder: ResourceBuilder{ ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(), k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient( multiClient: fake.NewMultiClient(
fake.WithDefaultClient( fake.WithDefaultClient(
@ -875,7 +875,7 @@ var _ = Describe("EphemeralRunner", func() {
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ResourceBuilder: ResourceBuilder{ ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(), k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },
@ -890,7 +890,7 @@ var _ = Describe("EphemeralRunner", func() {
It("uses an actions client with proxy transport", func() { It("uses an actions client with proxy transport", func() {
// Use an actual client // Use an actual client
controller.ResourceBuilder = ResourceBuilder{ controller.ResourceBuilder = ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(), k8sClient: mgr.GetClient(),
multiClient: actions.NewMultiClient(logr.Discard()), multiClient: actions.NewMultiClient(logr.Discard()),
}, },
@ -1049,7 +1049,7 @@ var _ = Describe("EphemeralRunner", func() {
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ResourceBuilder: ResourceBuilder{ ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(), k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },
@ -1085,7 +1085,7 @@ var _ = Describe("EphemeralRunner", func() {
// Use an actual client // Use an actual client
controller.ResourceBuilder = ResourceBuilder{ controller.ResourceBuilder = ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(), k8sClient: mgr.GetClient(),
multiClient: actions.NewMultiClient(logr.Discard()), multiClient: actions.NewMultiClient(logr.Discard()),
}, },

View File

@ -331,7 +331,7 @@ func (r *EphemeralRunnerSetReconciler) cleanUpEphemeralRunners(ctx context.Conte
return false, nil return false, nil
} }
actionsClient, err := r.ActionsClientPool.Get(ctx, ephemeralRunnerSet) actionsClient, err := r.SecretResolver.GetActionsService(ctx, ephemeralRunnerSet)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -439,7 +439,7 @@ func (r *EphemeralRunnerSetReconciler) deleteIdleEphemeralRunners(ctx context.Co
log.Info("No pending or running ephemeral runners running at this time for scale down") log.Info("No pending or running ephemeral runners running at this time for scale down")
return nil return nil
} }
actionsClient, err := r.ActionsClientPool.Get(ctx, ephemeralRunnerSet) actionsClient, err := r.SecretResolver.GetActionsService(ctx, ephemeralRunnerSet)
if err != nil { if err != nil {
return fmt.Errorf("failed to create actions client for ephemeral runner replica set: %w", err) return fmt.Errorf("failed to create actions client for ephemeral runner replica set: %w", err)
} }

View File

@ -58,7 +58,7 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ResourceBuilder: ResourceBuilder{ ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(), k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },
@ -1113,7 +1113,7 @@ var _ = Describe("Test EphemeralRunnerSet controller with proxy settings", func(
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ResourceBuilder: ResourceBuilder{ ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(), k8sClient: mgr.GetClient(),
multiClient: actions.NewMultiClient(logr.Discard()), multiClient: actions.NewMultiClient(logr.Discard()),
}, },
@ -1417,7 +1417,7 @@ var _ = Describe("Test EphemeralRunnerSet controller with custom root CA", func(
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ResourceBuilder: ResourceBuilder{ ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{ SecretResolver: &SecretResolver{
k8sClient: mgr.GetClient(), k8sClient: mgr.GetClient(),
multiClient: actions.NewMultiClient(logr.Discard()), multiClient: actions.NewMultiClient(logr.Discard()),
}, },

View File

@ -18,6 +18,7 @@ import (
"github.com/actions/actions-runner-controller/github/actions" "github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/actions-runner-controller/hash" "github.com/actions/actions-runner-controller/hash"
"github.com/actions/actions-runner-controller/logging" "github.com/actions/actions-runner-controller/logging"
"github.com/actions/actions-runner-controller/vault"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1" rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -73,7 +74,7 @@ func SetListenerEntrypoint(entrypoint string) {
type ResourceBuilder struct { type ResourceBuilder struct {
ExcludeLabelPropagationPrefixes []string ExcludeLabelPropagationPrefixes []string
*ActionsClientPool *SecretResolver
} }
// boolPtr returns a pointer to a bool value // boolPtr returns a pointer to a bool value
@ -166,7 +167,7 @@ func (lm *listenerMetricsServerConfig) containerPort() (corev1.ContainerPort, er
}, nil }, nil
} }
func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha1.AutoscalingListener, secret *corev1.Secret, metricsConfig *listenerMetricsServerConfig, cert string) (*corev1.Secret, error) { func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha1.AutoscalingListener, appConfig *appconfig.AppConfig, metricsConfig *listenerMetricsServerConfig, cert string) (*corev1.Secret, error) {
var ( var (
metricsAddr = "" metricsAddr = ""
metricsEndpoint = "" metricsEndpoint = ""
@ -193,13 +194,13 @@ func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha
} }
if ty, ok := autoscalingListener.Annotations[AnnotationKeyGitHubVaultType]; !ok { if ty, ok := autoscalingListener.Annotations[AnnotationKeyGitHubVaultType]; !ok {
appConfig, err := appconfig.FromSecret(secret) config.AppConfig = appConfig
if err != nil {
return nil, fmt.Errorf("failed to parse appconfig from secret: %v", err)
}
config.AppConfig = *appConfig
} else { } else {
config.VaultType = ty vaultType := vault.VaultType(ty)
if err := vaultType.Validate(); err != nil {
return nil, fmt.Errorf("vault type validation error: %w", err)
}
config.VaultType = vaultType
config.VaultLookupKey = autoscalingListener.Spec.GitHubConfigSecret config.VaultLookupKey = autoscalingListener.Spec.GitHubConfigSecret
} }

View File

@ -18,29 +18,29 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
) )
type ActionsClientPool struct { type SecretResolver struct {
k8sClient client.Client k8sClient client.Client
vaultResolvers map[string]resolver vaultResolvers map[vault.VaultType]resolver
multiClient actions.MultiClient multiClient actions.MultiClient
} }
type ActionsClientPoolOption func(*ActionsClientPool) type SecretResolverOption func(*SecretResolver)
func WithVault(ty string, vault vault.Vault) ActionsClientPoolOption { func WithVault(ty vault.VaultType, vault vault.Vault) SecretResolverOption {
return func(pool *ActionsClientPool) { return func(pool *SecretResolver) {
pool.vaultResolvers[ty] = &vaultResolver{vault} pool.vaultResolvers[ty] = &vaultResolver{vault}
} }
} }
func NewActionsClientPool(k8sClient client.Client, multiClient actions.MultiClient, opts ...ActionsClientPoolOption) *ActionsClientPool { func NewSecretResolver(k8sClient client.Client, multiClient actions.MultiClient, opts ...SecretResolverOption) *SecretResolver {
if k8sClient == nil { if k8sClient == nil {
panic("k8sClient must not be nil") panic("k8sClient must not be nil")
} }
pool := &ActionsClientPool{ pool := &SecretResolver{
k8sClient: k8sClient, k8sClient: k8sClient,
multiClient: multiClient, multiClient: multiClient,
vaultResolvers: make(map[string]resolver), vaultResolvers: make(map[vault.VaultType]resolver),
} }
for _, opt := range opts { for _, opt := range opts {
@ -58,8 +58,22 @@ type ActionsGitHubObject interface {
GitHubServerTLS() *v1alpha1.GitHubServerTLSConfig GitHubServerTLS() *v1alpha1.GitHubServerTLSConfig
} }
func (p *ActionsClientPool) Get(ctx context.Context, obj ActionsGitHubObject) (actions.ActionsService, error) { func (sr *SecretResolver) GetAppConfig(ctx context.Context, obj ActionsGitHubObject) (*appconfig.AppConfig, error) {
resolver, err := p.resolverForObject(obj) resolver, err := sr.resolverForObject(obj)
if err != nil {
return nil, fmt.Errorf("failed to get resolver for object: %v", err)
}
appConfig, err := resolver.appConfig(ctx, obj.GitHubConfigSecret())
if err != nil {
return nil, fmt.Errorf("failed to resolve app config: %v", err)
}
return appConfig, nil
}
func (sr *SecretResolver) GetActionsService(ctx context.Context, obj ActionsGitHubObject) (actions.ActionsService, error) {
resolver, err := sr.resolverForObject(obj)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get resolver for object: %v", err) return nil, fmt.Errorf("failed to get resolver for object: %v", err)
} }
@ -118,7 +132,7 @@ func (p *ActionsClientPool) Get(ctx context.Context, obj ActionsGitHubObject) (a
if tlsConfig != nil { if tlsConfig != nil {
pool, err := tlsConfig.ToCertPool(func(name, key string) ([]byte, error) { pool, err := tlsConfig.ToCertPool(func(name, key string) ([]byte, error) {
var configmap corev1.ConfigMap var configmap corev1.ConfigMap
err := p.k8sClient.Get( err := sr.k8sClient.Get(
ctx, ctx,
types.NamespacedName{ types.NamespacedName{
Namespace: obj.GetNamespace(), Namespace: obj.GetNamespace(),
@ -139,7 +153,7 @@ func (p *ActionsClientPool) Get(ctx context.Context, obj ActionsGitHubObject) (a
clientOptions = append(clientOptions, actions.WithRootCAs(pool)) clientOptions = append(clientOptions, actions.WithRootCAs(pool))
} }
return p.multiClient.GetClientFor( return sr.multiClient.GetClientFor(
ctx, ctx,
obj.GitHubConfigUrl(), obj.GitHubConfigUrl(),
appConfig, appConfig,
@ -148,7 +162,7 @@ func (p *ActionsClientPool) Get(ctx context.Context, obj ActionsGitHubObject) (a
) )
} }
func (p *ActionsClientPool) resolverForObject(obj ActionsGitHubObject) (resolver, error) { func (p *SecretResolver) resolverForObject(obj ActionsGitHubObject) (resolver, error) {
ty, ok := obj.GetAnnotations()[AnnotationKeyGitHubVaultType] ty, ok := obj.GetAnnotations()[AnnotationKeyGitHubVaultType]
if !ok { if !ok {
return &k8sResolver{ return &k8sResolver{
@ -157,7 +171,12 @@ func (p *ActionsClientPool) resolverForObject(obj ActionsGitHubObject) (resolver
}, nil }, nil
} }
vault, ok := p.vaultResolvers[ty] vaultType := vault.VaultType(ty)
if err := vaultType.Validate(); err != nil {
return nil, fmt.Errorf("invalid vault type %q: %v", ty, err)
}
vault, ok := p.vaultResolvers[vaultType]
if !ok { if !ok {
return nil, fmt.Errorf("unknown vault resolver %q", ty) return nil, fmt.Errorf("unknown vault resolver %q", ty)
} }

10
main.go
View File

@ -281,20 +281,20 @@ func main() {
os.Exit(1) os.Exit(1)
} }
var poolOptions []actionsgithubcom.ActionsClientPoolOption var secretResolverOptions []actionsgithubcom.SecretResolverOption
for name, vault := range vaults { for name, vault := range vaults {
poolOptions = append(poolOptions, actionsgithubcom.WithVault(name, vault)) secretResolverOptions = append(secretResolverOptions, actionsgithubcom.WithVault(name, vault))
} }
clientPool := actionsgithubcom.NewActionsClientPool( secretResolver := actionsgithubcom.NewSecretResolver(
mgr.GetClient(), mgr.GetClient(),
actionsMultiClient, actionsMultiClient,
poolOptions..., secretResolverOptions...,
) )
rb := actionsgithubcom.ResourceBuilder{ rb := actionsgithubcom.ResourceBuilder{
ExcludeLabelPropagationPrefixes: excludeLabelPropagationPrefixes, ExcludeLabelPropagationPrefixes: excludeLabelPropagationPrefixes,
ActionsClientPool: clientPool, SecretResolver: secretResolver,
} }
log.Info("Resource builder initializing") log.Info("Resource builder initializing")

View File

@ -14,11 +14,28 @@ type Vault interface {
GetSecret(ctx context.Context, name string) (string, error) GetSecret(ctx context.Context, name string) (string, error)
} }
// VaultType represents the type of vault that can be used in the application.
// It is used to identify which vault integration should be used to resolve secrets.
type VaultType string
// VaultType is the type of vault supported // VaultType is the type of vault supported
const ( const (
VaultTypeAzureKeyVault = "azure_key_vault" VaultTypeAzureKeyVault VaultType = "azure_key_vault"
) )
func (t VaultType) String() string {
return string(t)
}
func (t VaultType) Validate() error {
switch t {
case VaultTypeAzureKeyVault:
return nil
default:
return fmt.Errorf("unknown vault type: %q", t)
}
}
// Compile-time checks // Compile-time checks
var _ Vault = (*azurekeyvault.AzureKeyVault)(nil) var _ Vault = (*azurekeyvault.AzureKeyVault)(nil)
@ -31,10 +48,10 @@ var _ Vault = (*azurekeyvault.AzureKeyVault)(nil)
// //
// For example, listener has prefix "LISTENER_", has "AZURE_KEY_VAULT_" configured, // For example, listener has prefix "LISTENER_", has "AZURE_KEY_VAULT_" configured,
// and should read the vault URL. The environment variable will be "LISTENER_AZURE_KEY_VAULT_URL". // and should read the vault URL. The environment variable will be "LISTENER_AZURE_KEY_VAULT_URL".
func InitAll(prefix string) (map[string]Vault, error) { func InitAll(prefix string) (map[VaultType]Vault, error) {
envs := os.Environ() envs := os.Environ()
result := make(map[string]Vault) result := make(map[VaultType]Vault)
for _, env := range envs { for _, env := range envs {
if strings.HasPrefix(env, prefix+"AZURE_KEY_VAULT_") { if strings.HasPrefix(env, prefix+"AZURE_KEY_VAULT_") {
akv, err := azurekeyvault.FromEnv(prefix + "AZURE_KEY_VAULT_") akv, err := azurekeyvault.FromEnv(prefix + "AZURE_KEY_VAULT_")