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 SecretResolver struct { k8sClient client.Client vaultResolvers map[vault.VaultType]resolver 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") } pool := &SecretResolver{ k8sClient: k8sClient, multiClient: multiClient, vaultResolvers: make(map[vault.VaultType]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 (sr *SecretResolver) GetAppConfig(ctx context.Context, obj ActionsGitHubObject) (*appconfig.AppConfig, error) { resolver, err := sr.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) } return appConfig, nil } func (sr *SecretResolver) GetActionsService(ctx context.Context, obj ActionsGitHubObject) (actions.ActionsService, error) { resolver, err := sr.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 := 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 (p *SecretResolver) resolverForObject(obj ActionsGitHubObject) (resolver, error) { ty, ok := obj.GetAnnotations()[AnnotationKeyGitHubVaultType] if !ok { return &k8sResolver{ namespace: obj.GetNamespace(), client: p.k8sClient, }, nil } vaultType := vault.VaultType(ty) if err := vaultType.Validate(); err != nil { return nil, fmt.Errorf("invalid vault type %q: %v", ty, err) } vault, ok := p.vaultResolvers[vaultType] 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 }