281 lines
7.4 KiB
Go
281 lines
7.4 KiB
Go
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"
|
|
"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"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
)
|
|
|
|
type SecretResolver struct {
|
|
k8sClient client.Client
|
|
multiClient actions.MultiClient
|
|
}
|
|
|
|
type SecretResolverOption func(*SecretResolver)
|
|
|
|
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,
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(secretResolver)
|
|
}
|
|
|
|
return secretResolver
|
|
}
|
|
|
|
type ActionsGitHubObject interface {
|
|
client.Object
|
|
GitHubConfigUrl() string
|
|
GitHubConfigSecret() string
|
|
GitHubProxy() *v1alpha1.ProxyConfig
|
|
GitHubServerTLS() *v1alpha1.TLSConfig
|
|
VaultConfig() *v1alpha1.VaultConfig
|
|
VaultProxy() *v1alpha1.ProxyConfig
|
|
}
|
|
|
|
func (sr *SecretResolver) GetAppConfig(ctx context.Context, obj ActionsGitHubObject) (*appconfig.AppConfig, error) {
|
|
resolver, err := sr.resolverForObject(ctx, obj)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get resolver for object: %v", err)
|
|
}
|
|
|
|
appConfig, err := resolver.appConfig(ctx, obj.GitHubConfigSecret())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to resolve app config: %v", err)
|
|
}
|
|
|
|
return appConfig, nil
|
|
}
|
|
|
|
func (sr *SecretResolver) GetActionsService(ctx context.Context, obj ActionsGitHubObject) (actions.ActionsService, error) {
|
|
resolver, err := sr.resolverForObject(ctx, 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.GitHubProxy(); 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 := sr.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 sr.multiClient.GetClientFor(
|
|
ctx,
|
|
obj.GitHubConfigUrl(),
|
|
appConfig,
|
|
obj.GetNamespace(),
|
|
clientOptions...,
|
|
)
|
|
}
|
|
|
|
func (sr *SecretResolver) resolverForObject(ctx context.Context, obj ActionsGitHubObject) (resolver, error) {
|
|
vaultConfig := obj.VaultConfig()
|
|
if vaultConfig == nil || vaultConfig.Type == "" {
|
|
return &k8sResolver{
|
|
namespace: obj.GetNamespace(),
|
|
client: sr.k8sClient,
|
|
}, nil
|
|
}
|
|
|
|
var proxy *httpproxy.Config
|
|
if vaultProxy := obj.VaultProxy(); vaultProxy != nil {
|
|
p, err := vaultProxy.ToHTTPProxyConfig(func(s string) (*corev1.Secret, error) {
|
|
var secret corev1.Secret
|
|
err := sr.k8sClient.Get(ctx, types.NamespacedName{Name: s, Namespace: obj.GetNamespace()}, &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 create proxy config: %v", err)
|
|
}
|
|
proxy = p
|
|
}
|
|
|
|
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,
|
|
Proxy: proxy,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Azure Key Vault client: %v", err)
|
|
}
|
|
return &vaultResolver{
|
|
vault: akv,
|
|
}, nil
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unknown vault type %q", vaultConfig.Type)
|
|
}
|
|
}
|
|
|
|
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.FromJSONString(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
|
|
}
|