mirror of
https://github.com/actions/actions-runner-controller.git
synced 2025-12-10 11:41:27 +00:00
This introduces a linter to PRs to help with code reviews and code hygiene. I've also gone ahead and fixed (or ignored) the existing lints. I've only setup the default linters right now. There are many more options that are documented at https://golangci-lint.run/. The GitHub Action should add appropriate annotations to the lint job for the PR. Contributors can also lint locally using `make lint`.
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,
|
|
}
|
|
}
|