799 lines
26 KiB
Go
799 lines
26 KiB
Go
package actionsgithubcom
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"math"
|
|
"net"
|
|
"strconv"
|
|
"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/build"
|
|
ghalistenerconfig "github.com/actions/actions-runner-controller/cmd/ghalistener/config"
|
|
"github.com/actions/actions-runner-controller/github/actions"
|
|
"github.com/actions/actions-runner-controller/hash"
|
|
"github.com/actions/actions-runner-controller/logging"
|
|
"github.com/actions/actions-runner-controller/vault/azurekeyvault"
|
|
corev1 "k8s.io/api/core/v1"
|
|
rbacv1 "k8s.io/api/rbac/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
)
|
|
|
|
// secret constants
|
|
const (
|
|
jitTokenKey = "jitToken"
|
|
)
|
|
|
|
var commonLabelKeys = [...]string{
|
|
LabelKeyKubernetesPartOf,
|
|
LabelKeyKubernetesComponent,
|
|
LabelKeyKubernetesVersion,
|
|
LabelKeyGitHubScaleSetName,
|
|
LabelKeyGitHubScaleSetNamespace,
|
|
LabelKeyGitHubEnterprise,
|
|
LabelKeyGitHubOrganization,
|
|
LabelKeyGitHubRepository,
|
|
}
|
|
|
|
const labelValueKubernetesPartOf = "gha-runner-scale-set"
|
|
|
|
var (
|
|
scaleSetListenerLogLevel = DefaultScaleSetListenerLogLevel
|
|
scaleSetListenerLogFormat = DefaultScaleSetListenerLogFormat
|
|
scaleSetListenerEntrypoint = "/ghalistener"
|
|
)
|
|
|
|
func SetListenerLoggingParameters(level string, format string) bool {
|
|
switch level {
|
|
case logging.LogLevelDebug, logging.LogLevelInfo, logging.LogLevelWarn, logging.LogLevelError:
|
|
default:
|
|
return false
|
|
}
|
|
|
|
switch format {
|
|
case logging.LogFormatJSON, logging.LogFormatText:
|
|
default:
|
|
return false
|
|
}
|
|
|
|
scaleSetListenerLogLevel = level
|
|
scaleSetListenerLogFormat = format
|
|
return true
|
|
}
|
|
|
|
func SetListenerEntrypoint(entrypoint string) {
|
|
if entrypoint != "" {
|
|
scaleSetListenerEntrypoint = entrypoint
|
|
}
|
|
}
|
|
|
|
type ResourceBuilder struct {
|
|
ExcludeLabelPropagationPrefixes []string
|
|
*SecretResolver
|
|
}
|
|
|
|
// boolPtr returns a pointer to a bool value
|
|
func boolPtr(v bool) *bool {
|
|
return &v
|
|
}
|
|
|
|
func (b *ResourceBuilder) newAutoScalingListener(autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet, namespace, image string, imagePullSecrets []corev1.LocalObjectReference) (*v1alpha1.AutoscalingListener, error) {
|
|
runnerScaleSetId, err := strconv.Atoi(autoscalingRunnerSet.Annotations[runnerScaleSetIdAnnotationKey])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
effectiveMinRunners := 0
|
|
effectiveMaxRunners := math.MaxInt32
|
|
if autoscalingRunnerSet.Spec.MaxRunners != nil {
|
|
effectiveMaxRunners = *autoscalingRunnerSet.Spec.MaxRunners
|
|
}
|
|
if autoscalingRunnerSet.Spec.MinRunners != nil {
|
|
effectiveMinRunners = *autoscalingRunnerSet.Spec.MinRunners
|
|
}
|
|
|
|
labels := b.mergeLabels(autoscalingRunnerSet.Labels, map[string]string{
|
|
LabelKeyGitHubScaleSetNamespace: autoscalingRunnerSet.Namespace,
|
|
LabelKeyGitHubScaleSetName: autoscalingRunnerSet.Name,
|
|
LabelKeyKubernetesPartOf: labelValueKubernetesPartOf,
|
|
LabelKeyKubernetesComponent: "runner-scale-set-listener",
|
|
LabelKeyKubernetesVersion: autoscalingRunnerSet.Labels[LabelKeyKubernetesVersion],
|
|
})
|
|
|
|
annotations := map[string]string{
|
|
annotationKeyRunnerSpecHash: autoscalingRunnerSet.ListenerSpecHash(),
|
|
annotationKeyValuesHash: autoscalingRunnerSet.Annotations[annotationKeyValuesHash],
|
|
}
|
|
|
|
if err := applyGitHubURLLabels(autoscalingRunnerSet.Spec.GitHubConfigUrl, labels); err != nil {
|
|
return nil, fmt.Errorf("failed to apply GitHub URL labels: %v", err)
|
|
}
|
|
|
|
autoscalingListener := &v1alpha1.AutoscalingListener{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: scaleSetListenerName(autoscalingRunnerSet),
|
|
Namespace: namespace,
|
|
Labels: labels,
|
|
Annotations: annotations,
|
|
},
|
|
Spec: v1alpha1.AutoscalingListenerSpec{
|
|
GitHubConfigUrl: autoscalingRunnerSet.Spec.GitHubConfigUrl,
|
|
GitHubConfigSecret: autoscalingRunnerSet.Spec.GitHubConfigSecret,
|
|
VaultConfig: autoscalingRunnerSet.VaultConfig(),
|
|
RunnerScaleSetId: runnerScaleSetId,
|
|
AutoscalingRunnerSetNamespace: autoscalingRunnerSet.Namespace,
|
|
AutoscalingRunnerSetName: autoscalingRunnerSet.Name,
|
|
EphemeralRunnerSetName: ephemeralRunnerSet.Name,
|
|
MinRunners: effectiveMinRunners,
|
|
MaxRunners: effectiveMaxRunners,
|
|
Image: image,
|
|
ImagePullSecrets: imagePullSecrets,
|
|
Proxy: autoscalingRunnerSet.Spec.Proxy,
|
|
GitHubServerTLS: autoscalingRunnerSet.Spec.GitHubServerTLS,
|
|
Metrics: autoscalingRunnerSet.Spec.ListenerMetrics,
|
|
Template: autoscalingRunnerSet.Spec.ListenerTemplate,
|
|
},
|
|
}
|
|
|
|
if namespace == autoscalingRunnerSet.Namespace {
|
|
autoscalingListener.ObjectMeta.OwnerReferences = []metav1.OwnerReference{
|
|
{
|
|
APIVersion: autoscalingRunnerSet.GetObjectKind().GroupVersionKind().GroupVersion().String(),
|
|
Kind: autoscalingRunnerSet.GetObjectKind().GroupVersionKind().Kind,
|
|
UID: autoscalingRunnerSet.GetUID(),
|
|
Name: autoscalingRunnerSet.GetName(),
|
|
Controller: boolPtr(true),
|
|
BlockOwnerDeletion: boolPtr(true),
|
|
},
|
|
}
|
|
}
|
|
|
|
return autoscalingListener, nil
|
|
}
|
|
|
|
type listenerMetricsServerConfig struct {
|
|
addr string
|
|
endpoint string
|
|
}
|
|
|
|
func (lm *listenerMetricsServerConfig) containerPort() (corev1.ContainerPort, error) {
|
|
_, portStr, err := net.SplitHostPort(lm.addr)
|
|
if err != nil {
|
|
return corev1.ContainerPort{}, err
|
|
}
|
|
port, err := strconv.ParseInt(portStr, 10, 32)
|
|
if err != nil {
|
|
return corev1.ContainerPort{}, err
|
|
}
|
|
return corev1.ContainerPort{
|
|
ContainerPort: int32(port),
|
|
Protocol: corev1.ProtocolTCP,
|
|
Name: "metrics",
|
|
}, nil
|
|
}
|
|
|
|
func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha1.AutoscalingListener, appConfig *appconfig.AppConfig, metricsConfig *listenerMetricsServerConfig, cert string) (*corev1.Secret, error) {
|
|
var (
|
|
metricsAddr = ""
|
|
metricsEndpoint = ""
|
|
)
|
|
if metricsConfig != nil {
|
|
metricsAddr = metricsConfig.addr
|
|
metricsEndpoint = metricsConfig.endpoint
|
|
}
|
|
|
|
config := ghalistenerconfig.Config{
|
|
ConfigureUrl: autoscalingListener.Spec.GitHubConfigUrl,
|
|
EphemeralRunnerSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
|
EphemeralRunnerSetName: autoscalingListener.Spec.EphemeralRunnerSetName,
|
|
MaxRunners: autoscalingListener.Spec.MaxRunners,
|
|
MinRunners: autoscalingListener.Spec.MinRunners,
|
|
RunnerScaleSetId: autoscalingListener.Spec.RunnerScaleSetId,
|
|
RunnerScaleSetName: autoscalingListener.Spec.AutoscalingRunnerSetName,
|
|
ServerRootCA: cert,
|
|
LogLevel: scaleSetListenerLogLevel,
|
|
LogFormat: scaleSetListenerLogFormat,
|
|
MetricsAddr: metricsAddr,
|
|
MetricsEndpoint: metricsEndpoint,
|
|
Metrics: autoscalingListener.Spec.Metrics,
|
|
}
|
|
|
|
vault := autoscalingListener.Spec.VaultConfig
|
|
if vault == nil {
|
|
config.AppConfig = appConfig
|
|
} else {
|
|
config.VaultType = vault.Type
|
|
config.VaultLookupKey = autoscalingListener.Spec.GitHubConfigSecret
|
|
config.AzureKeyVaultConfig = &azurekeyvault.Config{
|
|
TenantID: vault.AzureKeyVault.TenantID,
|
|
ClientID: vault.AzureKeyVault.ClientID,
|
|
URL: vault.AzureKeyVault.URL,
|
|
CertificatePath: vault.AzureKeyVault.CertificatePath,
|
|
}
|
|
}
|
|
|
|
if err := config.Validate(); err != nil {
|
|
return nil, fmt.Errorf("invalid listener config: %w", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := json.NewEncoder(&buf).Encode(config); err != nil {
|
|
return nil, fmt.Errorf("failed to encode config: %w", err)
|
|
}
|
|
|
|
return &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: scaleSetListenerConfigName(autoscalingListener),
|
|
Namespace: autoscalingListener.Namespace,
|
|
},
|
|
Data: map[string][]byte{
|
|
"config.json": buf.Bytes(),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (b *ResourceBuilder) newScaleSetListenerPod(autoscalingListener *v1alpha1.AutoscalingListener, podConfig *corev1.Secret, serviceAccount *corev1.ServiceAccount, metricsConfig *listenerMetricsServerConfig, envs ...corev1.EnvVar) (*corev1.Pod, error) {
|
|
listenerEnv := []corev1.EnvVar{
|
|
{
|
|
Name: "LISTENER_CONFIG_PATH",
|
|
Value: "/etc/gha-listener/config.json",
|
|
},
|
|
}
|
|
listenerEnv = append(listenerEnv, envs...)
|
|
|
|
var ports []corev1.ContainerPort
|
|
if metricsConfig != nil && len(metricsConfig.addr) != 0 {
|
|
port, err := metricsConfig.containerPort()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert metrics server address to container port: %v", err)
|
|
}
|
|
ports = append(ports, port)
|
|
}
|
|
|
|
terminationGracePeriodSeconds := int64(60)
|
|
podSpec := corev1.PodSpec{
|
|
ServiceAccountName: serviceAccount.Name,
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: autoscalingListenerContainerName,
|
|
Image: autoscalingListener.Spec.Image,
|
|
Env: listenerEnv,
|
|
Command: []string{
|
|
scaleSetListenerEntrypoint,
|
|
},
|
|
Ports: ports,
|
|
VolumeMounts: []corev1.VolumeMount{
|
|
{
|
|
Name: "listener-config",
|
|
MountPath: "/etc/gha-listener",
|
|
ReadOnly: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Volumes: []corev1.Volume{
|
|
{
|
|
Name: "listener-config",
|
|
VolumeSource: corev1.VolumeSource{
|
|
Secret: &corev1.SecretVolumeSource{
|
|
SecretName: podConfig.Name,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ImagePullSecrets: autoscalingListener.Spec.ImagePullSecrets,
|
|
RestartPolicy: corev1.RestartPolicyNever,
|
|
TerminationGracePeriodSeconds: &terminationGracePeriodSeconds,
|
|
}
|
|
|
|
labels := make(map[string]string, len(autoscalingListener.Labels))
|
|
maps.Copy(labels, autoscalingListener.Labels)
|
|
|
|
newRunnerScaleSetListenerPod := &corev1.Pod{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "Pod",
|
|
APIVersion: "v1",
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: autoscalingListener.Name,
|
|
Namespace: autoscalingListener.Namespace,
|
|
Labels: labels,
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: autoscalingListener.GetObjectKind().GroupVersionKind().GroupVersion().String(),
|
|
Kind: autoscalingListener.GetObjectKind().GroupVersionKind().Kind,
|
|
UID: autoscalingListener.GetUID(),
|
|
Name: autoscalingListener.GetName(),
|
|
Controller: boolPtr(true),
|
|
BlockOwnerDeletion: boolPtr(true),
|
|
},
|
|
},
|
|
},
|
|
Spec: podSpec,
|
|
}
|
|
|
|
if autoscalingListener.Spec.Template != nil {
|
|
mergeListenerPodWithTemplate(newRunnerScaleSetListenerPod, autoscalingListener.Spec.Template)
|
|
}
|
|
|
|
return newRunnerScaleSetListenerPod, nil
|
|
}
|
|
|
|
func mergeListenerPodWithTemplate(pod *corev1.Pod, tmpl *corev1.PodTemplateSpec) {
|
|
if pod.Annotations == nil {
|
|
pod.Annotations = make(map[string]string)
|
|
}
|
|
for k, v := range tmpl.Annotations {
|
|
if _, ok := pod.Annotations[k]; !ok {
|
|
pod.Annotations[k] = v
|
|
}
|
|
}
|
|
|
|
for k, v := range tmpl.Labels {
|
|
if _, ok := pod.Labels[k]; !ok {
|
|
pod.Labels[k] = v
|
|
}
|
|
}
|
|
|
|
// apply spec
|
|
|
|
// apply container
|
|
listenerContainer := &pod.Spec.Containers[0] // if this panics, we have bigger problems
|
|
for i := range tmpl.Spec.Containers {
|
|
c := &tmpl.Spec.Containers[i]
|
|
|
|
switch c.Name {
|
|
case autoscalingListenerContainerName:
|
|
mergeListenerContainer(listenerContainer, c)
|
|
default:
|
|
pod.Spec.Containers = append(pod.Spec.Containers, *c)
|
|
}
|
|
}
|
|
|
|
// apply pod related spec
|
|
// NOTE: fields that should be ignored
|
|
// - service account based fields
|
|
|
|
if tmpl.Spec.RestartPolicy != "" {
|
|
pod.Spec.RestartPolicy = tmpl.Spec.RestartPolicy
|
|
}
|
|
|
|
if tmpl.Spec.ImagePullSecrets != nil {
|
|
pod.Spec.ImagePullSecrets = tmpl.Spec.ImagePullSecrets
|
|
}
|
|
|
|
pod.Spec.Volumes = append(pod.Spec.Volumes, tmpl.Spec.Volumes...)
|
|
pod.Spec.InitContainers = tmpl.Spec.InitContainers
|
|
pod.Spec.EphemeralContainers = tmpl.Spec.EphemeralContainers
|
|
pod.Spec.TerminationGracePeriodSeconds = tmpl.Spec.TerminationGracePeriodSeconds
|
|
pod.Spec.ActiveDeadlineSeconds = tmpl.Spec.ActiveDeadlineSeconds
|
|
pod.Spec.DNSPolicy = tmpl.Spec.DNSPolicy
|
|
pod.Spec.NodeSelector = tmpl.Spec.NodeSelector
|
|
pod.Spec.NodeName = tmpl.Spec.NodeName
|
|
pod.Spec.HostNetwork = tmpl.Spec.HostNetwork
|
|
pod.Spec.HostPID = tmpl.Spec.HostPID
|
|
pod.Spec.HostIPC = tmpl.Spec.HostIPC
|
|
pod.Spec.ShareProcessNamespace = tmpl.Spec.ShareProcessNamespace
|
|
pod.Spec.SecurityContext = tmpl.Spec.SecurityContext
|
|
pod.Spec.Hostname = tmpl.Spec.Hostname
|
|
pod.Spec.Subdomain = tmpl.Spec.Subdomain
|
|
pod.Spec.Affinity = tmpl.Spec.Affinity
|
|
pod.Spec.SchedulerName = tmpl.Spec.SchedulerName
|
|
pod.Spec.Tolerations = tmpl.Spec.Tolerations
|
|
pod.Spec.HostAliases = tmpl.Spec.HostAliases
|
|
pod.Spec.PriorityClassName = tmpl.Spec.PriorityClassName
|
|
pod.Spec.Priority = tmpl.Spec.Priority
|
|
pod.Spec.DNSConfig = tmpl.Spec.DNSConfig
|
|
pod.Spec.ReadinessGates = tmpl.Spec.ReadinessGates
|
|
pod.Spec.RuntimeClassName = tmpl.Spec.RuntimeClassName
|
|
pod.Spec.EnableServiceLinks = tmpl.Spec.EnableServiceLinks
|
|
pod.Spec.PreemptionPolicy = tmpl.Spec.PreemptionPolicy
|
|
pod.Spec.Overhead = tmpl.Spec.Overhead
|
|
pod.Spec.TopologySpreadConstraints = tmpl.Spec.TopologySpreadConstraints
|
|
pod.Spec.SetHostnameAsFQDN = tmpl.Spec.SetHostnameAsFQDN
|
|
pod.Spec.OS = tmpl.Spec.OS
|
|
pod.Spec.HostUsers = tmpl.Spec.HostUsers
|
|
pod.Spec.SchedulingGates = tmpl.Spec.SchedulingGates
|
|
pod.Spec.ResourceClaims = tmpl.Spec.ResourceClaims
|
|
}
|
|
|
|
func mergeListenerContainer(base, from *corev1.Container) {
|
|
// name should not be modified
|
|
|
|
if from.Image != "" {
|
|
base.Image = from.Image
|
|
}
|
|
|
|
if len(from.Command) > 0 {
|
|
base.Command = from.Command
|
|
}
|
|
|
|
base.Env = append(base.Env, from.Env...)
|
|
|
|
base.ImagePullPolicy = from.ImagePullPolicy
|
|
base.Args = append(base.Args, from.Args...)
|
|
base.WorkingDir = from.WorkingDir
|
|
base.Ports = append(base.Ports, from.Ports...)
|
|
base.EnvFrom = append(base.EnvFrom, from.EnvFrom...)
|
|
base.Resources = from.Resources
|
|
base.VolumeMounts = append(base.VolumeMounts, from.VolumeMounts...)
|
|
base.VolumeDevices = append(base.VolumeDevices, from.VolumeDevices...)
|
|
base.LivenessProbe = from.LivenessProbe
|
|
base.ReadinessProbe = from.ReadinessProbe
|
|
base.StartupProbe = from.StartupProbe
|
|
base.Lifecycle = from.Lifecycle
|
|
base.TerminationMessagePath = from.TerminationMessagePath
|
|
base.TerminationMessagePolicy = from.TerminationMessagePolicy
|
|
base.ImagePullPolicy = from.ImagePullPolicy
|
|
base.SecurityContext = from.SecurityContext
|
|
base.ResizePolicy = from.ResizePolicy
|
|
base.RestartPolicy = from.RestartPolicy
|
|
base.Stdin = from.Stdin
|
|
base.StdinOnce = from.StdinOnce
|
|
base.TTY = from.TTY
|
|
}
|
|
|
|
func (b *ResourceBuilder) newScaleSetListenerServiceAccount(autoscalingListener *v1alpha1.AutoscalingListener) *corev1.ServiceAccount {
|
|
return &corev1.ServiceAccount{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: autoscalingListener.Name,
|
|
Namespace: autoscalingListener.Namespace,
|
|
Labels: b.mergeLabels(autoscalingListener.Labels, map[string]string{
|
|
LabelKeyGitHubScaleSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
|
LabelKeyGitHubScaleSetName: autoscalingListener.Spec.AutoscalingRunnerSetName,
|
|
}),
|
|
},
|
|
}
|
|
}
|
|
|
|
func (b *ResourceBuilder) newScaleSetListenerRole(autoscalingListener *v1alpha1.AutoscalingListener) *rbacv1.Role {
|
|
rules := rulesForListenerRole([]string{autoscalingListener.Spec.EphemeralRunnerSetName})
|
|
rulesHash := hash.ComputeTemplateHash(&rules)
|
|
newRole := &rbacv1.Role{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: autoscalingListener.Name,
|
|
Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
|
Labels: b.mergeLabels(autoscalingListener.Labels, map[string]string{
|
|
LabelKeyGitHubScaleSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
|
LabelKeyGitHubScaleSetName: autoscalingListener.Spec.AutoscalingRunnerSetName,
|
|
labelKeyListenerNamespace: autoscalingListener.Namespace,
|
|
labelKeyListenerName: autoscalingListener.Name,
|
|
"role-policy-rules-hash": rulesHash,
|
|
}),
|
|
},
|
|
Rules: rules,
|
|
}
|
|
|
|
return newRole
|
|
}
|
|
|
|
func (b *ResourceBuilder) newScaleSetListenerRoleBinding(autoscalingListener *v1alpha1.AutoscalingListener, listenerRole *rbacv1.Role, serviceAccount *corev1.ServiceAccount) *rbacv1.RoleBinding {
|
|
roleRef := rbacv1.RoleRef{
|
|
Kind: "Role",
|
|
Name: listenerRole.Name,
|
|
}
|
|
roleRefHash := hash.ComputeTemplateHash(&roleRef)
|
|
|
|
subjects := []rbacv1.Subject{
|
|
{
|
|
Kind: "ServiceAccount",
|
|
Namespace: serviceAccount.Namespace,
|
|
Name: serviceAccount.Name,
|
|
},
|
|
}
|
|
subjectHash := hash.ComputeTemplateHash(&subjects)
|
|
|
|
newRoleBinding := &rbacv1.RoleBinding{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: autoscalingListener.Name,
|
|
Namespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
|
Labels: b.mergeLabels(autoscalingListener.Labels, map[string]string{
|
|
LabelKeyGitHubScaleSetNamespace: autoscalingListener.Spec.AutoscalingRunnerSetNamespace,
|
|
LabelKeyGitHubScaleSetName: autoscalingListener.Spec.AutoscalingRunnerSetName,
|
|
labelKeyListenerNamespace: autoscalingListener.Namespace,
|
|
labelKeyListenerName: autoscalingListener.Name,
|
|
"role-binding-role-ref-hash": roleRefHash,
|
|
"role-binding-subject-hash": subjectHash,
|
|
}),
|
|
},
|
|
RoleRef: roleRef,
|
|
Subjects: subjects,
|
|
}
|
|
|
|
return newRoleBinding
|
|
}
|
|
|
|
func (b *ResourceBuilder) newEphemeralRunnerSet(autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) (*v1alpha1.EphemeralRunnerSet, error) {
|
|
runnerScaleSetId, err := strconv.Atoi(autoscalingRunnerSet.Annotations[runnerScaleSetIdAnnotationKey])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
runnerSpecHash := autoscalingRunnerSet.RunnerSetSpecHash()
|
|
|
|
labels := b.mergeLabels(autoscalingRunnerSet.Labels, map[string]string{
|
|
LabelKeyKubernetesPartOf: labelValueKubernetesPartOf,
|
|
LabelKeyKubernetesComponent: "runner-set",
|
|
LabelKeyKubernetesVersion: autoscalingRunnerSet.Labels[LabelKeyKubernetesVersion],
|
|
LabelKeyGitHubScaleSetName: autoscalingRunnerSet.Name,
|
|
LabelKeyGitHubScaleSetNamespace: autoscalingRunnerSet.Namespace,
|
|
})
|
|
|
|
if err := applyGitHubURLLabels(autoscalingRunnerSet.Spec.GitHubConfigUrl, labels); err != nil {
|
|
return nil, fmt.Errorf("failed to apply GitHub URL labels: %v", err)
|
|
}
|
|
|
|
newAnnotations := map[string]string{
|
|
AnnotationKeyGitHubRunnerGroupName: autoscalingRunnerSet.Annotations[AnnotationKeyGitHubRunnerGroupName],
|
|
AnnotationKeyGitHubRunnerScaleSetName: autoscalingRunnerSet.Annotations[AnnotationKeyGitHubRunnerScaleSetName],
|
|
annotationKeyRunnerSpecHash: runnerSpecHash,
|
|
}
|
|
|
|
newEphemeralRunnerSet := &v1alpha1.EphemeralRunnerSet{
|
|
TypeMeta: metav1.TypeMeta{},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: autoscalingRunnerSet.Name + "-",
|
|
Namespace: autoscalingRunnerSet.Namespace,
|
|
Labels: labels,
|
|
Annotations: newAnnotations,
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: autoscalingRunnerSet.GetObjectKind().GroupVersionKind().GroupVersion().String(),
|
|
Kind: autoscalingRunnerSet.GetObjectKind().GroupVersionKind().Kind,
|
|
UID: autoscalingRunnerSet.GetUID(),
|
|
Name: autoscalingRunnerSet.GetName(),
|
|
Controller: boolPtr(true),
|
|
BlockOwnerDeletion: boolPtr(true),
|
|
},
|
|
},
|
|
},
|
|
Spec: v1alpha1.EphemeralRunnerSetSpec{
|
|
Replicas: 0,
|
|
EphemeralRunnerSpec: v1alpha1.EphemeralRunnerSpec{
|
|
RunnerScaleSetId: runnerScaleSetId,
|
|
GitHubConfigUrl: autoscalingRunnerSet.Spec.GitHubConfigUrl,
|
|
GitHubConfigSecret: autoscalingRunnerSet.Spec.GitHubConfigSecret,
|
|
Proxy: autoscalingRunnerSet.Spec.Proxy,
|
|
GitHubServerTLS: autoscalingRunnerSet.Spec.GitHubServerTLS,
|
|
PodTemplateSpec: autoscalingRunnerSet.Spec.Template,
|
|
VaultConfig: autoscalingRunnerSet.VaultConfig(),
|
|
},
|
|
},
|
|
}
|
|
|
|
return newEphemeralRunnerSet, nil
|
|
}
|
|
|
|
func (b *ResourceBuilder) newEphemeralRunner(ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet) *v1alpha1.EphemeralRunner {
|
|
labels := make(map[string]string)
|
|
for k, v := range ephemeralRunnerSet.Labels {
|
|
if k == LabelKeyKubernetesComponent {
|
|
labels[k] = "runner"
|
|
} else {
|
|
labels[k] = v
|
|
}
|
|
}
|
|
|
|
annotations := make(map[string]string)
|
|
for key, val := range ephemeralRunnerSet.Annotations {
|
|
annotations[key] = val
|
|
}
|
|
|
|
annotations[AnnotationKeyPatchID] = strconv.Itoa(ephemeralRunnerSet.Spec.PatchID)
|
|
return &v1alpha1.EphemeralRunner{
|
|
TypeMeta: metav1.TypeMeta{},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: ephemeralRunnerSet.Name + "-runner-",
|
|
Namespace: ephemeralRunnerSet.Namespace,
|
|
Labels: labels,
|
|
Annotations: annotations,
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: ephemeralRunnerSet.GetObjectKind().GroupVersionKind().GroupVersion().String(),
|
|
Kind: ephemeralRunnerSet.GetObjectKind().GroupVersionKind().Kind,
|
|
UID: ephemeralRunnerSet.GetUID(),
|
|
Name: ephemeralRunnerSet.GetName(),
|
|
Controller: boolPtr(true),
|
|
BlockOwnerDeletion: boolPtr(true),
|
|
},
|
|
},
|
|
},
|
|
Spec: ephemeralRunnerSet.Spec.EphemeralRunnerSpec,
|
|
}
|
|
}
|
|
|
|
func (b *ResourceBuilder) newEphemeralRunnerPod(ctx context.Context, runner *v1alpha1.EphemeralRunner, secret *corev1.Secret, envs ...corev1.EnvVar) *corev1.Pod {
|
|
var newPod corev1.Pod
|
|
|
|
labels := map[string]string{}
|
|
annotations := map[string]string{}
|
|
|
|
for k, v := range runner.Labels {
|
|
labels[k] = v
|
|
}
|
|
for k, v := range runner.Spec.Labels {
|
|
labels[k] = v
|
|
}
|
|
labels["actions-ephemeral-runner"] = string(corev1.ConditionTrue)
|
|
|
|
for k, v := range runner.Annotations {
|
|
annotations[k] = v
|
|
}
|
|
for k, v := range runner.Spec.Annotations {
|
|
annotations[k] = v
|
|
}
|
|
|
|
labels[LabelKeyPodTemplateHash] = hash.FNVHashStringObjects(
|
|
FilterLabels(labels, LabelKeyRunnerTemplateHash),
|
|
annotations,
|
|
runner.Spec,
|
|
secret.Data,
|
|
)
|
|
|
|
objectMeta := metav1.ObjectMeta{
|
|
Name: runner.Name,
|
|
Namespace: runner.Namespace,
|
|
Labels: labels,
|
|
Annotations: annotations,
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: runner.GetObjectKind().GroupVersionKind().GroupVersion().String(),
|
|
Kind: runner.GetObjectKind().GroupVersionKind().Kind,
|
|
UID: runner.GetUID(),
|
|
Name: runner.GetName(),
|
|
Controller: boolPtr(true),
|
|
BlockOwnerDeletion: boolPtr(true),
|
|
},
|
|
},
|
|
}
|
|
|
|
newPod.ObjectMeta = objectMeta
|
|
newPod.Spec = runner.Spec.Spec
|
|
newPod.Spec.Containers = make([]corev1.Container, 0, len(runner.Spec.Spec.Containers))
|
|
|
|
for _, c := range runner.Spec.Spec.Containers {
|
|
if c.Name == v1alpha1.EphemeralRunnerContainerName {
|
|
c.Env = append(
|
|
c.Env,
|
|
corev1.EnvVar{
|
|
Name: EnvVarRunnerJITConfig,
|
|
ValueFrom: &corev1.EnvVarSource{
|
|
SecretKeyRef: &corev1.SecretKeySelector{
|
|
LocalObjectReference: corev1.LocalObjectReference{
|
|
Name: secret.Name,
|
|
},
|
|
Key: jitTokenKey,
|
|
},
|
|
},
|
|
},
|
|
corev1.EnvVar{
|
|
Name: EnvVarRunnerExtraUserAgent,
|
|
Value: fmt.Sprintf("actions-runner-controller/%s", build.Version),
|
|
},
|
|
)
|
|
c.Env = append(c.Env, envs...)
|
|
}
|
|
|
|
newPod.Spec.Containers = append(newPod.Spec.Containers, c)
|
|
}
|
|
|
|
return &newPod
|
|
}
|
|
|
|
func (b *ResourceBuilder) newEphemeralRunnerJitSecret(ephemeralRunner *v1alpha1.EphemeralRunner, jitConfig *actions.RunnerScaleSetJitRunnerConfig) *corev1.Secret {
|
|
return &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: ephemeralRunner.Name,
|
|
Namespace: ephemeralRunner.Namespace,
|
|
},
|
|
Data: map[string][]byte{
|
|
jitTokenKey: []byte(jitConfig.EncodedJITConfig),
|
|
"runnerName": []byte(jitConfig.Runner.Name),
|
|
"runnerId": []byte(strconv.Itoa(jitConfig.Runner.Id)),
|
|
"scaleSetId": []byte(strconv.Itoa(jitConfig.Runner.RunnerScaleSetId)),
|
|
},
|
|
}
|
|
}
|
|
|
|
func scaleSetListenerConfigName(autoscalingListener *v1alpha1.AutoscalingListener) string {
|
|
return fmt.Sprintf("%s-config", autoscalingListener.Name)
|
|
}
|
|
|
|
func scaleSetListenerName(autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) string {
|
|
namespaceHash := hash.FNVHashString(autoscalingRunnerSet.Namespace)
|
|
if len(namespaceHash) > 8 {
|
|
namespaceHash = namespaceHash[:8]
|
|
}
|
|
return fmt.Sprintf("%v-%v-listener", autoscalingRunnerSet.Name, namespaceHash)
|
|
}
|
|
|
|
func proxyListenerSecretName(autoscalingListener *v1alpha1.AutoscalingListener) string {
|
|
namespaceHash := hash.FNVHashString(autoscalingListener.Spec.AutoscalingRunnerSetNamespace)
|
|
if len(namespaceHash) > 8 {
|
|
namespaceHash = namespaceHash[:8]
|
|
}
|
|
return fmt.Sprintf("%v-%v-listener-proxy", autoscalingListener.Spec.AutoscalingRunnerSetName, namespaceHash)
|
|
}
|
|
|
|
func proxyEphemeralRunnerSetSecretName(ephemeralRunnerSet *v1alpha1.EphemeralRunnerSet) string {
|
|
namespaceHash := hash.FNVHashString(ephemeralRunnerSet.Namespace)
|
|
if len(namespaceHash) > 8 {
|
|
namespaceHash = namespaceHash[:8]
|
|
}
|
|
return fmt.Sprintf("%v-%v-runner-proxy", ephemeralRunnerSet.Name, namespaceHash)
|
|
}
|
|
|
|
func rulesForListenerRole(resourceNames []string) []rbacv1.PolicyRule {
|
|
return []rbacv1.PolicyRule{
|
|
{
|
|
APIGroups: []string{"actions.github.com"},
|
|
Resources: []string{"ephemeralrunnersets"},
|
|
ResourceNames: resourceNames,
|
|
Verbs: []string{"patch"},
|
|
},
|
|
{
|
|
APIGroups: []string{"actions.github.com"},
|
|
Resources: []string{"ephemeralrunners", "ephemeralrunners/status"},
|
|
Verbs: []string{"patch"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func applyGitHubURLLabels(url string, labels map[string]string) error {
|
|
githubConfig, err := actions.ParseGitHubConfigFromURL(url)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse github config from url: %v", err)
|
|
}
|
|
|
|
if len(githubConfig.Enterprise) > 0 {
|
|
labels[LabelKeyGitHubEnterprise] = trimLabelValue(githubConfig.Enterprise)
|
|
}
|
|
if len(githubConfig.Organization) > 0 {
|
|
labels[LabelKeyGitHubOrganization] = trimLabelValue(githubConfig.Organization)
|
|
}
|
|
if len(githubConfig.Repository) > 0 {
|
|
labels[LabelKeyGitHubRepository] = trimLabelValue(githubConfig.Repository)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const trimLabelVauleSuffix = "-trim"
|
|
|
|
func trimLabelValue(val string) string {
|
|
if len(val) > 63 {
|
|
return val[:63-len(trimLabelVauleSuffix)] + trimLabelVauleSuffix
|
|
}
|
|
return strings.Trim(val, "-_.")
|
|
}
|
|
|
|
func (b *ResourceBuilder) mergeLabels(base, overwrite map[string]string) map[string]string {
|
|
mergedLabels := make(map[string]string, len(base))
|
|
|
|
base:
|
|
for k, v := range base {
|
|
for _, prefix := range b.ExcludeLabelPropagationPrefixes {
|
|
if strings.HasPrefix(k, prefix) {
|
|
continue base
|
|
}
|
|
}
|
|
mergedLabels[k] = v
|
|
}
|
|
|
|
overwrite:
|
|
for k, v := range overwrite {
|
|
for _, prefix := range b.ExcludeLabelPropagationPrefixes {
|
|
if strings.HasPrefix(k, prefix) {
|
|
continue overwrite
|
|
}
|
|
}
|
|
mergedLabels[k] = v
|
|
}
|
|
|
|
return mergedLabels
|
|
}
|