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 }