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
 | |
| }
 |