Skip to content

Commit e46c929

Browse files
authored
Azure Key Vault integration to resolve secrets (#4090)
1 parent d4af75d commit e46c929

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2013
-599
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package appconfig
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"strconv"
8+
9+
corev1 "k8s.io/api/core/v1"
10+
)
11+
12+
type AppConfig struct {
13+
AppID string `json:"github_app_id"`
14+
AppInstallationID int64 `json:"github_app_installation_id"`
15+
AppPrivateKey string `json:"github_app_private_key"`
16+
17+
Token string `json:"github_token"`
18+
}
19+
20+
func (c *AppConfig) tidy() *AppConfig {
21+
if len(c.Token) > 0 {
22+
return &AppConfig{
23+
Token: c.Token,
24+
}
25+
}
26+
27+
return &AppConfig{
28+
AppID: c.AppID,
29+
AppInstallationID: c.AppInstallationID,
30+
AppPrivateKey: c.AppPrivateKey,
31+
}
32+
}
33+
34+
func (c *AppConfig) Validate() error {
35+
if c == nil {
36+
return fmt.Errorf("missing app config")
37+
}
38+
hasToken := len(c.Token) > 0
39+
hasGitHubAppAuth := c.hasGitHubAppAuth()
40+
if hasToken && hasGitHubAppAuth {
41+
return fmt.Errorf("both PAT and GitHub App credentials provided. should only provide one")
42+
}
43+
if !hasToken && !hasGitHubAppAuth {
44+
return fmt.Errorf("no credentials provided: either a PAT or GitHub App credentials should be provided")
45+
}
46+
47+
return nil
48+
}
49+
50+
func (c *AppConfig) hasGitHubAppAuth() bool {
51+
return len(c.AppID) > 0 && c.AppInstallationID > 0 && len(c.AppPrivateKey) > 0
52+
}
53+
54+
func FromSecret(secret *corev1.Secret) (*AppConfig, error) {
55+
var appInstallationID int64
56+
if v := string(secret.Data["github_app_installation_id"]); v != "" {
57+
val, err := strconv.ParseInt(v, 10, 64)
58+
if err != nil {
59+
return nil, err
60+
}
61+
appInstallationID = val
62+
}
63+
64+
cfg := &AppConfig{
65+
Token: string(secret.Data["github_token"]),
66+
AppID: string(secret.Data["github_app_id"]),
67+
AppInstallationID: appInstallationID,
68+
AppPrivateKey: string(secret.Data["github_app_private_key"]),
69+
}
70+
71+
if err := cfg.Validate(); err != nil {
72+
return nil, fmt.Errorf("failed to validate config: %v", err)
73+
}
74+
75+
return cfg.tidy(), nil
76+
}
77+
78+
func FromJSONString(v string) (*AppConfig, error) {
79+
var appConfig AppConfig
80+
if err := json.NewDecoder(bytes.NewBufferString(v)).Decode(&appConfig); err != nil {
81+
return nil, err
82+
}
83+
84+
if err := appConfig.Validate(); err != nil {
85+
return nil, fmt.Errorf("failed to validate app config decoded from string: %w", err)
86+
}
87+
88+
return appConfig.tidy(), nil
89+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package appconfig
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
corev1 "k8s.io/api/core/v1"
10+
)
11+
12+
func TestAppConfigValidate_invalid(t *testing.T) {
13+
tt := map[string]*AppConfig{
14+
"empty": {},
15+
"token and app config": {
16+
AppID: "1",
17+
AppInstallationID: 2,
18+
AppPrivateKey: "private key",
19+
Token: "token",
20+
},
21+
"app id not set": {
22+
AppInstallationID: 2,
23+
AppPrivateKey: "private key",
24+
},
25+
"app installation id not set": {
26+
AppID: "2",
27+
AppPrivateKey: "private key",
28+
},
29+
"private key empty": {
30+
AppID: "2",
31+
AppInstallationID: 1,
32+
AppPrivateKey: "",
33+
},
34+
}
35+
36+
for name, cfg := range tt {
37+
t.Run(name, func(t *testing.T) {
38+
err := cfg.Validate()
39+
require.Error(t, err)
40+
})
41+
}
42+
}
43+
44+
func TestAppConfigValidate_valid(t *testing.T) {
45+
tt := map[string]*AppConfig{
46+
"token": {
47+
Token: "token",
48+
},
49+
"app ID": {
50+
AppID: "1",
51+
AppInstallationID: 2,
52+
AppPrivateKey: "private key",
53+
},
54+
}
55+
56+
for name, cfg := range tt {
57+
t.Run(name, func(t *testing.T) {
58+
err := cfg.Validate()
59+
require.NoError(t, err)
60+
})
61+
}
62+
}
63+
64+
func TestAppConfigFromSecret_invalid(t *testing.T) {
65+
tt := map[string]map[string]string{
66+
"empty": {},
67+
"token and app provided": {
68+
"github_token": "token",
69+
"github_app_id": "2",
70+
"githu_app_installation_id": "3",
71+
"github_app_private_key": "private key",
72+
},
73+
"invalid app id": {
74+
"github_app_id": "abc",
75+
"githu_app_installation_id": "3",
76+
"github_app_private_key": "private key",
77+
},
78+
"invalid app installation_id": {
79+
"github_app_id": "1",
80+
"githu_app_installation_id": "abc",
81+
"github_app_private_key": "private key",
82+
},
83+
"empty private key": {
84+
"github_app_id": "1",
85+
"githu_app_installation_id": "2",
86+
"github_app_private_key": "",
87+
},
88+
}
89+
90+
for name, data := range tt {
91+
t.Run(name, func(t *testing.T) {
92+
secret := &corev1.Secret{
93+
StringData: data,
94+
}
95+
96+
appConfig, err := FromSecret(secret)
97+
assert.Error(t, err)
98+
assert.Nil(t, appConfig)
99+
})
100+
}
101+
}
102+
103+
func TestAppConfigFromSecret_valid(t *testing.T) {
104+
tt := map[string]map[string]string{
105+
"with token": {
106+
"github_token": "token",
107+
},
108+
"app config": {
109+
"github_app_id": "2",
110+
"githu_app_installation_id": "3",
111+
"github_app_private_key": "private key",
112+
},
113+
}
114+
115+
for name, data := range tt {
116+
t.Run(name, func(t *testing.T) {
117+
secret := &corev1.Secret{
118+
StringData: data,
119+
}
120+
121+
appConfig, err := FromSecret(secret)
122+
assert.Error(t, err)
123+
assert.Nil(t, appConfig)
124+
})
125+
}
126+
}
127+
128+
func TestAppConfigFromString_valid(t *testing.T) {
129+
tt := map[string]*AppConfig{
130+
"token": {
131+
Token: "token",
132+
},
133+
"app ID": {
134+
AppID: "1",
135+
AppInstallationID: 2,
136+
AppPrivateKey: "private key",
137+
},
138+
}
139+
140+
for name, cfg := range tt {
141+
t.Run(name, func(t *testing.T) {
142+
bytes, err := json.Marshal(cfg)
143+
require.NoError(t, err)
144+
145+
got, err := FromJSONString(string(bytes))
146+
require.NoError(t, err)
147+
148+
want := cfg.tidy()
149+
assert.Equal(t, want, got)
150+
})
151+
}
152+
}

apis/actions.github.com/v1alpha1/autoscalinglistener_types.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ type AutoscalingListenerSpec struct {
5959
Proxy *ProxyConfig `json:"proxy,omitempty"`
6060

6161
// +optional
62-
GitHubServerTLS *GitHubServerTLSConfig `json:"githubServerTLS,omitempty"`
62+
GitHubServerTLS *TLSConfig `json:"githubServerTLS,omitempty"`
63+
64+
// +optional
65+
VaultConfig *VaultConfig `json:"vaultConfig,omitempty"`
6366

6467
// +optional
6568
Metrics *MetricsConfig `json:"metrics,omitempty"`
@@ -87,7 +90,6 @@ type AutoscalingListener struct {
8790
}
8891

8992
// +kubebuilder:object:root=true
90-
9193
// AutoscalingListenerList contains a list of AutoscalingListener
9294
type AutoscalingListenerList struct {
9395
metav1.TypeMeta `json:",inline"`

apis/actions.github.com/v1alpha1/autoscalingrunnerset_types.go

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"strings"
2525

2626
"github.com/actions/actions-runner-controller/hash"
27+
"github.com/actions/actions-runner-controller/vault"
2728
"golang.org/x/net/http/httpproxy"
2829
corev1 "k8s.io/api/core/v1"
2930
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -69,7 +70,10 @@ type AutoscalingRunnerSetSpec struct {
6970
Proxy *ProxyConfig `json:"proxy,omitempty"`
7071

7172
// +optional
72-
GitHubServerTLS *GitHubServerTLSConfig `json:"githubServerTLS,omitempty"`
73+
GitHubServerTLS *TLSConfig `json:"githubServerTLS,omitempty"`
74+
75+
// +optional
76+
VaultConfig *VaultConfig `json:"vaultConfig,omitempty"`
7377

7478
// Required
7579
Template corev1.PodTemplateSpec `json:"template,omitempty"`
@@ -89,12 +93,12 @@ type AutoscalingRunnerSetSpec struct {
8993
MinRunners *int `json:"minRunners,omitempty"`
9094
}
9195

92-
type GitHubServerTLSConfig struct {
96+
type TLSConfig struct {
9397
// Required
9498
CertificateFrom *TLSCertificateSource `json:"certificateFrom,omitempty"`
9599
}
96100

97-
func (c *GitHubServerTLSConfig) ToCertPool(keyFetcher func(name, key string) ([]byte, error)) (*x509.CertPool, error) {
101+
func (c *TLSConfig) ToCertPool(keyFetcher func(name, key string) ([]byte, error)) (*x509.CertPool, error) {
98102
if c.CertificateFrom == nil {
99103
return nil, fmt.Errorf("certificateFrom not specified")
100104
}
@@ -142,7 +146,7 @@ type ProxyConfig struct {
142146
NoProxy []string `json:"noProxy,omitempty"`
143147
}
144148

145-
func (c *ProxyConfig) toHTTPProxyConfig(secretFetcher func(string) (*corev1.Secret, error)) (*httpproxy.Config, error) {
149+
func (c *ProxyConfig) ToHTTPProxyConfig(secretFetcher func(string) (*corev1.Secret, error)) (*httpproxy.Config, error) {
146150
config := &httpproxy.Config{
147151
NoProxy: strings.Join(c.NoProxy, ","),
148152
}
@@ -201,7 +205,7 @@ func (c *ProxyConfig) toHTTPProxyConfig(secretFetcher func(string) (*corev1.Secr
201205
}
202206

203207
func (c *ProxyConfig) ToSecretData(secretFetcher func(string) (*corev1.Secret, error)) (map[string][]byte, error) {
204-
config, err := c.toHTTPProxyConfig(secretFetcher)
208+
config, err := c.ToHTTPProxyConfig(secretFetcher)
205209
if err != nil {
206210
return nil, err
207211
}
@@ -215,7 +219,7 @@ func (c *ProxyConfig) ToSecretData(secretFetcher func(string) (*corev1.Secret, e
215219
}
216220

217221
func (c *ProxyConfig) ProxyFunc(secretFetcher func(string) (*corev1.Secret, error)) (func(*http.Request) (*url.URL, error), error) {
218-
config, err := c.toHTTPProxyConfig(secretFetcher)
222+
config, err := c.ToHTTPProxyConfig(secretFetcher)
219223
if err != nil {
220224
return nil, err
221225
}
@@ -235,6 +239,26 @@ type ProxyServerConfig struct {
235239
CredentialSecretRef string `json:"credentialSecretRef,omitempty"`
236240
}
237241

242+
type VaultConfig struct {
243+
// +optional
244+
Type vault.VaultType `json:"type,omitempty"`
245+
// +optional
246+
AzureKeyVault *AzureKeyVaultConfig `json:"azureKeyVault,omitempty"`
247+
// +optional
248+
Proxy *ProxyConfig `json:"proxy,omitempty"`
249+
}
250+
251+
type AzureKeyVaultConfig struct {
252+
// +required
253+
URL string `json:"url,omitempty"`
254+
// +required
255+
TenantID string `json:"tenantId,omitempty"`
256+
// +required
257+
ClientID string `json:"clientId,omitempty"`
258+
// +required
259+
CertificatePath string `json:"certificatePath,omitempty"`
260+
}
261+
238262
// MetricsConfig holds configuration parameters for each metric type
239263
type MetricsConfig struct {
240264
// +optional
@@ -285,14 +309,41 @@ func (ars *AutoscalingRunnerSet) ListenerSpecHash() string {
285309
return hash.ComputeTemplateHash(&spec)
286310
}
287311

312+
func (ars *AutoscalingRunnerSet) GitHubConfigSecret() string {
313+
return ars.Spec.GitHubConfigSecret
314+
}
315+
316+
func (ars *AutoscalingRunnerSet) GitHubConfigUrl() string {
317+
return ars.Spec.GitHubConfigUrl
318+
}
319+
320+
func (ars *AutoscalingRunnerSet) GitHubProxy() *ProxyConfig {
321+
return ars.Spec.Proxy
322+
}
323+
324+
func (ars *AutoscalingRunnerSet) GitHubServerTLS() *TLSConfig {
325+
return ars.Spec.GitHubServerTLS
326+
}
327+
328+
func (ars *AutoscalingRunnerSet) VaultConfig() *VaultConfig {
329+
return ars.Spec.VaultConfig
330+
}
331+
332+
func (ars *AutoscalingRunnerSet) VaultProxy() *ProxyConfig {
333+
if ars.Spec.VaultConfig != nil {
334+
return ars.Spec.VaultConfig.Proxy
335+
}
336+
return nil
337+
}
338+
288339
func (ars *AutoscalingRunnerSet) RunnerSetSpecHash() string {
289340
type runnerSetSpec struct {
290341
GitHubConfigUrl string
291342
GitHubConfigSecret string
292343
RunnerGroup string
293344
RunnerScaleSetName string
294345
Proxy *ProxyConfig
295-
GitHubServerTLS *GitHubServerTLSConfig
346+
GitHubServerTLS *TLSConfig
296347
Template corev1.PodTemplateSpec
297348
}
298349
spec := &runnerSetSpec{

0 commit comments

Comments
 (0)