actions-runner-controller/github/actions/multi_client.go

188 lines
4.7 KiB
Go

package actions
import (
"context"
"crypto/x509"
"fmt"
"net/url"
"strconv"
"sync"
"github.com/go-logr/logr"
)
type MultiClient interface {
GetClientFor(ctx context.Context, githubConfigURL string, creds ActionsAuth, namespace string) (ActionsService, error)
GetClientFromSecret(ctx context.Context, githubConfigURL, namespace string, secretData KubernetesSecretData) (ActionsService, error)
}
type multiClient struct {
// To lock adding and removing of individual clients.
mu sync.Mutex
clients map[ActionsClientKey]*actionsClientWrapper
logger logr.Logger
userAgent string
}
type GitHubAppAuth struct {
AppID int64
AppInstallationID int64
AppPrivateKey string
}
type ActionsAuth struct {
// GitHub App
AppCreds *GitHubAppAuth
// GitHub PAT
Token string
}
type ActionsClientKey struct {
ActionsURL string
Auth ActionsAuth
Namespace string
}
type actionsClientWrapper struct {
// To lock client usage when tokens are being refreshed.
mu sync.Mutex
client ActionsService
}
func NewMultiClient(userAgent string, logger logr.Logger) MultiClient {
return &multiClient{
mu: sync.Mutex{},
clients: make(map[ActionsClientKey]*actionsClientWrapper),
logger: logger,
userAgent: userAgent,
}
}
func (m *multiClient) GetClientFor(ctx context.Context, githubConfigURL string, creds ActionsAuth, namespace string) (ActionsService, error) {
m.logger.Info("retrieve actions client", "githubConfigURL", githubConfigURL, "namespace", namespace)
parsedGitHubURL, err := url.Parse(githubConfigURL)
if err != nil {
return nil, err
}
if creds.Token == "" && creds.AppCreds == nil {
return nil, fmt.Errorf("no credentials provided. either a PAT or GitHub App credentials should be provided")
}
if creds.Token != "" && creds.AppCreds != nil {
return nil, fmt.Errorf("both PAT and GitHub App credentials provided. should only provide one")
}
key := ActionsClientKey{
ActionsURL: parsedGitHubURL.String(),
Namespace: namespace,
}
if creds.AppCreds != nil {
key.Auth = ActionsAuth{
AppCreds: creds.AppCreds,
}
}
if creds.Token != "" {
key.Auth = ActionsAuth{
Token: creds.Token,
}
}
m.mu.Lock()
defer m.mu.Unlock()
clientWrapper, has := m.clients[key]
if has {
m.logger.Info("using cache client", "githubConfigURL", githubConfigURL, "namespace", namespace)
return clientWrapper.client, nil
}
m.logger.Info("creating new client", "githubConfigURL", githubConfigURL, "namespace", namespace)
client, err := NewClient(
ctx,
githubConfigURL,
&creds,
WithUserAgent(m.userAgent),
WithLogger(m.logger),
)
if err != nil {
return nil, err
}
m.clients[key] = &actionsClientWrapper{
mu: sync.Mutex{},
client: client,
}
m.logger.Info("successfully created new client", "githubConfigURL", githubConfigURL, "namespace", namespace)
return client, nil
}
type KubernetesSecretData map[string][]byte
func (m *multiClient) GetClientFromSecret(ctx context.Context, githubConfigURL, namespace string, secretData KubernetesSecretData) (ActionsService, error) {
if len(secretData) == 0 {
return nil, fmt.Errorf("must provide secret data with either PAT or GitHub App Auth")
}
token := string(secretData["github_token"])
hasToken := len(token) > 0
appID := string(secretData["github_app_id"])
appInstallationID := string(secretData["github_app_installation_id"])
appPrivateKey := string(secretData["github_app_private_key"])
hasGitHubAppAuth := len(appID) > 0 && len(appInstallationID) > 0 && len(appPrivateKey) > 0
if hasToken && hasGitHubAppAuth {
return nil, fmt.Errorf("must provide secret with only PAT or GitHub App Auth to avoid ambiguity in client behavior")
}
if !hasToken && !hasGitHubAppAuth {
return nil, fmt.Errorf("neither PAT nor GitHub App Auth credentials provided in secret")
}
auth := ActionsAuth{}
if hasToken {
auth.Token = token
return m.GetClientFor(ctx, githubConfigURL, auth, namespace)
}
parsedAppID, err := strconv.ParseInt(appID, 10, 64)
if err != nil {
return nil, err
}
parsedAppInstallationID, err := strconv.ParseInt(appInstallationID, 10, 64)
if err != nil {
return nil, err
}
auth.AppCreds = &GitHubAppAuth{AppID: parsedAppID, AppInstallationID: parsedAppInstallationID, AppPrivateKey: appPrivateKey}
return m.GetClientFor(ctx, githubConfigURL, auth, namespace)
}
func RootCAsFromConfigMap(configMapData map[string][]byte) (*x509.CertPool, error) {
caCertPool, err := x509.SystemCertPool()
if err != nil {
caCertPool = x509.NewCertPool()
}
for key, certData := range configMapData {
ok := caCertPool.AppendCertsFromPEM(certData)
if !ok {
return nil, fmt.Errorf("no certificates successfully parsed from key %s", key)
}
}
return caCertPool, nil
}