Adding config read to the ghalistener

This commit is contained in:
Nikola Jokic 2025-01-15 11:58:58 +01:00
parent 3e97c05e0f
commit 25b32797ea
No known key found for this signature in database
GPG Key ID: E4104494F9B8DDF6
41 changed files with 1503 additions and 648 deletions

View File

@ -0,0 +1,86 @@
package appconfig
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
corev1 "k8s.io/api/core/v1"
)
type AppConfig struct {
AppID string `json:"github_app_id"`
AppInstallationID int64 `json:"github_app_installation_id"`
AppPrivateKey string `json:"github_app_private_key"`
Token string `json:"github_token"`
}
func (c *AppConfig) tidy() *AppConfig {
if len(c.Token) > 0 {
return &AppConfig{
Token: c.Token,
}
}
return &AppConfig{
AppID: c.AppID,
AppInstallationID: c.AppInstallationID,
AppPrivateKey: c.AppPrivateKey,
}
}
func (c *AppConfig) Validate() error {
hasToken := len(c.Token) > 0
hasGitHubAppAuth := c.hasGitHubAppAuth()
if hasToken && hasGitHubAppAuth {
return fmt.Errorf("both PAT and GitHub App credentials provided. should only provide one")
}
if !hasToken && !hasGitHubAppAuth {
return fmt.Errorf("no credentials provided: either a PAT or GitHub App credentials should be provided")
}
return nil
}
func (c *AppConfig) hasGitHubAppAuth() bool {
return len(c.AppID) > 0 && c.AppInstallationID > 0 && len(c.AppPrivateKey) > 0
}
func FromSecret(secret *corev1.Secret) (*AppConfig, error) {
var appInstallationID int64
if v := string(secret.Data["github_app_installation_id"]); v != "" {
val, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return nil, err
}
appInstallationID = val
}
cfg := &AppConfig{
Token: string(secret.Data["github_token"]),
AppID: string(secret.Data["github_app_id"]),
AppInstallationID: appInstallationID,
AppPrivateKey: string(secret.Data["github_app_private_key"]),
}
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate config: %v", err)
}
return cfg.tidy(), nil
}
func FromString(v string) (*AppConfig, error) {
var appConfig AppConfig
if err := json.NewDecoder(bytes.NewBufferString(v)).Decode(&appConfig); err != nil {
return nil, err
}
if err := appConfig.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate app config decoded from string: %w", err)
}
return appConfig.tidy(), nil
}

View File

@ -0,0 +1,152 @@
package appconfig
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
)
func TestAppConfigValidate_invalid(t *testing.T) {
tt := map[string]*AppConfig{
"empty": {},
"token and app config": {
AppID: "1",
AppInstallationID: 2,
AppPrivateKey: "private key",
Token: "token",
},
"app id not set": {
AppInstallationID: 2,
AppPrivateKey: "private key",
},
"app installation id not set": {
AppID: "2",
AppPrivateKey: "private key",
},
"private key empty": {
AppID: "2",
AppInstallationID: 1,
AppPrivateKey: "",
},
}
for name, cfg := range tt {
t.Run(name, func(t *testing.T) {
err := cfg.Validate()
require.Error(t, err)
})
}
}
func TestAppConfigValidate_valid(t *testing.T) {
tt := map[string]*AppConfig{
"token": {
Token: "token",
},
"app ID": {
AppID: "1",
AppInstallationID: 2,
AppPrivateKey: "private key",
},
}
for name, cfg := range tt {
t.Run(name, func(t *testing.T) {
err := cfg.Validate()
require.NoError(t, err)
})
}
}
func TestAppConfigFromSecret_invalid(t *testing.T) {
tt := map[string]map[string]string{
"empty": {},
"token and app provided": {
"github_token": "token",
"github_app_id": "2",
"githu_app_installation_id": "3",
"github_app_private_key": "private key",
},
"invalid app id": {
"github_app_id": "abc",
"githu_app_installation_id": "3",
"github_app_private_key": "private key",
},
"invalid app installation_id": {
"github_app_id": "1",
"githu_app_installation_id": "abc",
"github_app_private_key": "private key",
},
"empty private key": {
"github_app_id": "1",
"githu_app_installation_id": "2",
"github_app_private_key": "",
},
}
for name, data := range tt {
t.Run(name, func(t *testing.T) {
secret := &corev1.Secret{
StringData: data,
}
appConfig, err := FromSecret(secret)
assert.Error(t, err)
assert.Nil(t, appConfig)
})
}
}
func TestAppConfigFromSecret_valid(t *testing.T) {
tt := map[string]map[string]string{
"with token": {
"github_token": "token",
},
"app config": {
"github_app_id": "2",
"githu_app_installation_id": "3",
"github_app_private_key": "private key",
},
}
for name, data := range tt {
t.Run(name, func(t *testing.T) {
secret := &corev1.Secret{
StringData: data,
}
appConfig, err := FromSecret(secret)
assert.Error(t, err)
assert.Nil(t, appConfig)
})
}
}
func TestAppConfigFromString_valid(t *testing.T) {
tt := map[string]*AppConfig{
"token": {
Token: "token",
},
"app ID": {
AppID: "1",
AppInstallationID: 2,
AppPrivateKey: "private key",
},
}
for name, cfg := range tt {
t.Run(name, func(t *testing.T) {
bytes, err := json.Marshal(cfg)
require.NoError(t, err)
got, err := FromString(string(bytes))
require.NoError(t, err)
want := cfg.tidy()
assert.Equal(t, want, got)
})
}
}

View File

@ -86,8 +86,23 @@ type AutoscalingListener struct {
Status AutoscalingListenerStatus `json:"status,omitempty"` Status AutoscalingListenerStatus `json:"status,omitempty"`
} }
// +kubebuilder:object:root=true func (l *AutoscalingListener) GitHubConfigSecret() string {
return l.Spec.GitHubConfigSecret
}
func (l *AutoscalingListener) GitHubConfigUrl() string {
return l.Spec.GitHubConfigUrl
}
func (l *AutoscalingListener) Proxy() *ProxyConfig {
return l.Spec.Proxy
}
func (l *AutoscalingListener) GitHubServerTLS() *GitHubServerTLSConfig {
return l.Spec.GitHubServerTLS
}
// +kubebuilder:object:root=true
// AutoscalingListenerList contains a list of AutoscalingListener // AutoscalingListenerList contains a list of AutoscalingListener
type AutoscalingListenerList struct { type AutoscalingListenerList struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`

View File

@ -285,6 +285,22 @@ func (ars *AutoscalingRunnerSet) ListenerSpecHash() string {
return hash.ComputeTemplateHash(&spec) return hash.ComputeTemplateHash(&spec)
} }
func (ars *AutoscalingRunnerSet) GitHubConfigSecret() string {
return ars.Spec.GitHubConfigSecret
}
func (ars *AutoscalingRunnerSet) GitHubConfigUrl() string {
return ars.Spec.GitHubConfigUrl
}
func (ars *AutoscalingRunnerSet) Proxy() *ProxyConfig {
return ars.Spec.Proxy
}
func (ars *AutoscalingRunnerSet) GitHubServerTLS() *GitHubServerTLSConfig {
return ars.Spec.GitHubServerTLS
}
func (ars *AutoscalingRunnerSet) RunnerSetSpecHash() string { func (ars *AutoscalingRunnerSet) RunnerSetSpecHash() string {
type runnerSetSpec struct { type runnerSetSpec struct {
GitHubConfigUrl string GitHubConfigUrl string

View File

@ -67,6 +67,22 @@ func (er *EphemeralRunner) HasContainerHookConfigured() bool {
return false return false
} }
func (er *EphemeralRunner) GitHubConfigSecret() string {
return er.Spec.GitHubConfigSecret
}
func (er *EphemeralRunner) GitHubConfigUrl() string {
return er.Spec.GitHubConfigUrl
}
func (er *EphemeralRunner) Proxy() *ProxyConfig {
return er.Spec.Proxy
}
func (er *EphemeralRunner) GitHubServerTLS() *GitHubServerTLSConfig {
return er.Spec.GitHubServerTLS
}
// EphemeralRunnerSpec defines the desired state of EphemeralRunner // EphemeralRunnerSpec defines the desired state of EphemeralRunner
type EphemeralRunnerSpec struct { type EphemeralRunnerSpec struct {
// +required // +required

View File

@ -60,9 +60,24 @@ type EphemeralRunnerSet struct {
Status EphemeralRunnerSetStatus `json:"status,omitempty"` Status EphemeralRunnerSetStatus `json:"status,omitempty"`
} }
// +kubebuilder:object:root=true func (ers *EphemeralRunnerSet) GitHubConfigSecret() string {
return ers.Spec.EphemeralRunnerSpec.GitHubConfigSecret
}
func (ers *EphemeralRunnerSet) GitHubConfigUrl() string {
return ers.Spec.EphemeralRunnerSpec.GitHubConfigUrl
}
func (ers *EphemeralRunnerSet) Proxy() *ProxyConfig {
return ers.Spec.EphemeralRunnerSpec.Proxy
}
func (ers *EphemeralRunnerSet) GitHubServerTLS() *GitHubServerTLSConfig {
return ers.Spec.EphemeralRunnerSpec.GitHubServerTLS
}
// EphemeralRunnerSetList contains a list of EphemeralRunnerSet // EphemeralRunnerSetList contains a list of EphemeralRunnerSet
// +kubebuilder:object:root=true
type EphemeralRunnerSetList struct { type EphemeralRunnerSetList struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"` metav1.ListMeta `json:"metadata,omitempty"`

View File

@ -45,6 +45,7 @@ metadata:
{{- if and (ne $containerMode.type "kubernetes") (not .Values.template.spec.serviceAccountName) }} {{- if and (ne $containerMode.type "kubernetes") (not .Values.template.spec.serviceAccountName) }}
actions.github.com/cleanup-no-permission-service-account-name: {{ include "gha-runner-scale-set.noPermissionServiceAccountName" . }} actions.github.com/cleanup-no-permission-service-account-name: {{ include "gha-runner-scale-set.noPermissionServiceAccountName" . }}
{{- end }} {{- end }}
spec: spec:
githubConfigUrl: {{ required ".Values.githubConfigUrl is required" (trimSuffix "/" .Values.githubConfigUrl) }} githubConfigUrl: {{ required ".Values.githubConfigUrl is required" (trimSuffix "/" .Values.githubConfigUrl) }}
githubConfigSecret: {{ include "gha-runner-scale-set.githubsecret" . }} githubConfigSecret: {{ include "gha-runner-scale-set.githubsecret" . }}

View File

@ -2468,3 +2468,43 @@ func TestNamespaceOverride(t *testing.T) {
}) })
} }
} }
func TestAutoscalingRunnerSetCustomAnnotationsAndLabelsApplied(t *testing.T) {
t.Parallel()
// Path to the helm chart we will test
helmChartPath, err := filepath.Abs("../../gha-runner-scale-set")
require.NoError(t, err)
releaseName := "test-runners"
namespaceName := "test-" + strings.ToLower(random.UniqueId())
options := &helm.Options{
Logger: logger.Discard,
SetValues: map[string]string{
"githubConfigUrl": "https://github.com/actions",
"githubConfigSecret.github_token": "gh_token12345",
"controllerServiceAccount.name": "arc",
"controllerServiceAccount.namespace": "arc-system",
"annotations.actions\\.github\\.com/vault": "azure_key_vault",
"annotations.actions\\.github\\.com/cleanup-manager-role-name": "not-propagated",
"labels.custom": "custom",
"labels.app\\.kubernetes\\.io/component": "not-propagated",
},
KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName),
}
output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/autoscalingrunnerset.yaml"})
var autoscalingRunnerSet v1alpha1.AutoscalingRunnerSet
helm.UnmarshalK8SYaml(t, output, &autoscalingRunnerSet)
vault := autoscalingRunnerSet.Annotations["actions.github.com/vault"]
assert.Equal(t, "azure_key_vault", vault)
custom := autoscalingRunnerSet.Labels["custom"]
assert.Equal(t, "custom", custom)
assert.NotEqual(t, "not-propagated", autoscalingRunnerSet.Annotations["actions.github.com/cleanup-manager-role-name"])
assert.NotEqual(t, "not-propagated", autoscalingRunnerSet.Labels["app.kubernetes.io/component"])
}

View File

@ -6,7 +6,7 @@ githubConfigUrl: ""
## You can choose to supply: ## You can choose to supply:
## A) a PAT token, ## A) a PAT token,
## B) a GitHub App, or ## B) a GitHub App, or
## C) a pre-defined Kubernetes secret. ## C) a pre-defined secret.
## The syntax for each of these variations is documented below. ## The syntax for each of these variations is documented below.
## (Variation A) When using a PAT token, the syntax is as follows: ## (Variation A) When using a PAT token, the syntax is as follows:
githubConfigSecret: githubConfigSecret:
@ -28,8 +28,11 @@ githubConfigSecret:
# . # .
# private key line N # private key line N
# #
## (Variation C) When using a pre-defined Kubernetes secret in the same namespace that the gha-runner-scale-set is going to deploy, ## (Variation C) When using a pre-defined secret.
## the syntax is as follows: ## The secret can be pulled either directly from Kubernetes, or from the vault, depending on configuration.
## Kubernetes secret in the same namespace that the gha-runner-scale-set is going to deploy.
## On the other hand, if the vault is configured, secret name will be used to fetch the app configuration.
## The syntax is as follows:
# githubConfigSecret: pre-defined-secret # githubConfigSecret: pre-defined-secret
## Notes on using pre-defined Kubernetes secrets: ## Notes on using pre-defined Kubernetes secrets:
## You need to make sure your predefined secret has all the required secret data set properly. ## You need to make sure your predefined secret has all the required secret data set properly.

View File

@ -17,7 +17,7 @@ import (
// App is responsible for initializing required components and running the app. // App is responsible for initializing required components and running the app.
type App struct { type App struct {
// configured fields // configured fields
config config.Config config *config.Config
logger logr.Logger logger logr.Logger
// initialized fields // initialized fields
@ -38,8 +38,12 @@ type Worker interface {
} }
func New(config config.Config) (*App, error) { func New(config config.Config) (*App, error) {
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate config: %w", err)
}
app := &App{ app := &App{
config: config, config: &config,
} }
ghConfig, err := actions.ParseGitHubConfigFromURL(config.ConfigureUrl) ghConfig, err := actions.ParseGitHubConfigFromURL(config.ConfigureUrl)

View File

@ -1,6 +1,7 @@
package config package config
import ( import (
"context"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -9,20 +10,20 @@ import (
"os" "os"
"github.com/actions/actions-runner-controller/apis/actions.github.com/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/build" "github.com/actions/actions-runner-controller/build"
"github.com/actions/actions-runner-controller/github/actions" "github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/actions-runner-controller/logging" "github.com/actions/actions-runner-controller/logging"
"github.com/actions/actions-runner-controller/vault"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"golang.org/x/net/http/httpproxy" "golang.org/x/net/http/httpproxy"
) )
type Config struct { type Config struct {
ConfigureUrl string `json:"configure_url"` ConfigureUrl string `json:"configure_url"`
// AppID can be an ID of the app or the client ID VaultType string `json:"vault_type"`
AppID string `json:"app_id"` VaultLookupKey string `json:"vault_lookup_key"`
AppInstallationID int64 `json:"app_installation_id"` appconfig.AppConfig
AppPrivateKey string `json:"app_private_key"`
Token string `json:"token"`
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"`
@ -37,23 +38,57 @@ type Config struct {
Metrics *v1alpha1.MetricsConfig `json:"metrics"` Metrics *v1alpha1.MetricsConfig `json:"metrics"`
} }
func Read(path string) (Config, error) { func Read(ctx context.Context, configPath string) (*Config, error) {
f, err := os.Open(path) f, err := os.Open(configPath)
if err != nil { if err != nil {
return Config{}, err return nil, err
} }
defer f.Close() defer f.Close()
var config Config var config Config
if err := json.NewDecoder(f).Decode(&config); err != nil { if err := json.NewDecoder(f).Decode(&config); err != nil {
return Config{}, fmt.Errorf("failed to decode config: %w", err) return nil, fmt.Errorf("failed to decode config: %w", err)
} }
if config.VaultType == "" {
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate configuration: %v", err)
}
return &config, nil
}
if config.VaultLookupKey == "" {
panic(fmt.Errorf("vault type set to %q, but lookup key is empty", config.VaultType))
}
vaults, err := vault.InitAll("LISTENER_")
if err != nil {
return nil, fmt.Errorf("failed to initialize vaults: %v", err)
}
vault, ok := vaults[config.VaultType]
if !ok {
return nil, fmt.Errorf("vault %q is not initialized", config.VaultType)
}
appConfigRaw, err := vault.GetSecret(ctx, config.VaultLookupKey)
if err != nil {
return nil, err
}
appConfig, err := appconfig.FromString(appConfigRaw)
if err != nil {
return nil, fmt.Errorf("failed to read app config from string: %v", err)
}
config.AppConfig = *appConfig
if err := config.Validate(); err != nil { if err := config.Validate(); err != nil {
return Config{}, fmt.Errorf("failed to validate config: %w", err) return nil, fmt.Errorf("config validation failed: %w", err)
} }
return config, nil return &config, ctx.Err()
} }
// Validate checks the configuration for errors. // Validate checks the configuration for errors.

View File

@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/actions/actions-runner-controller/cmd/ghalistener/config" "github.com/actions/actions-runner-controller/cmd/ghalistener/config"
"github.com/actions/actions-runner-controller/github/actions" "github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/actions-runner-controller/github/actions/testserver" "github.com/actions/actions-runner-controller/github/actions/testserver"
@ -53,7 +54,9 @@ func TestCustomerServerRootCA(t *testing.T) {
config := config.Config{ config := config.Config{
ConfigureUrl: server.ConfigURLForOrg("myorg"), ConfigureUrl: server.ConfigURLForOrg("myorg"),
ServerRootCA: certsString, ServerRootCA: certsString,
Token: "token", AppConfig: appconfig.AppConfig{
Token: "token",
},
} }
client, err := config.ActionsClient(logr.Discard()) client, err := config.ActionsClient(logr.Discard())
@ -80,7 +83,9 @@ func TestProxySettings(t *testing.T) {
config := config.Config{ config := config.Config{
ConfigureUrl: "https://github.com/org/repo", ConfigureUrl: "https://github.com/org/repo",
Token: "token", AppConfig: appconfig.AppConfig{
Token: "token",
},
} }
client, err := config.ActionsClient(logr.Discard()) client, err := config.ActionsClient(logr.Discard())
@ -110,7 +115,9 @@ func TestProxySettings(t *testing.T) {
config := config.Config{ config := config.Config{
ConfigureUrl: "https://github.com/org/repo", ConfigureUrl: "https://github.com/org/repo",
Token: "token", AppConfig: appconfig.AppConfig{
Token: "token",
},
} }
client, err := config.ActionsClient(logr.Discard(), actions.WithRetryMax(0)) client, err := config.ActionsClient(logr.Discard(), actions.WithRetryMax(0))
@ -145,7 +152,9 @@ func TestProxySettings(t *testing.T) {
config := config.Config{ config := config.Config{
ConfigureUrl: "https://github.com/org/repo", ConfigureUrl: "https://github.com/org/repo",
Token: "token", AppConfig: appconfig.AppConfig{
Token: "token",
},
} }
client, err := config.ActionsClient(logr.Discard()) client, err := config.ActionsClient(logr.Discard())

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -15,7 +16,9 @@ func TestConfigValidationMinMax(t *testing.T) {
RunnerScaleSetId: 1, RunnerScaleSetId: 1,
MinRunners: 5, MinRunners: 5,
MaxRunners: 2, MaxRunners: 2,
Token: "token", AppConfig: appconfig.AppConfig{
Token: "token",
},
} }
err := config.Validate() err := config.Validate()
assert.ErrorContains(t, err, `MinRunners "5" cannot be greater than MaxRunners "2"`, "Expected error about MinRunners > MaxRunners") assert.ErrorContains(t, err, `MinRunners "5" cannot be greater than MaxRunners "2"`, "Expected error about MinRunners > MaxRunners")
@ -39,8 +42,10 @@ 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{
AppID: "1", AppConfig: appconfig.AppConfig{
AppInstallationID: 10, AppID: "1",
AppInstallationID: 10,
},
ConfigureUrl: "github.com/some_org/some_repo", ConfigureUrl: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace", EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment", EphemeralRunnerSetName: "deployment",
@ -54,8 +59,10 @@ 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{
AppID: "Iv23f8doAlphaNumer1c", AppConfig: appconfig.AppConfig{
AppInstallationID: 10, AppID: "Iv23f8doAlphaNumer1c",
AppInstallationID: 10,
},
ConfigureUrl: "github.com/some_org/some_repo", ConfigureUrl: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace", EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment", EphemeralRunnerSetName: "deployment",
@ -69,10 +76,12 @@ func TestConfigValidationAppKey(t *testing.T) {
func TestConfigValidationOnlyOneTypeOfCredentials(t *testing.T) { func TestConfigValidationOnlyOneTypeOfCredentials(t *testing.T) {
config := &Config{ config := &Config{
AppID: "1", AppConfig: appconfig.AppConfig{
AppInstallationID: 10, AppID: "1",
AppPrivateKey: "asdf", AppInstallationID: 10,
Token: "asdf", AppPrivateKey: "asdf",
Token: "asdf",
},
ConfigureUrl: "github.com/some_org/some_repo", ConfigureUrl: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace", EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment", EphemeralRunnerSetName: "deployment",
@ -91,7 +100,9 @@ func TestConfigValidation(t *testing.T) {
RunnerScaleSetId: 1, RunnerScaleSetId: 1,
MinRunners: 1, MinRunners: 1,
MaxRunners: 5, MaxRunners: 5,
Token: "asdf", AppConfig: appconfig.AppConfig{
Token: "asdf",
},
} }
err := config.Validate() err := config.Validate()

View File

@ -13,26 +13,27 @@ import (
) )
func main() { func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
configPath, ok := os.LookupEnv("LISTENER_CONFIG_PATH") configPath, ok := os.LookupEnv("LISTENER_CONFIG_PATH")
if !ok { if !ok {
fmt.Fprintf(os.Stderr, "Error: LISTENER_CONFIG_PATH environment variable is not set\n") fmt.Fprintf(os.Stderr, "Error: LISTENER_CONFIG_PATH environment variable is not set\n")
os.Exit(1) os.Exit(1)
} }
config, err := config.Read(configPath)
config, err := config.Read(ctx, configPath)
if err != nil { if err != nil {
log.Printf("Failed to read config: %v", err) log.Printf("Failed to read config: %v", err)
os.Exit(1) os.Exit(1)
} }
app, err := app.New(config) app, err := app.New(*config)
if err != nil { if err != nil {
log.Printf("Failed to initialize app: %v", err) log.Printf("Failed to initialize app: %v", err)
os.Exit(1) os.Exit(1)
} }
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
if err := app.Run(ctx); err != nil { if err := app.Run(ctx); err != nil {
log.Printf("Application returned an error: %v", err) log.Printf("Application returned an error: %v", err)
os.Exit(1) os.Exit(1)

View File

@ -0,0 +1,242 @@
package actionsgithubcom
import (
"context"
"encoding/json"
"fmt"
"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/vault"
"golang.org/x/net/http/httpproxy"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type ActionsClientPool struct {
k8sClient client.Client
vaultResolvers map[string]resolver
multiClient actions.MultiClient
}
type ActionsClientPoolOption func(*ActionsClientPool)
func WithVault(ty string, vault vault.Vault) ActionsClientPoolOption {
return func(pool *ActionsClientPool) {
pool.vaultResolvers[ty] = &vaultResolver{vault}
}
}
func NewActionsClientPool(k8sClient client.Client, multiClient actions.MultiClient, opts ...ActionsClientPoolOption) *ActionsClientPool {
if k8sClient == nil {
panic("k8sClient must not be nil")
}
pool := &ActionsClientPool{
k8sClient: k8sClient,
multiClient: multiClient,
vaultResolvers: make(map[string]resolver),
}
for _, opt := range opts {
opt(pool)
}
return pool
}
type ActionsGitHubObject interface {
client.Object
GitHubConfigUrl() string
GitHubConfigSecret() string
Proxy() *v1alpha1.ProxyConfig
GitHubServerTLS() *v1alpha1.GitHubServerTLSConfig
}
func (p *ActionsClientPool) Get(ctx context.Context, obj ActionsGitHubObject) (actions.ActionsService, error) {
resolver, err := p.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)
}
var clientOptions []actions.ClientOption
if proxy := obj.Proxy(); proxy != nil {
config := &httpproxy.Config{
NoProxy: strings.Join(proxy.NoProxy, ","),
}
if proxy.HTTP != nil {
u, err := url.Parse(proxy.HTTP.Url)
if err != nil {
return nil, fmt.Errorf("failed to parse proxy http url %q: %w", proxy.HTTP.Url, err)
}
if ref := proxy.HTTP.CredentialSecretRef; ref != "" {
u.User, err = resolver.proxyCredentials(ctx, ref)
if err != nil {
return nil, fmt.Errorf("failed to resolve proxy credentials: %v", err)
}
}
config.HTTPProxy = u.String()
}
if proxy.HTTPS != nil {
u, err := url.Parse(proxy.HTTPS.Url)
if err != nil {
return nil, fmt.Errorf("failed to parse proxy https url %q: %w", proxy.HTTPS.Url, err)
}
if ref := proxy.HTTPS.CredentialSecretRef; ref != "" {
u.User, err = resolver.proxyCredentials(ctx, ref)
if err != nil {
return nil, fmt.Errorf("failed to resolve proxy credentials: %v", err)
}
}
config.HTTPSProxy = u.String()
}
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 configmap corev1.ConfigMap
err := p.k8sClient.Get(
ctx,
types.NamespacedName{
Namespace: obj.GetNamespace(),
Name: name,
},
&configmap,
)
if err != nil {
return nil, fmt.Errorf("failed to get configmap %s: %w", name, err)
}
return []byte(configmap.Data[key]), nil
})
if err != nil {
return nil, fmt.Errorf("failed to get tls config: %w", err)
}
clientOptions = append(clientOptions, actions.WithRootCAs(pool))
}
return p.multiClient.GetClientFor(
ctx,
obj.GitHubConfigUrl(),
appConfig,
obj.GetNamespace(),
clientOptions...,
)
}
func (p *ActionsClientPool) resolverForObject(obj ActionsGitHubObject) (resolver, error) {
ty, ok := obj.GetAnnotations()[AnnotationKeyGitHubVaultType]
if !ok {
return &k8sResolver{
namespace: obj.GetNamespace(),
client: p.k8sClient,
}, nil
}
vault, ok := p.vaultResolvers[ty]
if !ok {
return nil, fmt.Errorf("unknown vault resolver %q", ty)
}
return vault, nil
}
type resolver interface {
appConfig(ctx context.Context, key string) (*appconfig.AppConfig, error)
proxyCredentials(ctx context.Context, key string) (*url.Userinfo, error)
}
type k8sResolver struct {
namespace string
client client.Client
}
func (r *k8sResolver) appConfig(ctx context.Context, key string) (*appconfig.AppConfig, error) {
nsName := types.NamespacedName{
Namespace: r.namespace,
Name: key,
}
secret := new(corev1.Secret)
if err := r.client.Get(
ctx,
nsName,
secret,
); err != nil {
return nil, fmt.Errorf("failed to get kubernetes secret: %q", nsName.String())
}
return appconfig.FromSecret(secret)
}
func (r *k8sResolver) proxyCredentials(ctx context.Context, key string) (*url.Userinfo, error) {
nsName := types.NamespacedName{Namespace: r.namespace, Name: key}
secret := new(corev1.Secret)
if err := r.client.Get(
ctx,
nsName,
secret,
); err != nil {
return nil, fmt.Errorf("failed to get kubernetes secret: %q", nsName.String())
}
return url.UserPassword(
string(secret.Data["username"]),
string(secret.Data["password"]),
), nil
}
type vaultResolver struct {
vault vault.Vault
}
func (r *vaultResolver) appConfig(ctx context.Context, key string) (*appconfig.AppConfig, error) {
val, err := r.vault.GetSecret(ctx, key)
if err != nil {
return nil, fmt.Errorf("failed to resolve secret: %v", err)
}
return appconfig.FromString(val)
}
func (r *vaultResolver) proxyCredentials(ctx context.Context, key string) (*url.Userinfo, error) {
val, err := r.vault.GetSecret(ctx, key)
if err != nil {
return nil, fmt.Errorf("failed to resolve secret: %v", err)
}
type info struct {
Username string `json:"username"`
Password string `json:"password"`
}
var i info
if err := json.Unmarshal([]byte(val), &i); err != nil {
return nil, fmt.Errorf("failed to unmarshal info: %v", err)
}
return url.UserPassword(i.Username, i.Password), nil
}

View File

@ -1,248 +0,0 @@
package actionsgithubcom
import (
"context"
"fmt"
"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/vault"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type ActionsClientGetter interface {
GetActionsClientForAutoscalingRunnerSet(ctx context.Context, ars *v1alpha1.AutoscalingRunnerSet) (actions.ActionsService, error)
GetActionsClientForEphemeralRunnerSet(ctx context.Context, ers *v1alpha1.EphemeralRunnerSet) (actions.ActionsService, error)
GetActionsClientForEphemeralRunner(ctx context.Context, er *v1alpha1.EphemeralRunner) (actions.ActionsService, error)
}
var (
_ ActionsClientGetter = (*ActionsClientSecretResolver)(nil)
_ ActionsClientGetter = (*ActionsClientVaultResolver)(nil)
)
type ActionsClientSecretResolver struct {
client.Client
actions.MultiClient
}
func (r *ActionsClientSecretResolver) GetActionsClientForAutoscalingRunnerSet(ctx context.Context, ars *v1alpha1.AutoscalingRunnerSet) (actions.ActionsService, error) {
var configSecret corev1.Secret
if err := r.Get(ctx, types.NamespacedName{Namespace: ars.Namespace, Name: ars.Spec.GitHubConfigSecret}, &configSecret); err != nil {
return nil, fmt.Errorf("failed to find GitHub config secret: %w", err)
}
opts, err := r.actionsClientOptionsForAutoscalingRunnerSet(ctx, ars)
if err != nil {
return nil, fmt.Errorf("failed to get actions client options: %w", err)
}
return r.MultiClient.GetClientFromSecret(
ctx,
ars.Spec.GitHubConfigUrl,
ars.Namespace,
configSecret.Data,
opts...,
)
}
func (r *ActionsClientSecretResolver) actionsClientOptionsForAutoscalingRunnerSet(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) ([]actions.ClientOption, error) {
var options []actions.ClientOption
if autoscalingRunnerSet.Spec.Proxy != nil {
proxyFunc, err := autoscalingRunnerSet.Spec.Proxy.ProxyFunc(func(s string) (*corev1.Secret, error) {
var secret corev1.Secret
err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingRunnerSet.Namespace, Name: s}, &secret)
if err != nil {
return nil, fmt.Errorf("failed to get proxy secret %s: %w", s, err)
}
return &secret, nil
})
if err != nil {
return nil, fmt.Errorf("failed to get proxy func: %w", err)
}
options = append(options, actions.WithProxy(proxyFunc))
}
tlsConfig := autoscalingRunnerSet.Spec.GitHubServerTLS
if tlsConfig != nil {
pool, err := tlsConfig.ToCertPool(func(name, key string) ([]byte, error) {
var configmap corev1.ConfigMap
err := r.Get(
ctx,
types.NamespacedName{
Namespace: autoscalingRunnerSet.Namespace,
Name: name,
},
&configmap,
)
if err != nil {
return nil, fmt.Errorf("failed to get configmap %s: %w", name, err)
}
return []byte(configmap.Data[key]), nil
})
if err != nil {
return nil, fmt.Errorf("failed to get tls config: %w", err)
}
options = append(options, actions.WithRootCAs(pool))
}
return options, nil
}
func (r *ActionsClientSecretResolver) GetActionsClientForEphemeralRunnerSet(ctx context.Context, rs *v1alpha1.EphemeralRunnerSet) (actions.ActionsService, error) {
secret := new(corev1.Secret)
if err := r.Get(ctx, types.NamespacedName{Namespace: rs.Namespace, Name: rs.Spec.EphemeralRunnerSpec.GitHubConfigSecret}, secret); err != nil {
return nil, fmt.Errorf("failed to get secret: %w", err)
}
opts, err := r.actionsClientOptionsForEphemeralRunnerSet(ctx, rs)
if err != nil {
return nil, fmt.Errorf("failed to get actions client options: %w", err)
}
return r.MultiClient.GetClientFromSecret(
ctx,
rs.Spec.EphemeralRunnerSpec.GitHubConfigUrl,
rs.Namespace,
secret.Data,
opts...,
)
}
func (r *ActionsClientSecretResolver) actionsClientOptionsForEphemeralRunnerSet(ctx context.Context, rs *v1alpha1.EphemeralRunnerSet) ([]actions.ClientOption, error) {
var opts []actions.ClientOption
if rs.Spec.EphemeralRunnerSpec.Proxy != nil {
proxyFunc, err := rs.Spec.EphemeralRunnerSpec.Proxy.ProxyFunc(func(s string) (*corev1.Secret, error) {
var secret corev1.Secret
err := r.Get(ctx, types.NamespacedName{Namespace: rs.Namespace, Name: s}, &secret)
if err != nil {
return nil, fmt.Errorf("failed to get secret %s: %w", s, err)
}
return &secret, nil
})
if err != nil {
return nil, fmt.Errorf("failed to get proxy func: %w", err)
}
opts = append(opts, actions.WithProxy(proxyFunc))
}
tlsConfig := rs.Spec.EphemeralRunnerSpec.GitHubServerTLS
if tlsConfig != nil {
pool, err := tlsConfig.ToCertPool(func(name, key string) ([]byte, error) {
var configmap corev1.ConfigMap
err := r.Get(
ctx,
types.NamespacedName{
Namespace: rs.Namespace,
Name: name,
},
&configmap,
)
if err != nil {
return nil, fmt.Errorf("failed to get configmap %s: %w", name, err)
}
return []byte(configmap.Data[key]), nil
})
if err != nil {
return nil, fmt.Errorf("failed to get tls config: %w", err)
}
opts = append(opts, actions.WithRootCAs(pool))
}
return opts, nil
}
func (r *ActionsClientSecretResolver) GetActionsClientForEphemeralRunner(ctx context.Context, runner *v1alpha1.EphemeralRunner) (actions.ActionsService, error) {
secret := new(corev1.Secret)
if err := r.Get(ctx, types.NamespacedName{Namespace: runner.Namespace, Name: runner.Spec.GitHubConfigSecret}, secret); err != nil {
return nil, fmt.Errorf("failed to get secret: %w", err)
}
opts, err := r.actionsClientOptionsForEphemeralRunner(ctx, runner)
if err != nil {
return nil, fmt.Errorf("failed to get actions client options: %w", err)
}
return r.MultiClient.GetClientFromSecret(
ctx,
runner.Spec.GitHubConfigUrl,
runner.Namespace,
secret.Data,
opts...,
)
}
func (r *ActionsClientSecretResolver) actionsClientOptionsForEphemeralRunner(ctx context.Context, runner *v1alpha1.EphemeralRunner) ([]actions.ClientOption, error) {
var opts []actions.ClientOption
if runner.Spec.Proxy != nil {
proxyFunc, err := runner.Spec.Proxy.ProxyFunc(func(s string) (*corev1.Secret, error) {
var secret corev1.Secret
err := r.Get(ctx, types.NamespacedName{Namespace: runner.Namespace, Name: s}, &secret)
if err != nil {
return nil, fmt.Errorf("failed to get proxy secret %s: %w", s, err)
}
return &secret, nil
})
if err != nil {
return nil, fmt.Errorf("failed to get proxy func: %w", err)
}
opts = append(opts, actions.WithProxy(proxyFunc))
}
tlsConfig := runner.Spec.GitHubServerTLS
if tlsConfig != nil {
pool, err := tlsConfig.ToCertPool(func(name, key string) ([]byte, error) {
var configmap corev1.ConfigMap
err := r.Get(
ctx,
types.NamespacedName{
Namespace: runner.Namespace,
Name: name,
},
&configmap,
)
if err != nil {
return nil, fmt.Errorf("failed to get configmap %s: %w", name, err)
}
return []byte(configmap.Data[key]), nil
})
if err != nil {
return nil, fmt.Errorf("failed to get tls config: %w", err)
}
opts = append(opts, actions.WithRootCAs(pool))
}
return opts, nil
}
type ActionsClientVaultResolver struct {
vault.Vault
actions.MultiClient
}
func (r *ActionsClientVaultResolver) GetActionsClientForAutoscalingRunnerSet(ctx context.Context, ars *v1alpha1.AutoscalingRunnerSet) (actions.ActionsService, error) {
panic("todo")
}
func (r *ActionsClientVaultResolver) GetActionsClientForEphemeralRunnerSet(ctx context.Context, ers *v1alpha1.EphemeralRunnerSet) (actions.ActionsService, error) {
panic("todo")
}
func (r *ActionsClientVaultResolver) GetActionsClientForEphemeralRunner(ctx context.Context, er *v1alpha1.EphemeralRunner) (actions.ActionsService, error) {
panic("todo")
}

View File

@ -137,27 +137,6 @@ func (r *AutoscalingListenerReconciler) Reconcile(ctx context.Context, req ctrl.
return ctrl.Result{}, err return ctrl.Result{}, err
} }
// Create a mirror secret in the same namespace as the AutoscalingListener
mirrorSecret := new(corev1.Secret)
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Namespace, Name: autoscalingListener.Name}, mirrorSecret); err != nil {
if !kerrors.IsNotFound(err) {
log.Error(err, "Unable to get listener secret mirror", "namespace", autoscalingListener.Namespace, "name", autoscalingListener.Name)
return ctrl.Result{}, err
}
// Create a mirror secret for the listener pod in the Controller namespace for listener pod to use
log.Info("Creating a mirror listener secret for the listener pod")
return r.createSecretsForListener(ctx, autoscalingListener, secret, log)
}
// make sure the mirror secret is up to date
mirrorSecretDataHash := mirrorSecret.Labels["secret-data-hash"]
secretDataHash := hash.ComputeTemplateHash(secret.Data)
if mirrorSecretDataHash != secretDataHash {
log.Info("Updating mirror listener secret for the listener pod", "mirrorSecretDataHash", mirrorSecretDataHash, "secretDataHash", secretDataHash)
return r.updateSecretsForListener(ctx, secret, mirrorSecret, log)
}
// Make sure the runner scale set listener service account is created for the listener pod in the controller namespace // Make sure the runner scale set listener service account is created for the listener pod in the controller namespace
serviceAccount := new(corev1.ServiceAccount) serviceAccount := new(corev1.ServiceAccount)
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Namespace, Name: autoscalingListener.Name}, serviceAccount); err != nil { if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingListener.Namespace, Name: autoscalingListener.Name}, serviceAccount); err != nil {
@ -239,7 +218,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, mirrorSecret, log) return r.createListenerPod(ctx, &autoscalingRunnerSet, autoscalingListener, serviceAccount, secret, log)
} }
cs := listenerContainerStatus(listenerPod) cs := listenerContainerStatus(listenerPod)
@ -486,7 +465,7 @@ func (r *AutoscalingListenerReconciler) createListenerPod(ctx context.Context, a
return ctrl.Result{Requeue: true}, nil return ctrl.Result{Requeue: true}, nil
} }
newPod, err := r.newScaleSetListenerPod(autoscalingListener, &podConfig, serviceAccount, secret, metricsConfig, envs...) newPod, err := r.newScaleSetListenerPod(autoscalingListener, &podConfig, serviceAccount, metricsConfig, envs...)
if err != nil { if err != nil {
logger.Error(err, "Failed to build listener pod") logger.Error(err, "Failed to build listener pod")
return ctrl.Result{}, err return ctrl.Result{}, err

View File

@ -14,7 +14,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log" logf "sigs.k8s.io/controller-runtime/pkg/log"
listenerconfig "github.com/actions/actions-runner-controller/cmd/ghalistener/config" ghalistenerconfig "github.com/actions/actions-runner-controller/cmd/ghalistener/config"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
kerrors "k8s.io/apimachinery/pkg/api/errors" kerrors "k8s.io/apimachinery/pkg/api/errors"
@ -1110,7 +1110,7 @@ var _ = Describe("Test GitHub Server TLS configuration", func() {
g.Expect(config.Data["config.json"]).ToNot(BeEmpty(), "listener configuration file should not be empty") g.Expect(config.Data["config.json"]).ToNot(BeEmpty(), "listener configuration file should not be empty")
var listenerConfig listenerconfig.Config var listenerConfig ghalistenerconfig.Config
err = json.Unmarshal(config.Data["config.json"], &listenerConfig) err = json.Unmarshal(config.Data["config.json"], &listenerConfig)
g.Expect(err).NotTo(HaveOccurred(), "failed to parse listener configuration file") g.Expect(err).NotTo(HaveOccurred(), "failed to parse listener configuration file")

View File

@ -207,14 +207,6 @@ func (r *AutoscalingRunnerSetReconciler) Reconcile(ctx context.Context, req ctrl
return r.updateRunnerScaleSetName(ctx, autoscalingRunnerSet, log) return r.updateRunnerScaleSetName(ctx, autoscalingRunnerSet, log)
} }
secret := new(corev1.Secret)
if err := r.Get(ctx, types.NamespacedName{Namespace: autoscalingRunnerSet.Namespace, Name: autoscalingRunnerSet.Spec.GitHubConfigSecret}, secret); err != nil {
log.Error(err, "Failed to find GitHub config secret.",
"namespace", autoscalingRunnerSet.Namespace,
"name", autoscalingRunnerSet.Spec.GitHubConfigSecret)
return ctrl.Result{}, err
}
existingRunnerSets, err := r.listEphemeralRunnerSets(ctx, autoscalingRunnerSet) existingRunnerSets, err := r.listEphemeralRunnerSets(ctx, autoscalingRunnerSet)
if err != nil { if err != nil {
log.Error(err, "Failed to list existing ephemeral runner sets") log.Error(err, "Failed to list existing ephemeral runner sets")
@ -402,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.ActionsClientGetter.GetActionsClientForAutoscalingRunnerSet(ctx, autoscalingRunnerSet) actionsClient, err := r.ActionsClientPool.Get(ctx, autoscalingRunnerSet)
if len(autoscalingRunnerSet.Spec.RunnerScaleSetName) == 0 { if len(autoscalingRunnerSet.Spec.RunnerScaleSetName) == 0 {
autoscalingRunnerSet.Spec.RunnerScaleSetName = autoscalingRunnerSet.Name autoscalingRunnerSet.Spec.RunnerScaleSetName = autoscalingRunnerSet.Name
} }
@ -498,7 +490,7 @@ func (r *AutoscalingRunnerSetReconciler) updateRunnerScaleSetRunnerGroup(ctx con
return ctrl.Result{}, err return ctrl.Result{}, err
} }
actionsClient, err := r.ActionsClientGetter.GetActionsClientForAutoscalingRunnerSet(ctx, autoscalingRunnerSet) actionsClient, err := r.ActionsClientPool.Get(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
@ -546,7 +538,7 @@ func (r *AutoscalingRunnerSetReconciler) updateRunnerScaleSetName(ctx context.Co
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
actionsClient, err := r.ActionsClientGetter.GetActionsClientForAutoscalingRunnerSet(ctx, autoscalingRunnerSet) actionsClient, err := r.ActionsClientPool.Get(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
@ -597,7 +589,7 @@ func (r *AutoscalingRunnerSetReconciler) deleteRunnerScaleSet(ctx context.Contex
return nil return nil
} }
actionsClient, err := r.ActionsClientGetter.GetActionsClientForAutoscalingRunnerSet(ctx, autoscalingRunnerSet) actionsClient, err := r.ActionsClientPool.Get(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,9 +71,9 @@ 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{
ActionsClientGetter: &ActionsClientSecretResolver{ ActionsClientPool: &ActionsClientPool{
Client: k8sClient, k8sClient: k8sClient,
MultiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },
}, },
} }
@ -711,9 +711,9 @@ 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{
ActionsClientGetter: &ActionsClientSecretResolver{ ActionsClientPool: &ActionsClientPool{
Client: k8sClient, k8sClient: k8sClient,
MultiClient: multiClient, multiClient: multiClient,
}, },
}, },
} }
@ -831,9 +831,9 @@ 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{
ActionsClientGetter: &ActionsClientSecretResolver{ ActionsClientPool: &ActionsClientPool{
Client: k8sClient, k8sClient: k8sClient,
MultiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },
}, },
} }
@ -962,9 +962,9 @@ 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{
ActionsClientGetter: &ActionsClientSecretResolver{ ActionsClientPool: &ActionsClientPool{
Client: k8sClient, k8sClient: k8sClient,
MultiClient: multiClient, multiClient: multiClient,
}, },
}, },
} }
@ -1150,9 +1150,9 @@ 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{
ActionsClientGetter: &ActionsClientSecretResolver{ ActionsClientPool: &ActionsClientPool{
Client: k8sClient, k8sClient: k8sClient,
MultiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },
}, },
} }
@ -1163,9 +1163,9 @@ 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.ResourceBuilder.ActionsClientGetter = &ActionsClientSecretResolver{ controller.ActionsClientPool = &ActionsClientPool{
Client: k8sClient, k8sClient: k8sClient,
MultiClient: actions.NewMultiClient(logr.Discard()), multiClient: actions.NewMultiClient(logr.Discard()),
} }
certsFolder := filepath.Join( certsFolder := filepath.Join(
@ -1392,9 +1392,9 @@ 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{
ActionsClientGetter: &ActionsClientSecretResolver{ ActionsClientPool: &ActionsClientPool{
Client: k8sClient, k8sClient: k8sClient,
MultiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },
}, },
} }
@ -1555,9 +1555,9 @@ 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{
ActionsClientGetter: &ActionsClientSecretResolver{ ActionsClientPool: &ActionsClientPool{
Client: k8sClient, k8sClient: k8sClient,
MultiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },
}, },
} }
@ -1768,9 +1768,9 @@ 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{
ActionsClientGetter: &ActionsClientSecretResolver{ ActionsClientPool: &ActionsClientPool{
Client: k8sClient, k8sClient: k8sClient,
MultiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },
}, },
} }

View File

@ -43,6 +43,8 @@ const (
AnnotationKeyGitHubRunnerGroupName = "actions.github.com/runner-group-name" AnnotationKeyGitHubRunnerGroupName = "actions.github.com/runner-group-name"
AnnotationKeyGitHubRunnerScaleSetName = "actions.github.com/runner-scale-set-name" AnnotationKeyGitHubRunnerScaleSetName = "actions.github.com/runner-scale-set-name"
AnnotationKeyPatchID = "actions.github.com/patch-id" AnnotationKeyPatchID = "actions.github.com/patch-id"
AnnotationKeyGitHubVaultType = "actions.github.com/vault"
) )
// Labels applied to listener roles // Labels applied to listener roles

View File

@ -45,9 +45,8 @@ const (
// EphemeralRunnerReconciler reconciles a EphemeralRunner object // EphemeralRunnerReconciler reconciles a EphemeralRunner object
type EphemeralRunnerReconciler struct { type EphemeralRunnerReconciler struct {
client.Client client.Client
Log logr.Logger Log logr.Logger
Scheme *runtime.Scheme Scheme *runtime.Scheme
ActionsClient actions.MultiClient
ResourceBuilder ResourceBuilder
} }
@ -529,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.actionsClientFor(ctx, ephemeralRunner) actionsClient, err := r.ActionsClientPool.Get(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)
} }
@ -753,77 +752,10 @@ func (r *EphemeralRunnerReconciler) updateRunStatusFromPod(ctx context.Context,
return nil return nil
} }
func (r *EphemeralRunnerReconciler) actionsClientFor(ctx context.Context, runner *v1alpha1.EphemeralRunner) (actions.ActionsService, error) {
secret := new(corev1.Secret)
if err := r.Get(ctx, types.NamespacedName{Namespace: runner.Namespace, Name: runner.Spec.GitHubConfigSecret}, secret); err != nil {
return nil, fmt.Errorf("failed to get secret: %w", err)
}
opts, err := r.actionsClientOptionsFor(ctx, runner)
if err != nil {
return nil, fmt.Errorf("failed to get actions client options: %w", err)
}
return r.ActionsClient.GetClientFromSecret(
ctx,
runner.Spec.GitHubConfigUrl,
runner.Namespace,
secret.Data,
opts...,
)
}
func (r *EphemeralRunnerReconciler) actionsClientOptionsFor(ctx context.Context, runner *v1alpha1.EphemeralRunner) ([]actions.ClientOption, error) {
var opts []actions.ClientOption
if runner.Spec.Proxy != nil {
proxyFunc, err := runner.Spec.Proxy.ProxyFunc(func(s string) (*corev1.Secret, error) {
var secret corev1.Secret
err := r.Get(ctx, types.NamespacedName{Namespace: runner.Namespace, Name: s}, &secret)
if err != nil {
return nil, fmt.Errorf("failed to get proxy secret %s: %w", s, err)
}
return &secret, nil
})
if err != nil {
return nil, fmt.Errorf("failed to get proxy func: %w", err)
}
opts = append(opts, actions.WithProxy(proxyFunc))
}
tlsConfig := runner.Spec.GitHubServerTLS
if tlsConfig != nil {
pool, err := tlsConfig.ToCertPool(func(name, key string) ([]byte, error) {
var configmap corev1.ConfigMap
err := r.Get(
ctx,
types.NamespacedName{
Namespace: runner.Namespace,
Name: name,
},
&configmap,
)
if err != nil {
return nil, fmt.Errorf("failed to get configmap %s: %w", name, err)
}
return []byte(configmap.Data[key]), nil
})
if err != nil {
return nil, fmt.Errorf("failed to get tls config: %w", err)
}
opts = append(opts, actions.WithRootCAs(pool))
}
return opts, nil
}
// 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.actionsClientFor(ctx, runner) actionsClient, err := r.ActionsClientPool.Get(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)
} }
@ -850,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.actionsClientFor(ctx, ephemeralRunner) client, err := r.ActionsClientPool.Get(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

@ -107,10 +107,15 @@ var _ = Describe("EphemeralRunner", func() {
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
controller = &EphemeralRunnerReconciler{ controller = &EphemeralRunnerReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ActionsClient: fake.NewMultiClient(), ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{
k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient(),
},
},
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
@ -789,22 +794,27 @@ var _ = Describe("EphemeralRunner", func() {
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ActionsClient: fake.NewMultiClient( ResourceBuilder: ResourceBuilder{
fake.WithDefaultClient( ActionsClientPool: &ActionsClientPool{
fake.NewFakeClient( k8sClient: mgr.GetClient(),
fake.WithGetRunner( multiClient: fake.NewMultiClient(
fake.WithDefaultClient(
fake.NewFakeClient(
fake.WithGetRunner(
nil,
&actions.ActionsError{
StatusCode: http.StatusNotFound,
Err: &actions.ActionsExceptionError{
ExceptionName: "AgentNotFoundException",
},
},
),
),
nil, nil,
&actions.ActionsError{
StatusCode: http.StatusNotFound,
Err: &actions.ActionsExceptionError{
ExceptionName: "AgentNotFoundException",
},
},
), ),
), ),
nil, },
), },
),
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
Expect(err).To(BeNil(), "failed to setup controller") Expect(err).To(BeNil(), "failed to setup controller")
@ -861,10 +871,15 @@ var _ = Describe("EphemeralRunner", func() {
configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoScalingNS.Name) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoScalingNS.Name)
controller = &EphemeralRunnerReconciler{ controller = &EphemeralRunnerReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ActionsClient: fake.NewMultiClient(), ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{
k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient(),
},
},
} }
err := controller.SetupWithManager(mgr) err := controller.SetupWithManager(mgr)
Expect(err).To(BeNil(), "failed to setup controller") Expect(err).To(BeNil(), "failed to setup controller")
@ -874,7 +889,12 @@ 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.ActionsClient = actions.NewMultiClient(logr.Discard()) controller.ResourceBuilder = ResourceBuilder{
ActionsClientPool: &ActionsClientPool{
k8sClient: mgr.GetClient(),
multiClient: actions.NewMultiClient(logr.Discard()),
},
}
proxySuccessfulllyCalled := false proxySuccessfulllyCalled := false
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -1025,10 +1045,15 @@ var _ = Describe("EphemeralRunner", func() {
Expect(err).NotTo(HaveOccurred(), "failed to create configmap with root CAs") Expect(err).NotTo(HaveOccurred(), "failed to create configmap with root CAs")
controller = &EphemeralRunnerReconciler{ controller = &EphemeralRunnerReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ActionsClient: fake.NewMultiClient(), ResourceBuilder: ResourceBuilder{
ActionsClientPool: &ActionsClientPool{
k8sClient: mgr.GetClient(),
multiClient: fake.NewMultiClient(),
},
},
} }
err = controller.SetupWithManager(mgr) err = controller.SetupWithManager(mgr)
@ -1059,7 +1084,12 @@ var _ = Describe("EphemeralRunner", func() {
server.StartTLS() server.StartTLS()
// Use an actual client // Use an actual client
controller.ActionsClient = actions.NewMultiClient(logr.Discard()) controller.ResourceBuilder = ResourceBuilder{
ActionsClientPool: &ActionsClientPool{
k8sClient: mgr.GetClient(),
multiClient: actions.NewMultiClient(logr.Discard()),
},
}
ephemeralRunner := newExampleRunner("test-runner", autoScalingNS.Name, configSecret.Name) ephemeralRunner := newExampleRunner("test-runner", autoScalingNS.Name, configSecret.Name)
ephemeralRunner.Spec.GitHubConfigUrl = server.ConfigURLForOrg("my-org") ephemeralRunner.Spec.GitHubConfigUrl = server.ConfigURLForOrg("my-org")

View File

@ -331,7 +331,7 @@ func (r *EphemeralRunnerSetReconciler) cleanUpEphemeralRunners(ctx context.Conte
return false, nil return false, nil
} }
actionsClient, err := r.ActionsClientGetter.GetActionsClientForEphemeralRunnerSet(ctx, ephemeralRunnerSet) actionsClient, err := r.ActionsClientPool.Get(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.ActionsClientGetter.GetActionsClientForEphemeralRunnerSet(ctx, ephemeralRunnerSet) actionsClient, err := r.ActionsClientPool.Get(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,9 +58,9 @@ var _ = Describe("Test EphemeralRunnerSet controller", func() {
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
Log: logf.Log, Log: logf.Log,
ResourceBuilder: ResourceBuilder{ ResourceBuilder: ResourceBuilder{
ActionsClientGetter: &ActionsClientSecretResolver{ ActionsClientPool: &ActionsClientPool{
Client: mgr.GetClient(), k8sClient: mgr.GetClient(),
MultiClient: fake.NewMultiClient(), multiClient: fake.NewMultiClient(),
}, },
}, },
} }
@ -1113,9 +1113,9 @@ 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{
ActionsClientGetter: &ActionsClientSecretResolver{ ActionsClientPool: &ActionsClientPool{
Client: mgr.GetClient(), k8sClient: mgr.GetClient(),
MultiClient: actions.NewMultiClient(logr.Discard()), multiClient: actions.NewMultiClient(logr.Discard()),
}, },
}, },
} }
@ -1417,9 +1417,9 @@ 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{
ActionsClientGetter: &ActionsClientSecretResolver{ ActionsClientPool: &ActionsClientPool{
Client: mgr.GetClient(), k8sClient: mgr.GetClient(),
MultiClient: actions.NewMultiClient(logr.Discard()), multiClient: actions.NewMultiClient(logr.Discard()),
}, },
}, },
} }

View File

@ -12,8 +12,9 @@ import (
"strings" "strings"
"github.com/actions/actions-runner-controller/apis/actions.github.com/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/build" "github.com/actions/actions-runner-controller/build"
listenerconfig "github.com/actions/actions-runner-controller/cmd/ghalistener/config" ghalistenerconfig "github.com/actions/actions-runner-controller/cmd/ghalistener/config"
"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"
@ -72,7 +73,7 @@ func SetListenerEntrypoint(entrypoint string) {
type ResourceBuilder struct { type ResourceBuilder struct {
ExcludeLabelPropagationPrefixes []string ExcludeLabelPropagationPrefixes []string
ActionsClientGetter *ActionsClientPool
} }
// boolPtr returns a pointer to a bool value // boolPtr returns a pointer to a bool value
@ -108,6 +109,10 @@ func (b *ResourceBuilder) newAutoScalingListener(autoscalingRunnerSet *v1alpha1.
annotationKeyValuesHash: autoscalingRunnerSet.Annotations[annotationKeyValuesHash], annotationKeyValuesHash: autoscalingRunnerSet.Annotations[annotationKeyValuesHash],
} }
if v, ok := autoscalingRunnerSet.Annotations[AnnotationKeyGitHubVaultType]; ok {
annotations[AnnotationKeyGitHubVaultType] = v
}
if err := applyGitHubURLLabels(autoscalingRunnerSet.Spec.GitHubConfigUrl, labels); err != nil { if err := applyGitHubURLLabels(autoscalingRunnerSet.Spec.GitHubConfigUrl, labels); err != nil {
return nil, fmt.Errorf("failed to apply GitHub URL labels: %v", err) return nil, fmt.Errorf("failed to apply GitHub URL labels: %v", err)
} }
@ -171,21 +176,8 @@ func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha
metricsEndpoint = metricsConfig.endpoint metricsEndpoint = metricsConfig.endpoint
} }
var appInstallationID int64 config := ghalistenerconfig.Config{
if id, ok := secret.Data["github_app_installation_id"]; ok {
var err error
appInstallationID, err = strconv.ParseInt(string(id), 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to convert github_app_installation_id to int: %v", err)
}
}
config := listenerconfig.Config{
ConfigureUrl: autoscalingListener.Spec.GitHubConfigUrl, ConfigureUrl: autoscalingListener.Spec.GitHubConfigUrl,
AppID: string(secret.Data["github_app_id"]),
AppInstallationID: appInstallationID,
AppPrivateKey: string(secret.Data["github_app_private_key"]),
Token: string(secret.Data["github_token"]),
EphemeralRunnerSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace, EphemeralRunnerSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
EphemeralRunnerSetName: autoscalingListener.Spec.EphemeralRunnerSetName, EphemeralRunnerSetName: autoscalingListener.Spec.EphemeralRunnerSetName,
MaxRunners: autoscalingListener.Spec.MaxRunners, MaxRunners: autoscalingListener.Spec.MaxRunners,
@ -200,6 +192,17 @@ func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha
Metrics: autoscalingListener.Spec.Metrics, Metrics: autoscalingListener.Spec.Metrics,
} }
if ty, ok := autoscalingListener.Annotations[AnnotationKeyGitHubVaultType]; !ok {
appConfig, err := appconfig.FromSecret(secret)
if err != nil {
return nil, fmt.Errorf("failed to parse appconfig from secret: %v", err)
}
config.AppConfig = *appConfig
} else {
config.VaultType = ty
config.VaultLookupKey = autoscalingListener.Spec.GitHubConfigSecret
}
if err := config.Validate(); err != nil { if err := config.Validate(); err != nil {
return nil, fmt.Errorf("invalid listener config: %w", err) return nil, fmt.Errorf("invalid listener config: %w", err)
} }
@ -220,7 +223,7 @@ func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha
}, nil }, nil
} }
func (b *ResourceBuilder) newScaleSetListenerPod(autoscalingListener *v1alpha1.AutoscalingListener, podConfig *corev1.Secret, serviceAccount *corev1.ServiceAccount, secret *corev1.Secret, metricsConfig *listenerMetricsServerConfig, envs ...corev1.EnvVar) (*corev1.Pod, error) { func (b *ResourceBuilder) newScaleSetListenerPod(autoscalingListener *v1alpha1.AutoscalingListener, podConfig *corev1.Secret, serviceAccount *corev1.ServiceAccount, metricsConfig *listenerMetricsServerConfig, envs ...corev1.EnvVar) (*corev1.Pod, error) {
listenerEnv := []corev1.EnvVar{ listenerEnv := []corev1.EnvVar{
{ {
Name: "LISTENER_CONFIG_PATH", Name: "LISTENER_CONFIG_PATH",
@ -535,6 +538,10 @@ func (b *ResourceBuilder) newEphemeralRunnerSet(autoscalingRunnerSet *v1alpha1.A
annotationKeyRunnerSpecHash: runnerSpecHash, annotationKeyRunnerSpecHash: runnerSpecHash,
} }
if v, ok := autoscalingRunnerSet.Annotations[AnnotationKeyGitHubVaultType]; ok {
newAnnotations[AnnotationKeyGitHubVaultType] = v
}
newEphemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{ newEphemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{
TypeMeta: metav1.TypeMeta{}, TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@ -583,6 +590,7 @@ func (b *ResourceBuilder) newEphemeralRunner(ephemeralRunnerSet *v1alpha1.Epheme
for key, val := range ephemeralRunnerSet.Annotations { for key, val := range ephemeralRunnerSet.Annotations {
annotations[key] = val annotations[key] = val
} }
annotations[AnnotationKeyPatchID] = strconv.Itoa(ephemeralRunnerSet.Spec.PatchID) annotations[AnnotationKeyPatchID] = strconv.Itoa(ephemeralRunnerSet.Spec.PatchID)
return &v1alpha1.EphemeralRunner{ return &v1alpha1.EphemeralRunner{
TypeMeta: metav1.TypeMeta{}, TypeMeta: metav1.TypeMeta{},

View File

@ -82,12 +82,7 @@ func TestLabelPropagation(t *testing.T) {
Name: "test", Name: "test",
}, },
} }
listenerSecret := &corev1.Secret{ listenerPod, err := b.newScaleSetListenerPod(listener, &corev1.Secret{}, listenerServiceAccount, nil)
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
}
listenerPod, err := b.newScaleSetListenerPod(listener, &corev1.Secret{}, listenerServiceAccount, listenerSecret, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, listenerPod.Labels, listener.Labels) assert.Equal(t, listenerPod.Labels, listener.Labels)

View File

@ -3,6 +3,7 @@ package fake
import ( import (
"context" "context"
"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/github/actions"
) )
@ -34,10 +35,6 @@ func NewMultiClient(opts ...MultiClientOption) actions.MultiClient {
return f return f
} }
func (f *fakeMultiClient) GetClientFor(ctx context.Context, githubConfigURL string, creds actions.ActionsAuth, namespace string, options ...actions.ClientOption) (actions.ActionsService, error) { func (f *fakeMultiClient) GetClientFor(ctx context.Context, githubConfigURL string, appConfig *appconfig.AppConfig, namespace string, options ...actions.ClientOption) (actions.ActionsService, error) {
return f.defaultClient, f.defaultErr
}
func (f *fakeMultiClient) GetClientFromSecret(ctx context.Context, githubConfigURL, namespace string, secretData actions.KubernetesSecretData, options ...actions.ClientOption) (actions.ActionsService, error) {
return f.defaultClient, f.defaultErr return f.defaultClient, f.defaultErr
} }

View File

@ -2,16 +2,14 @@ package actions
import ( import (
"context" "context"
"fmt"
"strconv"
"sync" "sync"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/go-logr/logr" "github.com/go-logr/logr"
) )
type MultiClient interface { type MultiClient interface {
GetClientFor(ctx context.Context, githubConfigURL string, creds ActionsAuth, namespace string, options ...ClientOption) (ActionsService, error) GetClientFor(ctx context.Context, githubConfigURL string, appConfig *appconfig.AppConfig, namespace string, options ...ClientOption) (ActionsService, error)
GetClientFromSecret(ctx context.Context, githubConfigURL, namespace string, secretData KubernetesSecretData, options ...ClientOption) (ActionsService, error)
} }
type multiClient struct { type multiClient struct {
@ -50,15 +48,22 @@ func NewMultiClient(logger logr.Logger) MultiClient {
} }
} }
func (m *multiClient) GetClientFor(ctx context.Context, githubConfigURL string, creds ActionsAuth, namespace string, options ...ClientOption) (ActionsService, error) { func (m *multiClient) GetClientFor(ctx context.Context, githubConfigURL string, appConfig *appconfig.AppConfig, namespace string, options ...ClientOption) (ActionsService, error) {
m.logger.Info("retrieve actions client", "githubConfigURL", githubConfigURL, "namespace", namespace) m.logger.Info("retrieve actions client", "githubConfigURL", githubConfigURL, "namespace", namespace)
if creds.Token == "" && creds.AppCreds == nil { if err := appConfig.Validate(); err != nil {
return nil, fmt.Errorf("no credentials provided. either a PAT or GitHub App credentials should be provided") return nil, err
} }
if creds.Token != "" && creds.AppCreds != nil { var creds ActionsAuth
return nil, fmt.Errorf("both PAT and GitHub App credentials provided. should only provide one") if len(appConfig.Token) > 0 {
creds.Token = appConfig.Token
} else {
creds.AppCreds = &GitHubAppAuth{
AppID: appConfig.AppID,
AppInstallationID: appConfig.AppInstallationID,
AppPrivateKey: appConfig.AppPrivateKey,
}
} }
client, err := NewClient( client, err := NewClient(
@ -94,42 +99,3 @@ func (m *multiClient) GetClientFor(ctx context.Context, githubConfigURL string,
return client, nil return client, nil
} }
type KubernetesSecretData map[string][]byte
func (m *multiClient) GetClientFromSecret(ctx context.Context, githubConfigURL, namespace string, secretData KubernetesSecretData, options ...ClientOption) (ActionsService, error) {
if len(secretData) == 0 {
return nil, fmt.Errorf("must provide secret data with either PAT or GitHub App Auth")
}
token := string(secretData["github_token"])
hasToken := len(token) > 0
appID := string(secretData["github_app_id"])
appInstallationID := string(secretData["github_app_installation_id"])
appPrivateKey := string(secretData["github_app_private_key"])
hasGitHubAppAuth := len(appID) > 0 && len(appInstallationID) > 0 && len(appPrivateKey) > 0
if hasToken && hasGitHubAppAuth {
return nil, fmt.Errorf("must provide secret with only PAT or GitHub App Auth to avoid ambiguity in client behavior")
}
if !hasToken && !hasGitHubAppAuth {
return nil, fmt.Errorf("neither PAT nor GitHub App Auth credentials provided in secret")
}
auth := ActionsAuth{}
if hasToken {
auth.Token = token
return m.GetClientFor(ctx, githubConfigURL, auth, namespace, options...)
}
parsedAppInstallationID, err := strconv.ParseInt(appInstallationID, 10, 64)
if err != nil {
return nil, err
}
auth.AppCreds = &GitHubAppAuth{AppID: appID, AppInstallationID: parsedAppInstallationID, AppPrivateKey: appPrivateKey}
return m.GetClientFor(ctx, githubConfigURL, auth, namespace, options...)
}

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -23,10 +24,13 @@ func TestMultiClientCaching(t *testing.T) {
defaultNamespace := "default" defaultNamespace := "default"
defaultConfigURL := "https://github.com/org/repo" defaultConfigURL := "https://github.com/org/repo"
defaultCreds := &ActionsAuth{ defaultCreds := &appconfig.AppConfig{
Token: "token", Token: "token",
} }
client, err := NewClient(defaultConfigURL, defaultCreds) defaultAuth := ActionsAuth{
Token: defaultCreds.Token,
}
client, err := NewClient(defaultConfigURL, &defaultAuth)
require.NoError(t, err) require.NoError(t, err)
multiClient.clients[ActionsClientKey{client.Identifier(), defaultNamespace}] = client multiClient.clients[ActionsClientKey{client.Identifier(), defaultNamespace}] = client
@ -35,7 +39,7 @@ func TestMultiClientCaching(t *testing.T) {
cachedClient, err := multiClient.GetClientFor( cachedClient, err := multiClient.GetClientFor(
ctx, ctx,
defaultConfigURL, defaultConfigURL,
*defaultCreds, defaultCreds,
defaultNamespace, defaultNamespace,
) )
require.NoError(t, err) require.NoError(t, err)
@ -47,7 +51,7 @@ func TestMultiClientCaching(t *testing.T) {
newClient, err := multiClient.GetClientFor( newClient, err := multiClient.GetClientFor(
ctx, ctx,
defaultConfigURL, defaultConfigURL,
*defaultCreds, defaultCreds,
otherNamespace, otherNamespace,
) )
require.NoError(t, err) require.NoError(t, err)
@ -63,7 +67,7 @@ func TestMultiClientOptions(t *testing.T) {
defaultConfigURL := "https://github.com/org/repo" defaultConfigURL := "https://github.com/org/repo"
t.Run("GetClientFor", func(t *testing.T) { t.Run("GetClientFor", func(t *testing.T) {
defaultCreds := &ActionsAuth{ defaultCreds := &appconfig.AppConfig{
Token: "token", Token: "token",
} }
@ -71,7 +75,7 @@ func TestMultiClientOptions(t *testing.T) {
service, err := multiClient.GetClientFor( service, err := multiClient.GetClientFor(
ctx, ctx,
defaultConfigURL, defaultConfigURL,
*defaultCreds, defaultCreds,
defaultNamespace, defaultNamespace,
) )
service.SetUserAgent(testUserAgent) service.SetUserAgent(testUserAgent)
@ -83,27 +87,6 @@ func TestMultiClientOptions(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, testUserAgent.String(), req.Header.Get("User-Agent")) assert.Equal(t, testUserAgent.String(), req.Header.Get("User-Agent"))
}) })
t.Run("GetClientFromSecret", func(t *testing.T) {
secret := map[string][]byte{
"github_token": []byte("token"),
}
multiClient := NewMultiClient(logger)
service, err := multiClient.GetClientFromSecret(
ctx,
defaultConfigURL,
defaultNamespace,
secret,
)
service.SetUserAgent(testUserAgent)
require.NoError(t, err)
client := service.(*Client)
req, err := client.NewGitHubAPIRequest(ctx, "GET", "/test", nil)
require.NoError(t, err)
assert.Equal(t, testUserAgent.String(), req.Header.Get("User-Agent"))
})
} }
func TestCreateJWT(t *testing.T) { func TestCreateJWT(t *testing.T) {

8
go.mod
View File

@ -4,8 +4,8 @@ go 1.24.3
require ( require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0 github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0
github.com/bradleyfalzon/ghinstallation/v2 v2.14.0 github.com/bradleyfalzon/ghinstallation/v2 v2.14.0
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/evanphx/json-patch v5.9.11+incompatible github.com/evanphx/json-patch v5.9.11+incompatible
@ -43,8 +43,8 @@ require (
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect

24
go.sum
View File

@ -3,20 +3,20 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4
github.com/Azure/azure-sdk-for-go v51.0.0+incompatible h1:p7blnyJSjJqf5jflHbSGhIhEpXIgIFmYZNg5uwqweso= github.com/Azure/azure-sdk-for-go v51.0.0+incompatible h1:p7blnyJSjJqf5jflHbSGhIhEpXIgIFmYZNg5uwqweso=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 h1:1mvYtZfWQAnwNah/C+Z+Jb9rQH95LPE2vlmMuWAHJk8=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1/go.mod h1:75I/mXtme1JyWFtz8GocPHVFyH421IBoZErnO16dd0k=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1 h1:Bk5uOhSAenHyR5P61D/NzeQCv+4fEVV8mOkJ82NqpWw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1/go.mod h1:QZ4pw3or1WPmRBxf0cHd1tknzrT54WPBOQoGutCPvSU=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0 h1:xnO4sFyG8UH2fElBkcqLTOZsAajvKfnSlgBBW8dXYjw= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 h1:WLUIpeyv04H0RCcQHaA4TNoyrQ39Ox7V+re+iaqzTe0=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0/go.mod h1:XD3DIOOVgBCO03OleB1fHjgktVRFxlT++KwKgIOewdM= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0/go.mod h1:hd8hTTIY3VmUVPRHNH7GVCHO3SHgXkJKZHReby/bnUQ=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 h1:FbH3BbSb4bvGluTesZZ+ttN/MDsnMmQP36OSnDuSXqw= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
@ -309,8 +309,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=

53
main.go
View File

@ -33,7 +33,6 @@ import (
"github.com/actions/actions-runner-controller/github/actions" "github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/actions-runner-controller/logging" "github.com/actions/actions-runner-controller/logging"
"github.com/actions/actions-runner-controller/vault" "github.com/actions/actions-runner-controller/vault"
"github.com/actions/actions-runner-controller/vault/azurekeyvault"
"github.com/kelseyhightower/envconfig" "github.com/kelseyhightower/envconfig"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -276,20 +275,30 @@ func main() {
log.WithName("actions-clients"), log.WithName("actions-clients"),
) )
actionsClientGetter, err := newActionsClientGetter( vaults, err := vault.InitAll("CONTROLLER_MANAGER_")
mgr.GetClient(),
actionsMultiClient,
)
if err != nil { if err != nil {
log.Error(err, "unable to create actions client resolver") log.Error(err, "unable to read vaults")
os.Exit(1) os.Exit(1)
} }
var poolOptions []actionsgithubcom.ActionsClientPoolOption
for name, vault := range vaults {
poolOptions = append(poolOptions, actionsgithubcom.WithVault(name, vault))
}
clientPool := actionsgithubcom.NewActionsClientPool(
mgr.GetClient(),
actionsMultiClient,
poolOptions...,
)
rb := actionsgithubcom.ResourceBuilder{ rb := actionsgithubcom.ResourceBuilder{
ExcludeLabelPropagationPrefixes: excludeLabelPropagationPrefixes, ExcludeLabelPropagationPrefixes: excludeLabelPropagationPrefixes,
ActionsClientGetter: actionsClientGetter, ActionsClientPool: clientPool,
} }
log.Info("Resource builder initializing")
if err = (&actionsgithubcom.AutoscalingRunnerSetReconciler{ if err = (&actionsgithubcom.AutoscalingRunnerSetReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Log: log.WithName("AutoscalingRunnerSet").WithValues("version", build.Version), Log: log.WithName("AutoscalingRunnerSet").WithValues("version", build.Version),
@ -309,7 +318,6 @@ func main() {
Client: mgr.GetClient(), Client: mgr.GetClient(),
Log: log.WithName("EphemeralRunner").WithValues("version", build.Version), Log: log.WithName("EphemeralRunner").WithValues("version", build.Version),
Scheme: mgr.GetScheme(), Scheme: mgr.GetScheme(),
ActionsClient: actionsMultiClient,
ResourceBuilder: rb, ResourceBuilder: rb,
}).SetupWithManager(mgr, actionsgithubcom.WithMaxConcurrentReconciles(opts.RunnerMaxConcurrentReconciles)); err != nil { }).SetupWithManager(mgr, actionsgithubcom.WithMaxConcurrentReconciles(opts.RunnerMaxConcurrentReconciles)); err != nil {
log.Error(err, "unable to create controller", "controller", "EphemeralRunner") log.Error(err, "unable to create controller", "controller", "EphemeralRunner")
@ -503,32 +511,3 @@ func (s *commaSeparatedStringSlice) Set(value string) error {
} }
return nil return nil
} }
func newActionsClientGetter(k8sClient client.Client, multiClient actions.MultiClient) (actionsgithubcom.ActionsClientGetter, error) {
vaultType := os.Getenv("CONTROLLER_MANAGER_VAULT_TYPE")
if vaultType == "" {
return &actionsgithubcom.ActionsClientSecretResolver{
Client: k8sClient,
MultiClient: multiClient,
}, nil
}
key := os.Getenv("CONTROLLER_MANAGER_VAULT_API_KEY")
var vault vault.Vault
switch vaultType {
case "azure":
v, err := azurekeyvault.New(azurekeyvault.Config{JWT: key})
if err != nil {
return nil, fmt.Errorf("failed to create Azure Key Vault client: %w", err)
}
vault = v
default:
return nil, fmt.Errorf("unsupported vault type: %q", vaultType)
}
return &actionsgithubcom.ActionsClientVaultResolver{
Vault: vault,
MultiClient: multiClient,
}, nil
}

129
proxyconfig/proxyconfig.go Normal file
View File

@ -0,0 +1,129 @@
package proxyconfig
import (
"fmt"
"net/url"
"os"
"strings"
"golang.org/x/net/http/httpproxy"
)
type ProxyConfig struct {
HTTP *ProxyServerConfig `json:"http,omitempty"`
HTTPS *ProxyServerConfig `json:"https,omitempty"`
NoProxy []string `json:"no_proxy,omitempty"`
}
func (pc *ProxyConfig) Validate() error {
if pc == nil {
return nil
}
if pc.HTTP != nil {
_, err := url.Parse(pc.HTTP.URL)
if err != nil {
return fmt.Errorf("proxy http set with invalid url: %v", err)
}
}
if pc.HTTPS != nil {
_, err := url.Parse(pc.HTTPS.URL)
if err != nil {
return fmt.Errorf("proxy https set with invalid url: %v", err)
}
}
// TODO: maybe validate noproxy?
return nil
}
func (c *ProxyConfig) ProxyConfig() (*httpproxy.Config, error) {
if c == nil {
return nil, nil
}
config := &httpproxy.Config{
NoProxy: strings.Join(c.NoProxy, ","),
}
if c.HTTP != nil {
u, err := c.HTTP.proxyURL()
if err != nil {
return nil, fmt.Errorf("failed to create proxy http url: %w", err)
}
config.HTTPProxy = u.String()
}
if c.HTTPS != nil {
u, err := c.HTTPS.proxyURL()
if err != nil {
return nil, fmt.Errorf("failed to create proxy https url: %w", err)
}
config.HTTPSProxy = u.String()
}
return config, nil
}
type ProxyServerConfig struct {
URL string `json:"url"`
Username string `json:"username"`
Password string `json:"password"`
}
func (c *ProxyServerConfig) proxyURL() (*url.URL, error) {
u, err := url.Parse(c.URL)
if err != nil {
return nil, fmt.Errorf("failed to parse proxy url %q: %w", c.URL, err)
}
u.User = url.UserPassword(
c.Username,
c.Password,
)
return u, nil
}
func ReadFromEnv(prefix string) (*ProxyConfig, error) {
url := os.Getenv(prefix + "HTTP_URL")
username := os.Getenv(prefix + "HTTP_USERNAME")
password := os.Getenv(prefix + "HTTP_PASSWORD")
var http *ProxyServerConfig
if url != "" || username != "" || password != "" {
http = &ProxyServerConfig{
URL: url,
Username: username,
Password: password,
}
}
url = os.Getenv(prefix + "HTTPS_URL")
username = os.Getenv(prefix + "HTTPS_USERNAME")
password = os.Getenv(prefix + "HTTPS_PASSWORD")
var https *ProxyServerConfig
if url != "" || username != "" || password != "" {
https = &ProxyServerConfig{
URL: url,
Username: username,
Password: password,
}
}
noProxyRaw := os.Getenv(prefix + "NO_PROXY")
if http == nil && https == nil && noProxyRaw == "" {
return nil, nil
}
var noProxy []string
if noProxyRaw != "" {
noProxy = strings.Split(noProxyRaw, ",")
}
return &ProxyConfig{
HTTP: http,
HTTPS: https,
NoProxy: noProxy,
}, nil
}

View File

@ -0,0 +1,104 @@
package proxyconfig
import (
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type kv struct {
key string
value string
}
func TestReadFromEnvNoPrefix(t *testing.T) {
var (
url = "example.com"
username = "user"
password = "password"
noProxy = "test.com,other.com"
)
tt := map[string]struct {
envs []*kv
want *ProxyConfig
}{
"no envs": {},
"http only": {
envs: []*kv{
{"HTTP_URL", url},
{"HTTP_USERNAME", username},
{"HTTP_PASSWORD", password},
},
want: &ProxyConfig{
HTTP: &ProxyServerConfig{
URL: url,
Username: username,
Password: password,
},
},
},
"https only": {
envs: []*kv{
{"HTTPS_URL", url},
{"HTTPS_USERNAME", username},
{"HTTPS_PASSWORD", password},
},
want: &ProxyConfig{
HTTPS: &ProxyServerConfig{
URL: url,
Username: username,
Password: password,
},
},
},
"no proxy only": {
envs: []*kv{
{"NO_PROXY", noProxy},
},
want: &ProxyConfig{
NoProxy: strings.Split(noProxy, ","),
},
},
"all set": {
envs: []*kv{
{"HTTP_URL", url},
{"HTTP_USERNAME", username},
{"HTTP_PASSWORD", password},
{"HTTPS_URL", url},
{"HTTPS_USERNAME", username},
{"HTTPS_PASSWORD", password},
{"NO_PROXY", noProxy},
},
want: &ProxyConfig{
HTTP: &ProxyServerConfig{
URL: url,
Username: username,
Password: password,
},
HTTPS: &ProxyServerConfig{
URL: url,
Username: username,
Password: password,
},
NoProxy: strings.Split(noProxy, ","),
},
},
}
for name, tc := range tt {
t.Run(name, func(t *testing.T) {
os.Clearenv()
for _, kv := range tc.envs {
os.Setenv(kv.key, kv.value)
}
got, err := ReadFromEnv("")
require.NoError(t, err)
assert.Equal(t, tc.want, got)
})
}
}

View File

@ -126,7 +126,6 @@ func TestARCJobs(t *testing.T) {
if !success { if !success {
t.Fatal("Expected pods count did not match available pods count during job run.") t.Fatal("Expected pods count did not match available pods count during job run.")
} }
}, },
) )
t.Run("Get available pods after job run", func(t *testing.T) { t.Run("Get available pods after job run", func(t *testing.T) {

View File

@ -3,50 +3,58 @@ package azurekeyvault
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/actions/actions-runner-controller/proxyconfig"
"github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets"
) )
// AzureKeyVault is a struct that holds the Azure Key Vault client.
type AzureKeyVault struct { type AzureKeyVault struct {
client *azsecrets.Client client *azsecrets.Client
} }
type Config struct {
ClientID string
TenantID string
JWT string
URL string
}
func (c *Config) getAssertion(ctx context.Context) (string, error) {
return c.JWT, nil
}
func New(cfg Config) (*AzureKeyVault, error) { func New(cfg Config) (*AzureKeyVault, error) {
cred, err := azidentity.NewClientAssertionCredential( if err := cfg.Validate(); err != nil {
cfg.TenantID, return nil, fmt.Errorf("failed to validate config: %v", err)
cfg.ClientID,
cfg.getAssertion,
&azidentity.ClientAssertionCredentialOptions{
ClientOptions: azcore.ClientOptions{},
},
)
if err != nil {
return nil, fmt.Errorf("failed to create client assertion credential: %w", err)
} }
client, err := azsecrets.NewClient(cfg.URL, cred, nil) client, err := cfg.Client()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to initialize keyvault client: %w", err) return nil, fmt.Errorf("failed to create azsecrets client from config: %v", err)
} }
return &AzureKeyVault{client: client}, nil return &AzureKeyVault{client: client}, nil
} }
func (v *AzureKeyVault) GetSecret(ctx context.Context, name, version string) (string, error) { // FromEnv creates a new AzureKeyVault instance from environment variables.
secret, err := v.client.GetSecret(context.Background(), name, version, nil) // The environment variables should be prefixed with the provided prefix.
// For example, if the prefix is "AZURE_KEY_VAULT_", the environment variables should be:
// AZURE_KEY_VAULT_TENANT_ID, AZURE_KEY_VAULT_CLIENT_ID, AZURE_KEY_VAULT_URL,
// AZURE_KEY_VAULT_CERT_PATH, AZURE_KEY_VAULT_CERT_PASSWORD.
// The proxy configuration can be set using the environment variables prefixed with "PROXY_".
// For example, AZURE_KEY_VAULT_PROXY_HTTP_URL, AZURE_KEY_VAULT_PROXY_HTTP_USERNAME, etc.
func FromEnv(prefix string) (*AzureKeyVault, error) {
cfg := Config{
TenantID: os.Getenv(prefix + "TENANT_ID"),
ClientID: os.Getenv(prefix + "CLIENT_ID"),
URL: os.Getenv(prefix + "URL"),
CertPath: os.Getenv(prefix + "CERT_PATH"),
CertPassword: os.Getenv(prefix + "CERT_PASSWORD"),
}
proxyConfig, err := proxyconfig.ReadFromEnv(prefix + "PROXY_")
if err != nil {
return nil, fmt.Errorf("failed to read proxy config: %v", err)
}
cfg.Proxy = proxyConfig
return New(cfg)
}
// GetSecret retrieves a secret from Azure Key Vault.
func (v *AzureKeyVault) GetSecret(ctx context.Context, name string) (string, error) {
secret, err := v.client.GetSecret(ctx, name, "", nil)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get secret: %w", err) return "", fmt.Errorf("failed to get secret: %w", err)
} }

View File

@ -0,0 +1,119 @@
package azurekeyvault
import (
"errors"
"fmt"
"net/http"
"net/url"
"os"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets"
"github.com/actions/actions-runner-controller/proxyconfig"
"github.com/hashicorp/go-retryablehttp"
)
// AzureKeyVault is a struct that holds the Azure Key Vault client.
type Config struct {
TenantID string `json:"tenant_id"`
ClientID string `json:"client_id"`
URL string `json:"url"`
CertPath string `json:"cert_path"`
CertPassword string `json:"cert_password"` // optional
Proxy *proxyconfig.ProxyConfig `json:"proxy,omitempty"`
}
func (c *Config) Validate() error {
if c.TenantID == "" {
return errors.New("tenant_id is not set")
}
if c.ClientID == "" {
return errors.New("client_id is not set")
}
if _, err := url.Parse(c.URL); err != nil {
return fmt.Errorf("failed to parse url: %v", err)
}
if c.CertPath != "" {
return errors.New("cert path must be provided")
}
if err := c.Proxy.Validate(); err != nil {
return fmt.Errorf("proxy validation failed: %v", err)
}
return nil
}
// Client creates a new Azure Key Vault client using the provided configuration.
func (c *Config) Client() (*azsecrets.Client, error) {
return c.certClient()
}
func (c *Config) certClient() (*azsecrets.Client, error) {
data, err := os.ReadFile(c.CertPath)
if err != nil {
return nil, fmt.Errorf("failed to read cert file from path %q: %v", c.CertPath, err)
}
certs, key, err := azidentity.ParseCertificates(data, []byte(c.CertPassword))
if err != nil {
return nil, fmt.Errorf("failed to parse certificates: %w", err)
}
httpClient, err := c.httpClient()
if err != nil {
return nil, fmt.Errorf("failed to instantiate http client: %v", err)
}
cred, err := azidentity.NewClientCertificateCredential(
c.TenantID,
c.ClientID,
certs,
key,
&azidentity.ClientCertificateCredentialOptions{
ClientOptions: policy.ClientOptions{
Transport: httpClient,
},
},
)
if err != nil {
return nil, fmt.Errorf("failed to create client certificate credential: %v", err)
}
client, err := azsecrets.NewClient(c.URL, cred, &azsecrets.ClientOptions{
ClientOptions: policy.ClientOptions{
Transport: httpClient,
},
})
if err != nil {
return nil, fmt.Errorf("failed to instantiate client for azsecrets: %v", err)
}
return client, nil
}
func (c *Config) httpClient() (*http.Client, error) {
retryClient := retryablehttp.NewClient()
retryClient.RetryMax = 4
retryClient.RetryWaitMax = 30 * time.Second
retryClient.HTTPClient.Timeout = 5 * time.Minute
transport, ok := retryClient.HTTPClient.Transport.(*http.Transport)
if !ok {
return nil, fmt.Errorf("failed to get http transport")
}
if c.Proxy != nil {
pc, err := c.Proxy.ProxyConfig()
if err != nil {
return nil, fmt.Errorf("failed to create proxy config: %v", err)
}
transport.Proxy = func(req *http.Request) (*url.URL, error) {
return pc.ProxyFunc()(req.URL)
}
}
return retryClient.StandardClient(), nil
}

View File

@ -0,0 +1,159 @@
package azurekeyvault
import (
"os"
"testing"
"github.com/actions/actions-runner-controller/proxyconfig"
"github.com/stretchr/testify/require"
)
func TestConfigValidate_invalid(t *testing.T) {
tenantID := "tenantID"
clientID := "clientID"
url := "https://example.com"
cp, err := os.CreateTemp("", "")
require.NoError(t, err)
err = cp.Close()
require.NoError(t, err)
certPath := cp.Name()
t.Cleanup(func() {
os.Remove(certPath)
})
proxy := &proxyconfig.ProxyConfig{
HTTP: &proxyconfig.ProxyServerConfig{
URL: "http://httpconfig.com",
Username: "user",
Password: "pass",
},
HTTPS: &proxyconfig.ProxyServerConfig{
URL: "https://httpsconfig.com",
Username: "user",
Password: "pass",
},
NoProxy: []string{
"http://noproxy.com",
},
}
tt := map[string]*Config{
"empty": {},
"no tenant id": {
TenantID: "",
ClientID: clientID,
URL: url,
CertPath: certPath,
CertPassword: "",
Proxy: proxy,
},
"no client id": {
TenantID: tenantID,
ClientID: "",
URL: url,
CertPath: certPath,
CertPassword: "",
Proxy: proxy,
},
"no url": {
TenantID: tenantID,
ClientID: clientID,
URL: "",
CertPath: certPath,
CertPassword: "",
Proxy: proxy,
},
"no jwt and no cert path": {
TenantID: tenantID,
ClientID: clientID,
URL: url,
CertPath: "",
CertPassword: "",
Proxy: proxy,
},
"invalid proxy": {
TenantID: tenantID,
ClientID: clientID,
URL: url,
CertPath: certPath,
CertPassword: "",
Proxy: &proxyconfig.ProxyConfig{
HTTP: &proxyconfig.ProxyServerConfig{},
},
},
}
for name, cfg := range tt {
t.Run(name, func(t *testing.T) {
err := cfg.Validate()
require.Error(t, err)
})
}
}
func TestValidate_valid(t *testing.T) {
tenantID := "tenantID"
clientID := "clientID"
url := "https://example.com"
cp, err := os.CreateTemp("", "")
require.NoError(t, err)
err = cp.Close()
require.NoError(t, err)
certPath := cp.Name()
t.Cleanup(func() {
os.Remove(certPath)
})
proxy := &proxyconfig.ProxyConfig{
HTTP: &proxyconfig.ProxyServerConfig{
URL: "http://httpconfig.com",
Username: "user",
Password: "pass",
},
HTTPS: &proxyconfig.ProxyServerConfig{
URL: "https://httpsconfig.com",
Username: "user",
Password: "pass",
},
NoProxy: []string{
"http://noproxy.com",
},
}
tt := map[string]*Config{
"with jwt": {
TenantID: tenantID,
ClientID: clientID,
URL: url,
CertPath: "",
CertPassword: "",
Proxy: proxy,
},
"with cert": {
TenantID: tenantID,
ClientID: clientID,
URL: url,
CertPath: certPath,
CertPassword: "",
Proxy: proxy,
},
"without proxy": {
TenantID: tenantID,
ClientID: clientID,
URL: url,
CertPath: certPath,
CertPassword: "",
},
}
for name, cfg := range tt {
t.Run(name, func(t *testing.T) {
err := cfg.Validate()
require.NoError(t, err)
})
}
}

View File

@ -1,7 +1,49 @@
package vault package vault
import "context" import (
"context"
"fmt"
"os"
"strings"
"github.com/actions/actions-runner-controller/vault/azurekeyvault"
)
// Vault is the interface every vault implementation needs to adhere to
type Vault interface { type Vault interface {
GetSecret(ctx context.Context, name, version string) (string, error) GetSecret(ctx context.Context, name string) (string, error)
}
// VaultType is the type of vault supported
const (
VaultTypeAzureKeyVault = "azure_key_vault"
)
// Compile-time checks
var _ Vault = (*azurekeyvault.AzureKeyVault)(nil)
// InitAll initializes all vaults based on the environment variables
// that start with the given prefix. It returns a map of vault types to their
// corresponding vault instances.
//
// Prefix is the namespace prefix used to filter environment variables.
// For example, the listener environment variable are prefixed with "LISTENER_", followed by the vault type, followed by the value.
//
// 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".
func InitAll(prefix string) (map[string]Vault, error) {
envs := os.Environ()
result := make(map[string]Vault)
for _, env := range envs {
if strings.HasPrefix(env, prefix+"AZURE_KEY_VAULT_") {
akv, err := azurekeyvault.FromEnv(prefix + "AZURE_KEY_VAULT_")
if err != nil {
return nil, fmt.Errorf("failed to instantiate azure key vault from env: %v", err)
}
result[VaultTypeAzureKeyVault] = akv
}
}
return result, nil
} }

34
vault/vault_test.go Normal file
View File

@ -0,0 +1,34 @@
package vault_test
import (
"os"
"testing"
"github.com/actions/actions-runner-controller/vault"
"github.com/actions/actions-runner-controller/vault/azurekeyvault"
"github.com/stretchr/testify/require"
)
func TestInitAll_AzureKeyVault(t *testing.T) {
os.Clearenv()
os.Setenv("LISTENER_AZURE_KEY_VAULT_TENANT_ID", "tenantID")
os.Setenv("LISTENER_AZURE_KEY_VAULT_CLIENT_ID", "clientID")
os.Setenv("LISTENER_AZURE_KEY_VAULT_URL", "https://example.com")
os.Setenv("LISTENER_AZURE_KEY_VAULT_CERT_PATH", "/path/to/cert")
os.Setenv("LISTENER_AZURE_KEY_VAULT_CERT_PASSWORD", "password")
os.Setenv("LISTENER_AZURE_KEY_VAULT_PROXY_HTTP_URL", "http://proxy.example.com")
os.Setenv("LISTENER_AZURE_KEY_VAULT_PROXY_HTTP_USERNAME", "username")
os.Setenv("LISTENER_AZURE_KEY_VAULT_PROXY_HTTP_PASSWORD", "password")
os.Setenv("LISTENER_AZURE_KEY_VAULT_PROXY_HTTPS_URL", "https://proxy.example.com")
os.Setenv("LISTENER_AZURE_KEY_VAULT_PROXY_HTTPS_USERNAME", "username")
os.Setenv("LISTENER_AZURE_KEY_VAULT_PROXY_HTTPS_PASSWORD", "password")
os.Setenv("LISTENER_AZURE_KEY_VAULT_PROXY_NO_PROXY", "temp.com")
vaults, err := vault.InitAll("LISTENER_")
require.NoError(t, err)
require.Len(t, vaults, 1)
require.Contains(t, vaults, vault.VaultTypeAzureKeyVault)
akv, ok := vaults[vault.VaultTypeAzureKeyVault].(*azurekeyvault.AzureKeyVault)
require.True(t, ok)
require.NotNil(t, akv)
}