Avoid envs and extend CRDs with vault config; remove controller dependency completely

This commit is contained in:
Nikola Jokic 2025-06-06 17:42:06 +02:00
parent 69dfa4e375
commit c3ce54cabc
No known key found for this signature in database
GPG Key ID: E4104494F9B8DDF6
30 changed files with 723 additions and 225 deletions

View File

@ -59,7 +59,10 @@ type AutoscalingListenerSpec struct {
Proxy *ProxyConfig `json:"proxy,omitempty"`
// +optional
GitHubServerTLS *GitHubServerTLSConfig `json:"githubServerTLS,omitempty"`
GitHubServerTLS *TLSConfig `json:"githubServerTLS,omitempty"`
// +optional
VaultConfig *VaultConfig `json:"vaultConfig,omitempty"`
// +optional
Metrics *MetricsConfig `json:"metrics,omitempty"`
@ -98,7 +101,7 @@ func (l *AutoscalingListener) Proxy() *ProxyConfig {
return l.Spec.Proxy
}
func (l *AutoscalingListener) GitHubServerTLS() *GitHubServerTLSConfig {
func (l *AutoscalingListener) GitHubServerTLS() *TLSConfig {
return l.Spec.GitHubServerTLS
}

View File

@ -24,6 +24,7 @@ import (
"strings"
"github.com/actions/actions-runner-controller/hash"
"github.com/actions/actions-runner-controller/vault"
"golang.org/x/net/http/httpproxy"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -69,7 +70,13 @@ type AutoscalingRunnerSetSpec struct {
Proxy *ProxyConfig `json:"proxy,omitempty"`
// +optional
GitHubServerTLS *GitHubServerTLSConfig `json:"githubServerTLS,omitempty"`
GitHubServerTLS *TLSConfig `json:"githubServerTLS,omitempty"`
// +optional
VaultServerTLS *TLSConfig `json:"vaultServerTLS,omitempty"`
// +optional
VaultConfig *VaultConfig `json:"vaultConfig,omitempty"`
// Required
Template corev1.PodTemplateSpec `json:"template,omitempty"`
@ -89,12 +96,12 @@ type AutoscalingRunnerSetSpec struct {
MinRunners *int `json:"minRunners,omitempty"`
}
type GitHubServerTLSConfig struct {
type TLSConfig struct {
// Required
CertificateFrom *TLSCertificateSource `json:"certificateFrom,omitempty"`
}
func (c *GitHubServerTLSConfig) ToCertPool(keyFetcher func(name, key string) ([]byte, error)) (*x509.CertPool, error) {
func (c *TLSConfig) ToCertPool(keyFetcher func(name, key string) ([]byte, error)) (*x509.CertPool, error) {
if c.CertificateFrom == nil {
return nil, fmt.Errorf("certificateFrom not specified")
}
@ -235,6 +242,26 @@ type ProxyServerConfig struct {
CredentialSecretRef string `json:"credentialSecretRef,omitempty"`
}
type VaultConfig struct {
// +optional
Type vault.VaultType `json:"type,omitempty"`
// +optional
AzureKeyVault *AzureKeyVaultConfig `json:"azureKeyVault,omitempty"`
// +optional
Proxy *ProxyConfig `json:"proxy,omitempty"`
}
type AzureKeyVaultConfig struct {
// +required
URL string `json:"url,omitempty"`
// +required
TenantID string `json:"tenantId,omitempty"`
// +required
ClientID string `json:"clientId,omitempty"`
// +required
CertificatePath string `json:"certificatePath,omitempty"`
}
// MetricsConfig holds configuration parameters for each metric type
type MetricsConfig struct {
// +optional
@ -293,11 +320,15 @@ func (ars *AutoscalingRunnerSet) GitHubConfigUrl() string {
return ars.Spec.GitHubConfigUrl
}
func (ars *AutoscalingRunnerSet) VaultConfig() *VaultConfig {
return ars.Spec.VaultConfig
}
func (ars *AutoscalingRunnerSet) Proxy() *ProxyConfig {
return ars.Spec.Proxy
}
func (ars *AutoscalingRunnerSet) GitHubServerTLS() *GitHubServerTLSConfig {
func (ars *AutoscalingRunnerSet) GitHubServerTLS() *TLSConfig {
return ars.Spec.GitHubServerTLS
}
@ -308,7 +339,7 @@ func (ars *AutoscalingRunnerSet) RunnerSetSpecHash() string {
RunnerGroup string
RunnerScaleSetName string
Proxy *ProxyConfig
GitHubServerTLS *GitHubServerTLSConfig
GitHubServerTLS *TLSConfig
Template corev1.PodTemplateSpec
}
spec := &runnerSetSpec{

View File

@ -79,10 +79,14 @@ func (er *EphemeralRunner) Proxy() *ProxyConfig {
return er.Spec.Proxy
}
func (er *EphemeralRunner) GitHubServerTLS() *GitHubServerTLSConfig {
func (er *EphemeralRunner) GitHubServerTLS() *TLSConfig {
return er.Spec.GitHubServerTLS
}
func (ars *EphemeralRunner) VaultConfig() *VaultConfig {
return ars.Spec.VaultConfig
}
// EphemeralRunnerSpec defines the desired state of EphemeralRunner
type EphemeralRunnerSpec struct {
// +required
@ -101,7 +105,10 @@ type EphemeralRunnerSpec struct {
ProxySecretRef string `json:"proxySecretRef,omitempty"`
// +optional
GitHubServerTLS *GitHubServerTLSConfig `json:"githubServerTLS,omitempty"`
GitHubServerTLS *TLSConfig `json:"githubServerTLS,omitempty"`
// +optional
VaultConfig *VaultConfig `json:"vaultConfig,omitempty"`
corev1.PodTemplateSpec `json:",inline"`
}

View File

@ -72,10 +72,14 @@ func (ers *EphemeralRunnerSet) Proxy() *ProxyConfig {
return ers.Spec.EphemeralRunnerSpec.Proxy
}
func (ers *EphemeralRunnerSet) GitHubServerTLS() *GitHubServerTLSConfig {
func (ers *EphemeralRunnerSet) GitHubServerTLS() *TLSConfig {
return ers.Spec.EphemeralRunnerSpec.GitHubServerTLS
}
func (ars *EphemeralRunnerSet) VaultConfig() *VaultConfig {
return ars.Spec.EphemeralRunnerSpec.VaultConfig
}
// EphemeralRunnerSetList contains a list of EphemeralRunnerSet
// +kubebuilder:object:root=true
type EphemeralRunnerSetList struct {

View File

@ -17,7 +17,7 @@ import (
func TestGitHubServerTLSConfig_ToCertPool(t *testing.T) {
t.Run("returns an error if CertificateFrom not specified", func(t *testing.T) {
c := &v1alpha1.GitHubServerTLSConfig{
c := &v1alpha1.TLSConfig{
CertificateFrom: nil,
}
@ -29,7 +29,7 @@ func TestGitHubServerTLSConfig_ToCertPool(t *testing.T) {
})
t.Run("returns an error if CertificateFrom.ConfigMapKeyRef not specified", func(t *testing.T) {
c := &v1alpha1.GitHubServerTLSConfig{
c := &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{},
}
@ -41,7 +41,7 @@ func TestGitHubServerTLSConfig_ToCertPool(t *testing.T) {
})
t.Run("returns a valid cert pool with correct configuration", func(t *testing.T) {
c := &v1alpha1.GitHubServerTLSConfig{
c := &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &v1.ConfigMapKeySelector{
LocalObjectReference: v1.LocalObjectReference{

View File

@ -100,7 +100,12 @@ func (in *AutoscalingListenerSpec) DeepCopyInto(out *AutoscalingListenerSpec) {
}
if in.GitHubServerTLS != nil {
in, out := &in.GitHubServerTLS, &out.GitHubServerTLS
*out = new(GitHubServerTLSConfig)
*out = new(TLSConfig)
(*in).DeepCopyInto(*out)
}
if in.VaultConfig != nil {
in, out := &in.VaultConfig, &out.VaultConfig
*out = new(VaultConfig)
(*in).DeepCopyInto(*out)
}
if in.Metrics != nil {
@ -209,7 +214,17 @@ func (in *AutoscalingRunnerSetSpec) DeepCopyInto(out *AutoscalingRunnerSetSpec)
}
if in.GitHubServerTLS != nil {
in, out := &in.GitHubServerTLS, &out.GitHubServerTLS
*out = new(GitHubServerTLSConfig)
*out = new(TLSConfig)
(*in).DeepCopyInto(*out)
}
if in.VaultServerTLS != nil {
in, out := &in.VaultServerTLS, &out.VaultServerTLS
*out = new(TLSConfig)
(*in).DeepCopyInto(*out)
}
if in.VaultConfig != nil {
in, out := &in.VaultConfig, &out.VaultConfig
*out = new(VaultConfig)
(*in).DeepCopyInto(*out)
}
in.Template.DeepCopyInto(&out.Template)
@ -260,6 +275,21 @@ func (in *AutoscalingRunnerSetStatus) DeepCopy() *AutoscalingRunnerSetStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AzureKeyVaultConfig) DeepCopyInto(out *AzureKeyVaultConfig) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureKeyVaultConfig.
func (in *AzureKeyVaultConfig) DeepCopy() *AzureKeyVaultConfig {
if in == nil {
return nil
}
out := new(AzureKeyVaultConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CounterMetric) DeepCopyInto(out *CounterMetric) {
*out = *in
@ -439,7 +469,12 @@ func (in *EphemeralRunnerSpec) DeepCopyInto(out *EphemeralRunnerSpec) {
}
if in.GitHubServerTLS != nil {
in, out := &in.GitHubServerTLS, &out.GitHubServerTLS
*out = new(GitHubServerTLSConfig)
*out = new(TLSConfig)
(*in).DeepCopyInto(*out)
}
if in.VaultConfig != nil {
in, out := &in.VaultConfig, &out.VaultConfig
*out = new(VaultConfig)
(*in).DeepCopyInto(*out)
}
in.PodTemplateSpec.DeepCopyInto(&out.PodTemplateSpec)
@ -497,26 +532,6 @@ func (in *GaugeMetric) DeepCopy() *GaugeMetric {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GitHubServerTLSConfig) DeepCopyInto(out *GitHubServerTLSConfig) {
*out = *in
if in.CertificateFrom != nil {
in, out := &in.CertificateFrom, &out.CertificateFrom
*out = new(TLSCertificateSource)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubServerTLSConfig.
func (in *GitHubServerTLSConfig) DeepCopy() *GitHubServerTLSConfig {
if in == nil {
return nil
}
out := new(GitHubServerTLSConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HistogramMetric) DeepCopyInto(out *HistogramMetric) {
*out = *in
@ -669,3 +684,48 @@ func (in *TLSCertificateSource) DeepCopy() *TLSCertificateSource {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSConfig) DeepCopyInto(out *TLSConfig) {
*out = *in
if in.CertificateFrom != nil {
in, out := &in.CertificateFrom, &out.CertificateFrom
*out = new(TLSCertificateSource)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSConfig.
func (in *TLSConfig) DeepCopy() *TLSConfig {
if in == nil {
return nil
}
out := new(TLSConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VaultConfig) DeepCopyInto(out *VaultConfig) {
*out = *in
if in.AzureKeyVault != nil {
in, out := &in.AzureKeyVault, &out.AzureKeyVault
*out = new(AzureKeyVaultConfig)
**out = **in
}
if in.Proxy != nil {
in, out := &in.Proxy, &out.Proxy
*out = new(ProxyConfig)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultConfig.
func (in *VaultConfig) DeepCopy() *VaultConfig {
if in == nil {
return nil
}
out := new(VaultConfig)
in.DeepCopyInto(out)
return out
}

View File

@ -7863,6 +7863,53 @@ spec:
- containers
type: object
type: object
vaultConfig:
properties:
azureKeyVault:
properties:
certificatePath:
type: string
clientId:
type: string
tenantId:
type: string
url:
type: string
required:
- certificatePath
- clientId
- tenantId
- url
type: object
proxy:
properties:
http:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
https:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
noProxy:
items:
type: string
type: array
type: object
type:
description: |-
VaultType represents the type of vault that can be used in the application.
It is used to identify which vault integration should be used to resolve secrets.
type: string
type: object
type: object
status:
description: AutoscalingListenerStatus defines the observed state of AutoscalingListener

View File

@ -15504,6 +15504,82 @@ spec:
- containers
type: object
type: object
vaultConfig:
properties:
azureKeyVault:
properties:
certificatePath:
type: string
clientId:
type: string
tenantId:
type: string
url:
type: string
required:
- certificatePath
- clientId
- tenantId
- url
type: object
proxy:
properties:
http:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
https:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
noProxy:
items:
type: string
type: array
type: object
type:
description: |-
VaultType represents the type of vault that can be used in the application.
It is used to identify which vault integration should be used to resolve secrets.
type: string
type: object
vaultServerTLS:
properties:
certificateFrom:
description: Required
properties:
configMapKeyRef:
description: Required
properties:
key:
description: The key to select.
type: string
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
optional:
description: Specify whether the ConfigMap or its key must be defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
type: object
type: object
type: object
status:
description: AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet

View File

@ -7784,6 +7784,53 @@ spec:
required:
- containers
type: object
vaultConfig:
properties:
azureKeyVault:
properties:
certificatePath:
type: string
clientId:
type: string
tenantId:
type: string
url:
type: string
required:
- certificatePath
- clientId
- tenantId
- url
type: object
proxy:
properties:
http:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
https:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
noProxy:
items:
type: string
type: array
type: object
type:
description: |-
VaultType represents the type of vault that can be used in the application.
It is used to identify which vault integration should be used to resolve secrets.
type: string
type: object
required:
- githubConfigSecret
- githubConfigUrl

View File

@ -7778,6 +7778,53 @@ spec:
required:
- containers
type: object
vaultConfig:
properties:
azureKeyVault:
properties:
certificatePath:
type: string
clientId:
type: string
tenantId:
type: string
url:
type: string
required:
- certificatePath
- clientId
- tenantId
- url
type: object
proxy:
properties:
http:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
https:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
noProxy:
items:
type: string
type: array
type: object
type:
description: |-
VaultType represents the type of vault that can be used in the application.
It is used to identify which vault integration should be used to resolve secrets.
type: string
type: object
required:
- githubConfigSecret
- githubConfigUrl

View File

@ -66,6 +66,24 @@ spec:
{{- end }}
{{- end }}
{{- if and .Values.keyVault .Values.keyVault.type }}
vaultConfig:
type: {{ .Values.keyVault.type }}
{{- if eq .Values.keyVault.type "azure_key_vault" }}
azureKeyVault:
url: {{ .Values.keyVault.azureKeyVault.url }}
tenantId: {{ .Values.keyVault.azureKeyVault.tenantId }}
clientId: {{ .Values.keyVault.azureKeyVault.clientId }}
certificatePath: {{ .Values.keyVault.azureKeyVault.certificatePath }}
secretKey: {{ .Values.keyVault.azureKeyVault.secretKey }}
{{- if .Values.keyVault.azureKeyVault.proxy }}
proxy: {{- toYaml .Values.keyVault.azureKeyVault.proxy | nindent 6 }}
{{- end }}
{{- else }}
{{- fail "Unsupported vaultConfig type: " .Values.vaultConfig.type }}
{{- end }}
{{- end }}
{{- if .Values.proxy }}
proxy:
{{- if .Values.proxy.http }}

View File

@ -1158,7 +1158,7 @@ func TestTemplateRenderedWithTLS(t *testing.T) {
ars := render(t, options)
require.NotNil(t, ars.Spec.GitHubServerTLS)
expected := &v1alpha1.GitHubServerTLSConfig{
expected := &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
@ -1218,7 +1218,7 @@ func TestTemplateRenderedWithTLS(t *testing.T) {
ars := render(t, options)
require.NotNil(t, ars.Spec.GitHubServerTLS)
expected := &v1alpha1.GitHubServerTLSConfig{
expected := &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
@ -1278,7 +1278,7 @@ func TestTemplateRenderedWithTLS(t *testing.T) {
ars := render(t, options)
require.NotNil(t, ars.Spec.GitHubServerTLS)
expected := &v1alpha1.GitHubServerTLSConfig{
expected := &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
@ -1338,7 +1338,7 @@ func TestTemplateRenderedWithTLS(t *testing.T) {
ars := render(t, options)
require.NotNil(t, ars.Spec.GitHubServerTLS)
expected := &v1alpha1.GitHubServerTLSConfig{
expected := &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
@ -1394,7 +1394,7 @@ func TestTemplateRenderedWithTLS(t *testing.T) {
ars := render(t, options)
require.NotNil(t, ars.Spec.GitHubServerTLS)
expected := &v1alpha1.GitHubServerTLSConfig{
expected := &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
@ -1450,7 +1450,7 @@ func TestTemplateRenderedWithTLS(t *testing.T) {
ars := render(t, options)
require.NotNil(t, ars.Spec.GitHubServerTLS)
expected := &v1alpha1.GitHubServerTLSConfig{
expected := &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{

View File

@ -88,6 +88,27 @@ githubConfigSecret:
# key: ca.crt
# runnerMountPath: /usr/local/share/ca-certificates/
# keyVault:
# Available values: "azure_key_vault"
# type: ""
# Configuration related to azure key vault
# azure_key_vault:
# url: ""
# client_id: ""
# tenant_id: ""
# certificate_path: ""
# proxy:
# http:
# url: http://proxy.com:1234
# credentialSecretRef: proxy-auth # a secret with `username` and `password` keys
# https:
# url: http://proxy.com:1234
# credentialSecretRef: proxy-auth # a secret with `username` and `password` keys
# noProxy:
# - example.com
# - example.org
## Container mode is an object that provides out-of-box configuration
## for dind and kubernetes mode. Template will be modified as documented under the
## template object.

View File

@ -15,6 +15,7 @@ import (
"github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/actions-runner-controller/logging"
"github.com/actions/actions-runner-controller/vault"
"github.com/actions/actions-runner-controller/vault/azurekeyvault"
"github.com/go-logr/logr"
"golang.org/x/net/http/httpproxy"
)
@ -23,6 +24,8 @@ type Config struct {
ConfigureUrl string `json:"configure_url"`
VaultType vault.VaultType `json:"vault_type"`
VaultLookupKey string `json:"vault_lookup_key"`
// If the VaultType is set to "azure_key_vault", this field must be populated.
AzureKeyVaultConfig *azurekeyvault.Config `json:"azure_key_vault,omitempty"`
// AppConfig contains the GitHub App configuration.
// It is initially set to nil if VaultType is set.
// Otherwise, it is populated with the GitHub App credentials from the GitHub secret.
@ -53,31 +56,28 @@ func Read(ctx context.Context, configPath string) (*Config, error) {
return nil, fmt.Errorf("failed to decode config: %w", err)
}
if config.VaultType == "" {
var vault vault.Vault
switch config.VaultType {
case "":
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate configuration: %v", err)
}
return &config, nil
}
case "azure_key_vault":
akv, err := azurekeyvault.New(*config.AzureKeyVaultConfig)
if err != nil {
return nil, fmt.Errorf("failed to create Azure Key Vault client: %w", err)
}
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)
vault = akv
default:
return nil, fmt.Errorf("unsupported vault type: %s", config.VaultType)
}
appConfigRaw, err := vault.GetSecret(ctx, config.VaultLookupKey)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to get app config from vault: %w", err)
}
appConfig, err := appconfig.FromString(appConfigRaw)

View File

@ -7863,6 +7863,53 @@ spec:
- containers
type: object
type: object
vaultConfig:
properties:
azureKeyVault:
properties:
certificatePath:
type: string
clientId:
type: string
tenantId:
type: string
url:
type: string
required:
- certificatePath
- clientId
- tenantId
- url
type: object
proxy:
properties:
http:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
https:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
noProxy:
items:
type: string
type: array
type: object
type:
description: |-
VaultType represents the type of vault that can be used in the application.
It is used to identify which vault integration should be used to resolve secrets.
type: string
type: object
type: object
status:
description: AutoscalingListenerStatus defines the observed state of AutoscalingListener

View File

@ -15504,6 +15504,82 @@ spec:
- containers
type: object
type: object
vaultConfig:
properties:
azureKeyVault:
properties:
certificatePath:
type: string
clientId:
type: string
tenantId:
type: string
url:
type: string
required:
- certificatePath
- clientId
- tenantId
- url
type: object
proxy:
properties:
http:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
https:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
noProxy:
items:
type: string
type: array
type: object
type:
description: |-
VaultType represents the type of vault that can be used in the application.
It is used to identify which vault integration should be used to resolve secrets.
type: string
type: object
vaultServerTLS:
properties:
certificateFrom:
description: Required
properties:
configMapKeyRef:
description: Required
properties:
key:
description: The key to select.
type: string
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
optional:
description: Specify whether the ConfigMap or its key must be defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
type: object
type: object
type: object
status:
description: AutoscalingRunnerSetStatus defines the observed state of AutoscalingRunnerSet

View File

@ -7784,6 +7784,53 @@ spec:
required:
- containers
type: object
vaultConfig:
properties:
azureKeyVault:
properties:
certificatePath:
type: string
clientId:
type: string
tenantId:
type: string
url:
type: string
required:
- certificatePath
- clientId
- tenantId
- url
type: object
proxy:
properties:
http:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
https:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
noProxy:
items:
type: string
type: array
type: object
type:
description: |-
VaultType represents the type of vault that can be used in the application.
It is used to identify which vault integration should be used to resolve secrets.
type: string
type: object
required:
- githubConfigSecret
- githubConfigUrl

View File

@ -7778,6 +7778,53 @@ spec:
required:
- containers
type: object
vaultConfig:
properties:
azureKeyVault:
properties:
certificatePath:
type: string
clientId:
type: string
tenantId:
type: string
url:
type: string
required:
- certificatePath
- clientId
- tenantId
- url
type: object
proxy:
properties:
http:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
https:
properties:
credentialSecretRef:
type: string
url:
description: Required
type: string
type: object
noProxy:
items:
type: string
type: array
type: object
type:
description: |-
VaultType represents the type of vault that can be used in the application.
It is used to identify which vault integration should be used to resolve secrets.
type: string
type: object
required:
- githubConfigSecret
- githubConfigUrl

View File

@ -1065,7 +1065,7 @@ var _ = Describe("Test GitHub Server TLS configuration", func() {
Spec: v1alpha1.AutoscalingRunnerSetSpec{
GitHubConfigUrl: "https://github.com/owner/repo",
GitHubConfigSecret: configSecret.Name,
GitHubServerTLS: &v1alpha1.GitHubServerTLSConfig{
GitHubServerTLS: &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
@ -1101,7 +1101,7 @@ var _ = Describe("Test GitHub Server TLS configuration", func() {
Spec: v1alpha1.AutoscalingListenerSpec{
GitHubConfigUrl: "https://github.com/owner/repo",
GitHubConfigSecret: configSecret.Name,
GitHubServerTLS: &v1alpha1.GitHubServerTLSConfig{
GitHubServerTLS: &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{

View File

@ -1201,7 +1201,7 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
Spec: v1alpha1.AutoscalingRunnerSetSpec{
GitHubConfigUrl: server.ConfigURLForOrg("my-org"),
GitHubConfigSecret: configSecret.Name,
GitHubServerTLS: &v1alpha1.GitHubServerTLSConfig{
GitHubServerTLS: &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
@ -1254,7 +1254,7 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
Spec: v1alpha1.AutoscalingRunnerSetSpec{
GitHubConfigUrl: "https://github.com/owner/repo",
GitHubConfigSecret: configSecret.Name,
GitHubServerTLS: &v1alpha1.GitHubServerTLSConfig{
GitHubServerTLS: &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
@ -1318,7 +1318,7 @@ var _ = Describe("Test client optional configuration", Ordered, func() {
Spec: v1alpha1.AutoscalingRunnerSetSpec{
GitHubConfigUrl: "https://github.com/owner/repo",
GitHubConfigSecret: configSecret.Name,
GitHubServerTLS: &v1alpha1.GitHubServerTLSConfig{
GitHubServerTLS: &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{

View File

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

View File

@ -1093,7 +1093,7 @@ var _ = Describe("EphemeralRunner", func() {
ephemeralRunner := newExampleRunner("test-runner", autoScalingNS.Name, configSecret.Name)
ephemeralRunner.Spec.GitHubConfigUrl = server.ConfigURLForOrg("my-org")
ephemeralRunner.Spec.GitHubServerTLS = &v1alpha1.GitHubServerTLSConfig{
ephemeralRunner.Spec.GitHubServerTLS = &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{

View File

@ -1460,7 +1460,7 @@ var _ = Describe("Test EphemeralRunnerSet controller with custom root CA", func(
EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
GitHubConfigUrl: server.ConfigURLForOrg("my-org"),
GitHubConfigSecret: configSecret.Name,
GitHubServerTLS: &v1alpha1.GitHubServerTLSConfig{
GitHubServerTLS: &v1alpha1.TLSConfig{
CertificateFrom: &v1alpha1.TLSCertificateSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{

View File

@ -18,7 +18,7 @@ import (
"github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/actions-runner-controller/hash"
"github.com/actions/actions-runner-controller/logging"
"github.com/actions/actions-runner-controller/vault"
"github.com/actions/actions-runner-controller/vault/azurekeyvault"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -110,10 +110,6 @@ func (b *ResourceBuilder) newAutoScalingListener(autoscalingRunnerSet *v1alpha1.
annotationKeyValuesHash: autoscalingRunnerSet.Annotations[annotationKeyValuesHash],
}
if v, ok := autoscalingRunnerSet.Annotations[AnnotationKeyGitHubVaultType]; ok {
annotations[AnnotationKeyGitHubVaultType] = v
}
if err := applyGitHubURLLabels(autoscalingRunnerSet.Spec.GitHubConfigUrl, labels); err != nil {
return nil, fmt.Errorf("failed to apply GitHub URL labels: %v", err)
}
@ -128,6 +124,7 @@ func (b *ResourceBuilder) newAutoScalingListener(autoscalingRunnerSet *v1alpha1.
Spec: v1alpha1.AutoscalingListenerSpec{
GitHubConfigUrl: autoscalingRunnerSet.Spec.GitHubConfigUrl,
GitHubConfigSecret: autoscalingRunnerSet.Spec.GitHubConfigSecret,
VaultConfig: autoscalingRunnerSet.VaultConfig(),
RunnerScaleSetId: runnerScaleSetId,
AutoscalingRunnerSetNamespace: autoscalingRunnerSet.Namespace,
AutoscalingRunnerSetName: autoscalingRunnerSet.Name,
@ -193,15 +190,18 @@ func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha
Metrics: autoscalingListener.Spec.Metrics,
}
if ty, ok := autoscalingListener.Annotations[AnnotationKeyGitHubVaultType]; !ok {
vault := autoscalingListener.Spec.VaultConfig
if vault == nil {
config.AppConfig = appConfig
} else {
vaultType := vault.VaultType(ty)
if err := vaultType.Validate(); err != nil {
return nil, fmt.Errorf("vault type validation error: %w", err)
}
config.VaultType = vaultType
config.VaultType = vault.Type
config.VaultLookupKey = autoscalingListener.Spec.GitHubConfigSecret
config.AzureKeyVaultConfig = &azurekeyvault.Config{
TenantID: vault.AzureKeyVault.TenantID,
ClientID: vault.AzureKeyVault.ClientID,
URL: vault.AzureKeyVault.URL,
CertificatePath: vault.AzureKeyVault.CertificatePath,
}
}
if err := config.Validate(); err != nil {
@ -520,10 +520,6 @@ func (b *ResourceBuilder) newEphemeralRunnerSet(autoscalingRunnerSet *v1alpha1.A
annotationKeyRunnerSpecHash: runnerSpecHash,
}
if v, ok := autoscalingRunnerSet.Annotations[AnnotationKeyGitHubVaultType]; ok {
newAnnotations[AnnotationKeyGitHubVaultType] = v
}
newEphemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
@ -551,6 +547,7 @@ func (b *ResourceBuilder) newEphemeralRunnerSet(autoscalingRunnerSet *v1alpha1.A
Proxy: autoscalingRunnerSet.Spec.Proxy,
GitHubServerTLS: autoscalingRunnerSet.Spec.GitHubServerTLS,
PodTemplateSpec: autoscalingRunnerSet.Spec.Template,
VaultConfig: autoscalingRunnerSet.VaultConfig(),
},
},
}

View File

@ -12,6 +12,7 @@ import (
"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"
"github.com/actions/actions-runner-controller/vault/azurekeyvault"
"golang.org/x/net/http/httpproxy"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
@ -19,28 +20,20 @@ import (
)
type SecretResolver struct {
k8sClient client.Client
vaultResolvers map[vault.VaultType]resolver
multiClient actions.MultiClient
k8sClient client.Client
multiClient actions.MultiClient
}
type SecretResolverOption func(*SecretResolver)
func WithVault(ty vault.VaultType, vault vault.Vault) SecretResolverOption {
return func(pool *SecretResolver) {
pool.vaultResolvers[ty] = &vaultResolver{vault}
}
}
func NewSecretResolver(k8sClient client.Client, multiClient actions.MultiClient, opts ...SecretResolverOption) *SecretResolver {
if k8sClient == nil {
panic("k8sClient must not be nil")
}
secretResolver := &SecretResolver{
k8sClient: k8sClient,
multiClient: multiClient,
vaultResolvers: make(map[vault.VaultType]resolver),
k8sClient: k8sClient,
multiClient: multiClient,
}
for _, opt := range opts {
@ -55,7 +48,8 @@ type ActionsGitHubObject interface {
GitHubConfigUrl() string
GitHubConfigSecret() string
Proxy() *v1alpha1.ProxyConfig
GitHubServerTLS() *v1alpha1.GitHubServerTLSConfig
GitHubServerTLS() *v1alpha1.TLSConfig
VaultConfig() *v1alpha1.VaultConfig
}
func (sr *SecretResolver) GetAppConfig(ctx context.Context, obj ActionsGitHubObject) (*appconfig.AppConfig, error) {
@ -163,25 +157,32 @@ func (sr *SecretResolver) GetActionsService(ctx context.Context, obj ActionsGitH
}
func (sr *SecretResolver) resolverForObject(obj ActionsGitHubObject) (resolver, error) {
ty, ok := obj.GetAnnotations()[AnnotationKeyGitHubVaultType]
if !ok {
vaultConfig := obj.VaultConfig()
if vaultConfig == nil || vaultConfig.Type == "" {
return &k8sResolver{
namespace: obj.GetNamespace(),
client: sr.k8sClient,
}, nil
}
vaultType := vault.VaultType(ty)
if err := vaultType.Validate(); err != nil {
return nil, fmt.Errorf("invalid vault type %q: %v", ty, err)
}
switch vaultConfig.Type {
case vault.VaultTypeAzureKeyVault:
akv, err := azurekeyvault.New(azurekeyvault.Config{
TenantID: vaultConfig.AzureKeyVault.TenantID,
ClientID: vaultConfig.AzureKeyVault.ClientID,
URL: vaultConfig.AzureKeyVault.URL,
CertificatePath: vaultConfig.AzureKeyVault.CertificatePath,
})
if err != nil {
return nil, fmt.Errorf("failed to create Azure Key Vault client: %v", err)
}
return &vaultResolver{
vault: akv,
}, nil
vault, ok := sr.vaultResolvers[vaultType]
if !ok {
return nil, fmt.Errorf("unknown vault resolver %q", ty)
default:
return nil, fmt.Errorf("unknown vault type %q", vaultConfig.Type)
}
return vault, nil
}
type resolver interface {

13
main.go
View File

@ -32,7 +32,6 @@ import (
"github.com/actions/actions-runner-controller/github"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/actions/actions-runner-controller/logging"
"github.com/actions/actions-runner-controller/vault"
"github.com/kelseyhightower/envconfig"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
@ -275,21 +274,9 @@ func main() {
log.WithName("actions-clients"),
)
vaults, err := vault.InitAll("CONTROLLER_MANAGER_")
if err != nil {
log.Error(err, "unable to read vaults")
os.Exit(1)
}
var secretResolverOptions []actionsgithubcom.SecretResolverOption
for name, vault := range vaults {
secretResolverOptions = append(secretResolverOptions, actionsgithubcom.WithVault(name, vault))
}
secretResolver := actionsgithubcom.NewSecretResolver(
mgr.GetClient(),
actionsMultiClient,
secretResolverOptions...,
)
rb := actionsgithubcom.ResourceBuilder{

View File

@ -3,10 +3,8 @@ package azurekeyvault
import (
"context"
"fmt"
"os"
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets"
"github.com/actions/actions-runner-controller/proxyconfig"
)
// AzureKeyVault is a struct that holds the Azure Key Vault client.
@ -27,31 +25,6 @@ func New(cfg Config) (*AzureKeyVault, error) {
return &AzureKeyVault{client: client}, nil
}
// FromEnv creates a new AzureKeyVault instance from environment variables.
// 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)

View File

@ -17,12 +17,11 @@ import (
// 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"`
TenantID string `json:"tenant_id"`
ClientID string `json:"client_id"`
URL string `json:"url"`
CertificatePath string `json:"certificate_path"`
Proxy *proxyconfig.ProxyConfig `json:"proxy,omitempty"`
}
func (c *Config) Validate() error {
@ -36,12 +35,12 @@ func (c *Config) Validate() error {
return fmt.Errorf("failed to parse url: %v", err)
}
if c.CertPath == "" {
if c.CertificatePath == "" {
return errors.New("cert path must be provided")
}
if _, err := os.Stat(c.CertPath); err != nil {
return fmt.Errorf("cert path %q does not exist: %v", c.CertPath, err)
if _, err := os.Stat(c.CertificatePath); err != nil {
return fmt.Errorf("cert path %q does not exist: %v", c.CertificatePath, err)
}
if err := c.Proxy.Validate(); err != nil {
@ -57,12 +56,12 @@ func (c *Config) Client() (*azsecrets.Client, error) {
}
func (c *Config) certClient() (*azsecrets.Client, error) {
data, err := os.ReadFile(c.CertPath)
data, err := os.ReadFile(c.CertificatePath)
if err != nil {
return nil, fmt.Errorf("failed to read cert file from path %q: %v", c.CertPath, err)
return nil, fmt.Errorf("failed to read cert file from path %q: %v", c.CertificatePath, err)
}
certs, key, err := azidentity.ParseCertificates(data, []byte(c.CertPassword))
certs, key, err := azidentity.ParseCertificates(data, nil)
if err != nil {
return nil, fmt.Errorf("failed to parse certificates: %w", err)
}

View File

@ -43,43 +43,38 @@ func TestConfigValidate_invalid(t *testing.T) {
tt := map[string]*Config{
"empty": {},
"no tenant id": {
TenantID: "",
ClientID: clientID,
URL: url,
CertPath: certPath,
CertPassword: "",
Proxy: proxy,
TenantID: "",
ClientID: clientID,
URL: url,
CertificatePath: certPath,
Proxy: proxy,
},
"no client id": {
TenantID: tenantID,
ClientID: "",
URL: url,
CertPath: certPath,
CertPassword: "",
Proxy: proxy,
TenantID: tenantID,
ClientID: "",
URL: url,
CertificatePath: certPath,
Proxy: proxy,
},
"no url": {
TenantID: tenantID,
ClientID: clientID,
URL: "",
CertPath: certPath,
CertPassword: "",
Proxy: proxy,
TenantID: tenantID,
ClientID: clientID,
URL: "",
CertificatePath: certPath,
Proxy: proxy,
},
"no jwt and no cert path": {
TenantID: tenantID,
ClientID: clientID,
URL: url,
CertPath: "",
CertPassword: "",
Proxy: proxy,
TenantID: tenantID,
ClientID: clientID,
URL: url,
CertificatePath: "",
Proxy: proxy,
},
"invalid proxy": {
TenantID: tenantID,
ClientID: clientID,
URL: url,
CertPath: certPath,
CertPassword: "",
TenantID: tenantID,
ClientID: clientID,
URL: url,
CertificatePath: certPath,
Proxy: &proxyconfig.ProxyConfig{
HTTP: &proxyconfig.ProxyServerConfig{},
},
@ -120,19 +115,17 @@ func TestValidate_valid(t *testing.T) {
tt := map[string]*Config{
"with cert": {
TenantID: tenantID,
ClientID: clientID,
URL: url,
CertPath: certPath,
CertPassword: "",
Proxy: proxy,
TenantID: tenantID,
ClientID: clientID,
URL: url,
CertificatePath: certPath,
Proxy: proxy,
},
"without proxy": {
TenantID: tenantID,
ClientID: clientID,
URL: url,
CertPath: certPath,
CertPassword: "",
TenantID: tenantID,
ClientID: clientID,
URL: url,
CertificatePath: certPath,
},
}

View File

@ -3,8 +3,6 @@ package vault
import (
"context"
"fmt"
"os"
"strings"
"github.com/actions/actions-runner-controller/vault/azurekeyvault"
)
@ -38,29 +36,3 @@ func (t VaultType) Validate() error {
// 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[VaultType]Vault, error) {
envs := os.Environ()
result := make(map[VaultType]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
}