335 lines
9.7 KiB
Go
335 lines
9.7 KiB
Go
package controllers
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
|
"github.com/actions-runner-controller/actions-runner-controller/github"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
)
|
|
|
|
const (
|
|
// The api creds scret annotation is added by the runner controller or the runnerset controller according to runner.spec.githubAPICredentialsFrom.secretRef.name,
|
|
// so that the runner pod controller can share the same GitHub API credentials and the instance of the GitHub API client with the upstream controllers.
|
|
annotationKeyGitHubAPICredsSecret = annotationKeyPrefix + "github-api-creds-secret"
|
|
)
|
|
|
|
type runnerOwnerRef struct {
|
|
// kind is either StatefulSet or Runner, and populated via the owner reference in the runner pod controller or via the reconcilation target's kind in
|
|
// runnerset and runner controllers.
|
|
kind string
|
|
ns, name string
|
|
}
|
|
|
|
type secretRef struct {
|
|
ns, name string
|
|
}
|
|
|
|
// savedClient is the each cache entry that contains the client for the specific set of credentials,
|
|
// like a PAT or a pair of key and cert.
|
|
// the `hash` is a part of the savedClient not the key because we are going to keep only the client for the latest creds
|
|
// in case the operator updated the k8s secret containing the credentials.
|
|
type savedClient struct {
|
|
hash string
|
|
|
|
// refs is the map of all the objects that references this client, used for reference counting to gc
|
|
// the client if unneeded.
|
|
refs map[runnerOwnerRef]struct{}
|
|
|
|
*github.Client
|
|
}
|
|
|
|
type resourceReader interface {
|
|
Get(context.Context, types.NamespacedName, client.Object) error
|
|
}
|
|
|
|
type MultiGitHubClient struct {
|
|
mu sync.Mutex
|
|
|
|
client resourceReader
|
|
|
|
githubClient *github.Client
|
|
|
|
// The saved client is freed once all its dependents disappear, or the contents of the secret changed.
|
|
// We track dependents via a golang map embedded within the savedClient struct. Each dependent is checked on their respective Kubernetes finalizer,
|
|
// so that we won't miss any dependent's termination.
|
|
// The change is the secret is determined using the hash of its contents.
|
|
clients map[secretRef]savedClient
|
|
}
|
|
|
|
func NewMultiGitHubClient(client resourceReader, githubClient *github.Client) *MultiGitHubClient {
|
|
return &MultiGitHubClient{
|
|
client: client,
|
|
githubClient: githubClient,
|
|
clients: map[secretRef]savedClient{},
|
|
}
|
|
}
|
|
|
|
// Init sets up and return the *github.Client for the object.
|
|
// In case the object (like RunnerDeployment) does not request a custom client, it returns the default client.
|
|
func (c *MultiGitHubClient) InitForRunnerPod(ctx context.Context, pod *corev1.Pod) (*github.Client, error) {
|
|
// These 3 default values are used only when the user created the pod directly, not via Runner, RunnerReplicaSet, RunnerDeploment, or RunnerSet resources.
|
|
ref := refFromRunnerPod(pod)
|
|
secretName := pod.Annotations[annotationKeyGitHubAPICredsSecret]
|
|
|
|
// kind can be any of Pod, Runner, RunnerReplicaSet, RunnerDeployment, or RunnerSet depending on which custom resource the user directly created.
|
|
return c.initClientWithSecretName(ctx, pod.Namespace, secretName, ref)
|
|
}
|
|
|
|
// Init sets up and return the *github.Client for the object.
|
|
// In case the object (like RunnerDeployment) does not request a custom client, it returns the default client.
|
|
func (c *MultiGitHubClient) InitForRunner(ctx context.Context, r *v1alpha1.Runner) (*github.Client, error) {
|
|
var secretName string
|
|
if r.Spec.GitHubAPICredentialsFrom != nil {
|
|
secretName = r.Spec.GitHubAPICredentialsFrom.SecretRef.Name
|
|
}
|
|
|
|
// These 3 default values are used only when the user created the runner resource directly, not via RunnerReplicaSet, RunnerDeploment, or RunnerSet resources.
|
|
ref := refFromRunner(r)
|
|
if ref.ns != r.Namespace {
|
|
return nil, fmt.Errorf("referencing github api creds secret from owner in another namespace is not supported yet")
|
|
}
|
|
|
|
// kind can be any of Runner, RunnerReplicaSet, or RunnerDeployment depending on which custom resource the user directly created.
|
|
return c.initClientWithSecretName(ctx, r.Namespace, secretName, ref)
|
|
}
|
|
|
|
// Init sets up and return the *github.Client for the object.
|
|
// In case the object (like RunnerDeployment) does not request a custom client, it returns the default client.
|
|
func (c *MultiGitHubClient) InitForRunnerSet(ctx context.Context, rs *v1alpha1.RunnerSet) (*github.Client, error) {
|
|
ref := refFromRunnerSet(rs)
|
|
|
|
var secretName string
|
|
if rs.Spec.GitHubAPICredentialsFrom != nil {
|
|
secretName = rs.Spec.GitHubAPICredentialsFrom.SecretRef.Name
|
|
}
|
|
|
|
return c.initClientWithSecretName(ctx, rs.Namespace, secretName, ref)
|
|
}
|
|
|
|
// Init sets up and return the *github.Client for the object.
|
|
// In case the object (like RunnerDeployment) does not request a custom client, it returns the default client.
|
|
func (c *MultiGitHubClient) InitForHRA(ctx context.Context, hra *v1alpha1.HorizontalRunnerAutoscaler) (*github.Client, error) {
|
|
ref := refFromHorizontalRunnerAutoscaler(hra)
|
|
|
|
var secretName string
|
|
if hra.Spec.GitHubAPICredentialsFrom != nil {
|
|
secretName = hra.Spec.GitHubAPICredentialsFrom.SecretRef.Name
|
|
}
|
|
|
|
return c.initClientWithSecretName(ctx, hra.Namespace, secretName, ref)
|
|
}
|
|
|
|
func (c *MultiGitHubClient) DeinitForRunnerPod(p *corev1.Pod) {
|
|
secretName := p.Annotations[annotationKeyGitHubAPICredsSecret]
|
|
c.derefClient(p.Namespace, secretName, refFromRunnerPod(p))
|
|
}
|
|
|
|
func (c *MultiGitHubClient) DeinitForRunner(r *v1alpha1.Runner) {
|
|
var secretName string
|
|
if r.Spec.GitHubAPICredentialsFrom != nil {
|
|
secretName = r.Spec.GitHubAPICredentialsFrom.SecretRef.Name
|
|
}
|
|
|
|
c.derefClient(r.Namespace, secretName, refFromRunner(r))
|
|
}
|
|
|
|
func (c *MultiGitHubClient) DeinitForRunnerSet(rs *v1alpha1.RunnerSet) {
|
|
var secretName string
|
|
if rs.Spec.GitHubAPICredentialsFrom != nil {
|
|
secretName = rs.Spec.GitHubAPICredentialsFrom.SecretRef.Name
|
|
}
|
|
|
|
c.derefClient(rs.Namespace, secretName, refFromRunnerSet(rs))
|
|
}
|
|
|
|
func (c *MultiGitHubClient) DeinitForHRA(hra *v1alpha1.HorizontalRunnerAutoscaler) {
|
|
var secretName string
|
|
if hra.Spec.GitHubAPICredentialsFrom != nil {
|
|
secretName = hra.Spec.GitHubAPICredentialsFrom.SecretRef.Name
|
|
}
|
|
|
|
c.derefClient(hra.Namespace, secretName, refFromHorizontalRunnerAutoscaler(hra))
|
|
}
|
|
|
|
func (c *MultiGitHubClient) initClientForSecret(secret *corev1.Secret, dependent *runnerOwnerRef) (*savedClient, error) {
|
|
secRef := secretRef{
|
|
ns: secret.Namespace,
|
|
name: secret.Name,
|
|
}
|
|
|
|
cliRef := c.clients[secRef]
|
|
|
|
var ks []string
|
|
|
|
for k := range secret.Data {
|
|
ks = append(ks, k)
|
|
}
|
|
|
|
sort.SliceStable(ks, func(i, j int) bool { return ks[i] < ks[j] })
|
|
|
|
hash := sha1.New()
|
|
for _, k := range ks {
|
|
hash.Write(secret.Data[k])
|
|
}
|
|
hashStr := hex.EncodeToString(hash.Sum(nil))
|
|
|
|
if cliRef.hash != hashStr {
|
|
delete(c.clients, secRef)
|
|
|
|
conf, err := secretDataToGitHubClientConfig(secret.Data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Fallback to the controller-wide setting if EnterpriseURL is not set and the original client is an enterprise client.
|
|
if conf.EnterpriseURL == "" && c.githubClient.IsEnterprise {
|
|
conf.EnterpriseURL = c.githubClient.GithubBaseURL
|
|
}
|
|
|
|
cli, err := conf.NewClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cliRef = savedClient{
|
|
hash: hashStr,
|
|
refs: map[runnerOwnerRef]struct{}{},
|
|
Client: cli,
|
|
}
|
|
|
|
c.clients[secRef] = cliRef
|
|
}
|
|
|
|
if dependent != nil {
|
|
c.clients[secRef].refs[*dependent] = struct{}{}
|
|
}
|
|
|
|
return &cliRef, nil
|
|
}
|
|
|
|
func (c *MultiGitHubClient) initClientWithSecretName(ctx context.Context, ns, secretName string, runRef *runnerOwnerRef) (*github.Client, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if secretName == "" {
|
|
return c.githubClient, nil
|
|
}
|
|
|
|
secRef := secretRef{
|
|
ns: ns,
|
|
name: secretName,
|
|
}
|
|
|
|
if _, ok := c.clients[secRef]; !ok {
|
|
c.clients[secRef] = savedClient{}
|
|
}
|
|
|
|
var sec corev1.Secret
|
|
if err := c.client.Get(ctx, types.NamespacedName{Namespace: ns, Name: secretName}, &sec); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
savedClient, err := c.initClientForSecret(&sec, runRef)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return savedClient.Client, nil
|
|
}
|
|
|
|
func (c *MultiGitHubClient) derefClient(ns, secretName string, dependent *runnerOwnerRef) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
secRef := secretRef{
|
|
ns: ns,
|
|
name: secretName,
|
|
}
|
|
|
|
if dependent != nil {
|
|
delete(c.clients[secRef].refs, *dependent)
|
|
}
|
|
|
|
cliRef := c.clients[secRef]
|
|
|
|
if dependent == nil || len(cliRef.refs) == 0 {
|
|
delete(c.clients, secRef)
|
|
}
|
|
}
|
|
|
|
func secretDataToGitHubClientConfig(data map[string][]byte) (*github.Config, error) {
|
|
var (
|
|
conf github.Config
|
|
|
|
err error
|
|
)
|
|
|
|
conf.URL = string(data["github_url"])
|
|
|
|
conf.UploadURL = string(data["github_upload_url"])
|
|
|
|
conf.EnterpriseURL = string(data["github_enterprise_url"])
|
|
|
|
conf.RunnerGitHubURL = string(data["github_runner_url"])
|
|
|
|
conf.Token = string(data["github_token"])
|
|
|
|
appID := string(data["github_app_id"])
|
|
|
|
conf.AppID, err = strconv.ParseInt(appID, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
instID := string(data["github_app_installation_id"])
|
|
|
|
conf.AppInstallationID, err = strconv.ParseInt(instID, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conf.AppPrivateKey = string(data["github_app_private_key"])
|
|
|
|
return &conf, nil
|
|
}
|
|
|
|
func refFromRunner(r *v1alpha1.Runner) *runnerOwnerRef {
|
|
return &runnerOwnerRef{
|
|
kind: r.Kind,
|
|
ns: r.Namespace,
|
|
name: r.Name,
|
|
}
|
|
}
|
|
|
|
func refFromRunnerPod(po *corev1.Pod) *runnerOwnerRef {
|
|
return &runnerOwnerRef{
|
|
kind: po.Kind,
|
|
ns: po.Namespace,
|
|
name: po.Name,
|
|
}
|
|
}
|
|
func refFromRunnerSet(rs *v1alpha1.RunnerSet) *runnerOwnerRef {
|
|
return &runnerOwnerRef{
|
|
kind: rs.Kind,
|
|
ns: rs.Namespace,
|
|
name: rs.Name,
|
|
}
|
|
}
|
|
|
|
func refFromHorizontalRunnerAutoscaler(hra *v1alpha1.HorizontalRunnerAutoscaler) *runnerOwnerRef {
|
|
return &runnerOwnerRef{
|
|
kind: hra.Kind,
|
|
ns: hra.Namespace,
|
|
name: hra.Name,
|
|
}
|
|
}
|