Support runner groups with selected visibility in webhooks autoscaler (#1012)
The current implementation doesn't support yet runner groups with custom visibility (e.g selected repositories only). If there are multiple runner groups with selected visibility - not all runner groups may be a potential target to be scaled up. Thus this PR introduces support to allow having runner groups with selected visibility. This requires to query GitHub API to find what are the potential runner groups that are linked to a specific repository (whether using visibility all or selected). This also improves resolving the `scaleTargetKey` that are used to match an HRA based on the inputs of the `RunnerSet`/`RunnerDeployment` spec to better support for runner groups. This requires to configure github auth in the webhook server, to keep backwards compatibility if github auth is not provided to the webhook server, this will assume all runner groups have no selected visibility and it will target any available runner group as before
This commit is contained in:
parent
0c34196d87
commit
4ebec38208
|
|
@ -54,6 +54,32 @@ spec:
|
||||||
key: github_webhook_secret_token
|
key: github_webhook_secret_token
|
||||||
name: {{ include "actions-runner-controller-github-webhook-server.secretName" . }}
|
name: {{ include "actions-runner-controller-github-webhook-server.secretName" . }}
|
||||||
optional: true
|
optional: true
|
||||||
|
{{- if .Values.authSecret.enabled }}
|
||||||
|
- name: GITHUB_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
key: github_token
|
||||||
|
name: {{ include "actions-runner-controller.secretName" . }}
|
||||||
|
optional: true
|
||||||
|
- name: GITHUB_APP_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
key: github_app_id
|
||||||
|
name: {{ include "actions-runner-controller.secretName" . }}
|
||||||
|
optional: true
|
||||||
|
- name: GITHUB_APP_INSTALLATION_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
key: github_app_installation_id
|
||||||
|
name: {{ include "actions-runner-controller.secretName" . }}
|
||||||
|
optional: true
|
||||||
|
- name: GITHUB_APP_PRIVATE_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
key: github_app_private_key
|
||||||
|
name: {{ include "actions-runner-controller.secretName" . }}
|
||||||
|
optional: true
|
||||||
|
{{- end }}
|
||||||
{{- range $key, $val := .Values.githubWebhookServer.env }}
|
{{- range $key, $val := .Values.githubWebhookServer.env }}
|
||||||
- name: {{ $key }}
|
- name: {{ $key }}
|
||||||
value: {{ $val | quote }}
|
value: {{ $val | quote }}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ import (
|
||||||
|
|
||||||
actionsv1alpha1 "github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
actionsv1alpha1 "github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||||
"github.com/actions-runner-controller/actions-runner-controller/controllers"
|
"github.com/actions-runner-controller/actions-runner-controller/controllers"
|
||||||
|
"github.com/actions-runner-controller/actions-runner-controller/github"
|
||||||
|
"github.com/kelseyhightower/envconfig"
|
||||||
zaplib "go.uber.org/zap"
|
zaplib "go.uber.org/zap"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
|
|
@ -76,8 +78,17 @@ func main() {
|
||||||
enableLeaderElection bool
|
enableLeaderElection bool
|
||||||
syncPeriod time.Duration
|
syncPeriod time.Duration
|
||||||
logLevel string
|
logLevel string
|
||||||
|
|
||||||
|
ghClient *github.Client
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var c github.Config
|
||||||
|
err = envconfig.Process("github", &c)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: processing environment variables: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
webhookSecretTokenEnv = os.Getenv(webhookSecretTokenEnvName)
|
webhookSecretTokenEnv = os.Getenv(webhookSecretTokenEnvName)
|
||||||
|
|
||||||
flag.StringVar(&webhookAddr, "webhook-addr", ":8000", "The address the metric endpoint binds to.")
|
flag.StringVar(&webhookAddr, "webhook-addr", ":8000", "The address the metric endpoint binds to.")
|
||||||
|
|
@ -88,6 +99,11 @@ func main() {
|
||||||
flag.DurationVar(&syncPeriod, "sync-period", 10*time.Minute, "Determines the minimum frequency at which K8s resources managed by this controller are reconciled. When you use autoscaling, set to a lower value like 10 minute, because this corresponds to the minimum time to react on demand change")
|
flag.DurationVar(&syncPeriod, "sync-period", 10*time.Minute, "Determines the minimum frequency at which K8s resources managed by this controller are reconciled. When you use autoscaling, set to a lower value like 10 minute, because this corresponds to the minimum time to react on demand change")
|
||||||
flag.StringVar(&logLevel, "log-level", logLevelDebug, `The verbosity of the logging. Valid values are "debug", "info", "warn", "error". Defaults to "debug".`)
|
flag.StringVar(&logLevel, "log-level", logLevelDebug, `The verbosity of the logging. Valid values are "debug", "info", "warn", "error". Defaults to "debug".`)
|
||||||
flag.StringVar(&webhookSecretToken, "github-webhook-secret-token", "", "The personal access token of GitHub.")
|
flag.StringVar(&webhookSecretToken, "github-webhook-secret-token", "", "The personal access token of GitHub.")
|
||||||
|
flag.StringVar(&c.Token, "github-token", c.Token, "The personal access token of GitHub.")
|
||||||
|
flag.Int64Var(&c.AppID, "github-app-id", c.AppID, "The application ID of GitHub App.")
|
||||||
|
flag.Int64Var(&c.AppInstallationID, "github-app-installation-id", c.AppInstallationID, "The installation ID of GitHub App.")
|
||||||
|
flag.StringVar(&c.AppPrivateKey, "github-app-private-key", c.AppPrivateKey, "The path of a private key file to authenticate as a GitHub App")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if webhookSecretToken == "" && webhookSecretTokenEnv != "" {
|
if webhookSecretToken == "" && webhookSecretTokenEnv != "" {
|
||||||
|
|
@ -121,6 +137,15 @@ func main() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if len(c.Token) > 0 || (c.AppID > 0 && c.AppInstallationID > 0 && c.AppPrivateKey != "") {
|
||||||
|
ghClient, err = c.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: Client creation failed.", err)
|
||||||
|
setupLog.Error(err, "unable to create controller", "controller", "Runner")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctrl.SetLogger(logger)
|
ctrl.SetLogger(logger)
|
||||||
|
|
||||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||||
|
|
@ -143,6 +168,7 @@ func main() {
|
||||||
Scheme: mgr.GetScheme(),
|
Scheme: mgr.GetScheme(),
|
||||||
SecretKeyBytes: []byte(webhookSecretToken),
|
SecretKeyBytes: []byte(webhookSecretToken),
|
||||||
Namespace: watchNamespace,
|
Namespace: watchNamespace,
|
||||||
|
GitHubClient: ghClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = hraGitHubWebhook.SetupWithManager(mgr); err != nil {
|
if err = hraGitHubWebhook.SetupWithManager(mgr); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,14 @@ import (
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||||
|
"github.com/actions-runner-controller/actions-runner-controller/github"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
scaleTargetKey = "scaleTarget"
|
scaleTargetKey = "scaleTarget"
|
||||||
|
|
||||||
keyPrefixEnterprise = "enterprises/"
|
keyPrefixEnterprise = "enterprises/"
|
||||||
|
keyRunnerGroup = "/group/"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HorizontalRunnerAutoscalerGitHubWebhook autoscales a HorizontalRunnerAutoscaler and the RunnerDeployment on each
|
// HorizontalRunnerAutoscalerGitHubWebhook autoscales a HorizontalRunnerAutoscaler and the RunnerDeployment on each
|
||||||
|
|
@ -57,6 +59,9 @@ type HorizontalRunnerAutoscalerGitHubWebhook struct {
|
||||||
// the administrator is generated and specified in GitHub Web UI.
|
// the administrator is generated and specified in GitHub Web UI.
|
||||||
SecretKeyBytes []byte
|
SecretKeyBytes []byte
|
||||||
|
|
||||||
|
// GitHub Client to discover runner groups assigned to a repository
|
||||||
|
GitHubClient *github.Client
|
||||||
|
|
||||||
// Namespace is the namespace to watch for HorizontalRunnerAutoscaler's to be
|
// Namespace is the namespace to watch for HorizontalRunnerAutoscaler's to be
|
||||||
// scaled on Webhook.
|
// scaled on Webhook.
|
||||||
// Set to empty for letting it watch for all namespaces.
|
// Set to empty for letting it watch for all namespaces.
|
||||||
|
|
@ -436,63 +441,30 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getScaleTarget(ctx co
|
||||||
}
|
}
|
||||||
|
|
||||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getScaleUpTarget(ctx context.Context, log logr.Logger, repo, owner, ownerType, enterprise string, f func(v1alpha1.ScaleUpTrigger) bool) (*ScaleTarget, error) {
|
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getScaleUpTarget(ctx context.Context, log logr.Logger, repo, owner, ownerType, enterprise string, f func(v1alpha1.ScaleUpTrigger) bool) (*ScaleTarget, error) {
|
||||||
repositoryRunnerKey := owner + "/" + repo
|
scaleTarget := func(value string) (*ScaleTarget, error) {
|
||||||
|
return autoscaler.getScaleTarget(ctx, value, f)
|
||||||
if target, err := autoscaler.getScaleTarget(ctx, repositoryRunnerKey, f); err != nil {
|
|
||||||
log.Info("finding repository-wide runner", "repository", repositoryRunnerKey)
|
|
||||||
return nil, err
|
|
||||||
} else if target != nil {
|
|
||||||
log.Info("scale up target is repository-wide runners", "repository", repo)
|
|
||||||
return target, nil
|
|
||||||
}
|
}
|
||||||
|
return autoscaler.getScaleUpTargetWithFunction(ctx, log, repo, owner, ownerType, enterprise, scaleTarget)
|
||||||
if ownerType == "User" {
|
|
||||||
log.V(1).Info("no repository runner found", "organization", owner)
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if target, err := autoscaler.getScaleTarget(ctx, owner, f); err != nil {
|
|
||||||
log.Info("finding organizational runner", "organization", owner)
|
|
||||||
return nil, err
|
|
||||||
} else if target != nil {
|
|
||||||
log.Info("scale up target is organizational runners", "organization", owner)
|
|
||||||
return target, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if enterprise == "" {
|
|
||||||
log.V(1).Info("no repository runner or organizational runner found",
|
|
||||||
"repository", repositoryRunnerKey,
|
|
||||||
"organization", owner,
|
|
||||||
)
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if target, err := autoscaler.getScaleTarget(ctx, enterpriseKey(enterprise), f); err != nil {
|
|
||||||
log.Error(err, "finding enterprise runner", "enterprise", enterprise)
|
|
||||||
return nil, err
|
|
||||||
} else if target != nil {
|
|
||||||
log.Info("scale up target is enterprise runners", "enterprise", enterprise)
|
|
||||||
return target, nil
|
|
||||||
} else {
|
|
||||||
log.V(1).Info("no repository/organizational/enterprise runner found",
|
|
||||||
"repository", repositoryRunnerKey,
|
|
||||||
"organization", owner,
|
|
||||||
"enterprises", enterprise,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getJobScaleUpTargetForRepoOrOrg(
|
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getJobScaleUpTargetForRepoOrOrg(
|
||||||
ctx context.Context, log logr.Logger, repo, owner, ownerType, enterprise string, labels []string,
|
ctx context.Context, log logr.Logger, repo, owner, ownerType, enterprise string, labels []string,
|
||||||
) (*ScaleTarget, error) {
|
) (*ScaleTarget, error) {
|
||||||
|
|
||||||
|
scaleTarget := func(value string) (*ScaleTarget, error) {
|
||||||
|
return autoscaler.getJobScaleTarget(ctx, value, labels)
|
||||||
|
}
|
||||||
|
return autoscaler.getScaleUpTargetWithFunction(ctx, log, repo, owner, ownerType, enterprise, scaleTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getScaleUpTargetWithFunction(
|
||||||
|
ctx context.Context, log logr.Logger, repo, owner, ownerType, enterprise string, scaleTarget func(value string) (*ScaleTarget, error)) (*ScaleTarget, error) {
|
||||||
|
|
||||||
repositoryRunnerKey := owner + "/" + repo
|
repositoryRunnerKey := owner + "/" + repo
|
||||||
|
|
||||||
if target, err := autoscaler.getJobScaleTarget(ctx, repositoryRunnerKey, labels); err != nil {
|
// Search for repository HRAs
|
||||||
log.Info("finding repository-wide runner", "repository", repositoryRunnerKey)
|
if target, err := scaleTarget(repositoryRunnerKey); err != nil {
|
||||||
|
log.Error(err, "finding repository-wide runner", "repository", repositoryRunnerKey)
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if target != nil {
|
} else if target != nil {
|
||||||
log.Info("job scale up target is repository-wide runners", "repository", repo)
|
log.Info("job scale up target is repository-wide runners", "repository", repo)
|
||||||
|
|
@ -500,34 +472,41 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getJobScaleUpTargetFo
|
||||||
}
|
}
|
||||||
|
|
||||||
if ownerType == "User" {
|
if ownerType == "User" {
|
||||||
log.V(1).Info("no repository runner found", "organization", owner)
|
log.V(1).Info("user repositories not supported", "owner", owner)
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if target, err := autoscaler.getJobScaleTarget(ctx, owner, labels); err != nil {
|
// Search for organization runner HRAs in default runner group
|
||||||
log.Info("finding organizational runner", "organization", owner)
|
if target, err := scaleTarget(owner); err != nil {
|
||||||
|
log.Error(err, "finding organizational runner", "organization", owner)
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if target != nil {
|
} else if target != nil {
|
||||||
log.Info("job scale up target is organizational runners", "organization", owner)
|
log.Info("job scale up target is organizational runners", "organization", owner)
|
||||||
return target, nil
|
return target, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if enterprise == "" {
|
if enterprise != "" {
|
||||||
log.V(1).Info("no repository runner or organizational runner found",
|
// Search for enterprise runner HRAs in default runner group
|
||||||
"repository", repositoryRunnerKey,
|
if target, err := scaleTarget(enterpriseKey(enterprise)); err != nil {
|
||||||
"organization", owner,
|
log.Error(err, "finding enterprise runner", "enterprise", enterprise)
|
||||||
)
|
return nil, err
|
||||||
return nil, nil
|
} else if target != nil {
|
||||||
|
log.Info("scale up target is default enterprise runners", "enterprise", enterprise)
|
||||||
|
return target, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if target, err := autoscaler.getJobScaleTarget(ctx, enterpriseKey(enterprise), labels); err != nil {
|
// At this point there were no default organization/enterprise runners available to use, try now
|
||||||
log.Error(err, "finding enterprise runner", "enterprise", enterprise)
|
// searching in runner groups
|
||||||
|
|
||||||
|
// We need to get the potential runner groups first to avoid spending API queries needless. Once/if GitHub improves an
|
||||||
|
// API to find related/linked runner groups from a specific repository this logic could be removed
|
||||||
|
availableEnterpriseGroups, availableOrganizationGroups, err := autoscaler.getPotentialGroupsFromHRAs(ctx, enterprise, owner)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, "finding potential organization runner groups from HRAs", "organization", owner)
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if target != nil {
|
}
|
||||||
log.Info("scale up target is enterprise runners", "enterprise", enterprise)
|
if len(availableEnterpriseGroups) == 0 && len(availableOrganizationGroups) == 0 {
|
||||||
return target, nil
|
|
||||||
} else {
|
|
||||||
log.V(1).Info("no repository/organizational/enterprise runner found",
|
log.V(1).Info("no repository/organizational/enterprise runner found",
|
||||||
"repository", repositoryRunnerKey,
|
"repository", repositoryRunnerKey,
|
||||||
"organization", owner,
|
"organization", owner,
|
||||||
|
|
@ -535,9 +514,103 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getJobScaleUpTargetFo
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var enterpriseGroups []string
|
||||||
|
var organizationGroups []string
|
||||||
|
if autoscaler.GitHubClient != nil {
|
||||||
|
// Get available organization runner groups and enterprise runner groups for a repository
|
||||||
|
// These are the sum of runner groups with repository access = All repositories plus
|
||||||
|
// runner groups where owner/repo has access to
|
||||||
|
enterpriseGroups, organizationGroups, err = autoscaler.GitHubClient.GetRunnerGroupsFromRepository(ctx, owner, repositoryRunnerKey, availableEnterpriseGroups, availableOrganizationGroups)
|
||||||
|
log.V(1).Info("Searching in runner groups", "enterprise.groups", enterpriseGroups, "organization.groups", organizationGroups)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, "Unable to find runner groups from repository", "organization", owner, "repository", repo)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For backwards compatibility if GitHub authentication is not configured, we assume all runner groups have
|
||||||
|
// visibility=all to honor the previous implementation, therefore any available enterprise/organization runner
|
||||||
|
// is a potential target for scaling
|
||||||
|
enterpriseGroups = availableEnterpriseGroups
|
||||||
|
organizationGroups = availableOrganizationGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range organizationGroups {
|
||||||
|
if target, err := scaleTarget(organizationalRunnerGroupKey(owner, group)); err != nil {
|
||||||
|
log.Error(err, "finding organizational runner group", "organization", owner)
|
||||||
|
return nil, err
|
||||||
|
} else if target != nil {
|
||||||
|
log.Info(fmt.Sprintf("job scale up target is organizational runner group %s", target.Name), "organization", owner)
|
||||||
|
return target, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range enterpriseGroups {
|
||||||
|
if target, err := scaleTarget(enterpriseRunnerGroupKey(enterprise, group)); err != nil {
|
||||||
|
log.Error(err, "finding enterprise runner group", "enterprise", owner)
|
||||||
|
return nil, err
|
||||||
|
} else if target != nil {
|
||||||
|
log.Info(fmt.Sprintf("job scale up target is enterprise runner group %s", target.Name), "enterprise", owner)
|
||||||
|
return target, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.V(1).Info("no repository/organizational/enterprise runner found",
|
||||||
|
"repository", repositoryRunnerKey,
|
||||||
|
"organization", owner,
|
||||||
|
"enterprises", enterprise,
|
||||||
|
)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getPotentialGroupsFromHRAs(ctx context.Context, enterprise, org string) ([]string, []string, error) {
|
||||||
|
var enterpriseRunnerGroups []string
|
||||||
|
var orgRunnerGroups []string
|
||||||
|
ns := autoscaler.Namespace
|
||||||
|
|
||||||
|
var defaultListOpts []client.ListOption
|
||||||
|
if ns != "" {
|
||||||
|
defaultListOpts = append(defaultListOpts, client.InNamespace(ns))
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := append([]client.ListOption{}, defaultListOpts...)
|
||||||
|
if autoscaler.Namespace != "" {
|
||||||
|
opts = append(opts, client.InNamespace(autoscaler.Namespace))
|
||||||
|
}
|
||||||
|
|
||||||
|
var hraList v1alpha1.HorizontalRunnerAutoscalerList
|
||||||
|
if err := autoscaler.List(ctx, &hraList, opts...); err != nil {
|
||||||
|
return orgRunnerGroups, enterpriseRunnerGroups, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, hra := range hraList.Items {
|
||||||
|
switch hra.Spec.ScaleTargetRef.Kind {
|
||||||
|
case "RunnerSet":
|
||||||
|
var rs v1alpha1.RunnerSet
|
||||||
|
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rs); err != nil {
|
||||||
|
return orgRunnerGroups, enterpriseRunnerGroups, err
|
||||||
|
}
|
||||||
|
if rs.Spec.Organization == org && rs.Spec.Group != "" {
|
||||||
|
orgRunnerGroups = append(orgRunnerGroups, rs.Spec.Group)
|
||||||
|
}
|
||||||
|
if rs.Spec.Enterprise == enterprise && rs.Spec.Group != "" {
|
||||||
|
enterpriseRunnerGroups = append(enterpriseRunnerGroups, rs.Spec.Group)
|
||||||
|
}
|
||||||
|
case "RunnerDeployment", "":
|
||||||
|
var rd v1alpha1.RunnerDeployment
|
||||||
|
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rd); err != nil {
|
||||||
|
return orgRunnerGroups, enterpriseRunnerGroups, err
|
||||||
|
}
|
||||||
|
if rd.Spec.Template.Spec.Organization == org && rd.Spec.Template.Spec.Group != "" {
|
||||||
|
orgRunnerGroups = append(orgRunnerGroups, rd.Spec.Template.Spec.Group)
|
||||||
|
}
|
||||||
|
if rd.Spec.Template.Spec.Enterprise == enterprise && rd.Spec.Template.Spec.Group != "" {
|
||||||
|
enterpriseRunnerGroups = append(enterpriseRunnerGroups, rd.Spec.Template.Spec.Group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enterpriseRunnerGroups, orgRunnerGroups, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getJobScaleTarget(ctx context.Context, name string, labels []string) (*ScaleTarget, error) {
|
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getJobScaleTarget(ctx context.Context, name string, labels []string) (*ScaleTarget, error) {
|
||||||
hras, err := autoscaler.findHRAsByKey(ctx, name)
|
hras, err := autoscaler.findHRAsByKey(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -582,6 +655,9 @@ HRA:
|
||||||
|
|
||||||
// Ensure that the RunnerSet-managed runners have all the labels requested by the workflow_job.
|
// Ensure that the RunnerSet-managed runners have all the labels requested by the workflow_job.
|
||||||
for _, l := range labels {
|
for _, l := range labels {
|
||||||
|
if l == "self-hosted" {
|
||||||
|
continue // label is automatically added to self-hosted runners
|
||||||
|
}
|
||||||
var matched bool
|
var matched bool
|
||||||
|
|
||||||
// ignore "self-hosted" label as all instance here are self-hosted
|
// ignore "self-hosted" label as all instance here are self-hosted
|
||||||
|
|
@ -613,6 +689,9 @@ HRA:
|
||||||
|
|
||||||
// Ensure that the RunnerDeployment-managed runners have all the labels requested by the workflow_job.
|
// Ensure that the RunnerDeployment-managed runners have all the labels requested by the workflow_job.
|
||||||
for _, l := range labels {
|
for _, l := range labels {
|
||||||
|
if l == "self-hosted" {
|
||||||
|
continue // label is automatically added to self-hosted runners
|
||||||
|
}
|
||||||
var matched bool
|
var matched bool
|
||||||
|
|
||||||
// ignore "self-hosted" label as all instance here are self-hosted
|
// ignore "self-hosted" label as all instance here are self-hosted
|
||||||
|
|
@ -724,31 +803,55 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) SetupWithManager(mgr
|
||||||
switch hra.Spec.ScaleTargetRef.Kind {
|
switch hra.Spec.ScaleTargetRef.Kind {
|
||||||
case "", "RunnerDeployment":
|
case "", "RunnerDeployment":
|
||||||
var rd v1alpha1.RunnerDeployment
|
var rd v1alpha1.RunnerDeployment
|
||||||
|
|
||||||
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rd); err != nil {
|
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rd); err != nil {
|
||||||
|
autoscaler.Log.V(1).Info(fmt.Sprintf("RunnerDeployment not found with scale target ref name %s for hra %s", hra.Spec.ScaleTargetRef.Name, hra.Name))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
keys := []string{rd.Spec.Template.Spec.Repository, rd.Spec.Template.Spec.Organization}
|
keys := []string{}
|
||||||
|
if rd.Spec.Template.Spec.Repository != "" {
|
||||||
if enterprise := rd.Spec.Template.Spec.Enterprise; enterprise != "" {
|
keys = append(keys, rd.Spec.Template.Spec.Repository) // Repository runners
|
||||||
keys = append(keys, enterpriseKey(enterprise))
|
|
||||||
}
|
}
|
||||||
|
if rd.Spec.Template.Spec.Organization != "" {
|
||||||
|
if group := rd.Spec.Template.Spec.Group; group != "" {
|
||||||
|
keys = append(keys, organizationalRunnerGroupKey(rd.Spec.Template.Spec.Organization, rd.Spec.Template.Spec.Group)) // Organization runner groups
|
||||||
|
} else {
|
||||||
|
keys = append(keys, rd.Spec.Template.Spec.Organization) // Organization runners
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if enterprise := rd.Spec.Template.Spec.Enterprise; enterprise != "" {
|
||||||
|
if group := rd.Spec.Template.Spec.Group; group != "" {
|
||||||
|
keys = append(keys, enterpriseRunnerGroupKey(enterprise, rd.Spec.Template.Spec.Group)) // Enterprise runner groups
|
||||||
|
} else {
|
||||||
|
keys = append(keys, enterpriseKey(enterprise)) // Enterprise runners
|
||||||
|
}
|
||||||
|
}
|
||||||
|
autoscaler.Log.V(1).Info(fmt.Sprintf("HRA keys indexed for HRA %s: %v", hra.Name, keys))
|
||||||
return keys
|
return keys
|
||||||
case "RunnerSet":
|
case "RunnerSet":
|
||||||
var rs v1alpha1.RunnerSet
|
var rs v1alpha1.RunnerSet
|
||||||
|
|
||||||
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rs); err != nil {
|
if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rs); err != nil {
|
||||||
|
autoscaler.Log.V(1).Info(fmt.Sprintf("RunnerSet not found with scale target ref name %s for hra %s", hra.Spec.ScaleTargetRef.Name, hra.Name))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
keys := []string{rs.Spec.Repository, rs.Spec.Organization}
|
keys := []string{}
|
||||||
|
if rs.Spec.Repository != "" {
|
||||||
if enterprise := rs.Spec.Enterprise; enterprise != "" {
|
keys = append(keys, rs.Spec.Repository) // Repository runners
|
||||||
keys = append(keys, enterpriseKey(enterprise))
|
|
||||||
}
|
}
|
||||||
|
if rs.Spec.Organization != "" {
|
||||||
|
keys = append(keys, rs.Spec.Organization) // Organization runners
|
||||||
|
if group := rs.Spec.Group; group != "" {
|
||||||
|
keys = append(keys, organizationalRunnerGroupKey(rs.Spec.Organization, rs.Spec.Group)) // Organization runner groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if enterprise := rs.Spec.Enterprise; enterprise != "" {
|
||||||
|
keys = append(keys, enterpriseKey(enterprise)) // Enterprise runners
|
||||||
|
if group := rs.Spec.Group; group != "" {
|
||||||
|
keys = append(keys, enterpriseRunnerGroupKey(enterprise, rs.Spec.Group)) // Enterprise runner groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
autoscaler.Log.V(1).Info(fmt.Sprintf("HRA keys indexed for HRA %s: %v", hra.Name, keys))
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -766,3 +869,11 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) SetupWithManager(mgr
|
||||||
func enterpriseKey(name string) string {
|
func enterpriseKey(name string) string {
|
||||||
return keyPrefixEnterprise + name
|
return keyPrefixEnterprise + name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func organizationalRunnerGroupKey(owner, group string) string {
|
||||||
|
return owner + keyRunnerGroup + group
|
||||||
|
}
|
||||||
|
|
||||||
|
func enterpriseRunnerGroupKey(enterprise, group string) string {
|
||||||
|
return keyPrefixEnterprise + enterprise + keyRunnerGroup + group
|
||||||
|
}
|
||||||
|
|
|
||||||
106
github/github.go
106
github/github.go
|
|
@ -103,7 +103,7 @@ func (c *Client) GetRegistrationToken(ctx context.Context, enterprise, org, repo
|
||||||
return rt, nil
|
return rt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
enterprise, owner, repo, err := getEnterpriseOrganisationAndRepo(enterprise, org, repo)
|
enterprise, owner, repo, err := getEnterpriseOrganizationAndRepo(enterprise, org, repo)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rt, err
|
return rt, err
|
||||||
|
|
@ -129,7 +129,7 @@ func (c *Client) GetRegistrationToken(ctx context.Context, enterprise, org, repo
|
||||||
|
|
||||||
// RemoveRunner removes a runner with specified runner ID from repository.
|
// RemoveRunner removes a runner with specified runner ID from repository.
|
||||||
func (c *Client) RemoveRunner(ctx context.Context, enterprise, org, repo string, runnerID int64) error {
|
func (c *Client) RemoveRunner(ctx context.Context, enterprise, org, repo string, runnerID int64) error {
|
||||||
enterprise, owner, repo, err := getEnterpriseOrganisationAndRepo(enterprise, org, repo)
|
enterprise, owner, repo, err := getEnterpriseOrganizationAndRepo(enterprise, org, repo)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -150,7 +150,7 @@ func (c *Client) RemoveRunner(ctx context.Context, enterprise, org, repo string,
|
||||||
|
|
||||||
// ListRunners returns a list of runners of specified owner/repository name.
|
// ListRunners returns a list of runners of specified owner/repository name.
|
||||||
func (c *Client) ListRunners(ctx context.Context, enterprise, org, repo string) ([]*github.Runner, error) {
|
func (c *Client) ListRunners(ctx context.Context, enterprise, org, repo string) ([]*github.Runner, error) {
|
||||||
enterprise, owner, repo, err := getEnterpriseOrganisationAndRepo(enterprise, org, repo)
|
enterprise, owner, repo, err := getEnterpriseOrganizationAndRepo(enterprise, org, repo)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -176,6 +176,93 @@ func (c *Client) ListRunners(ctx context.Context, enterprise, org, repo string)
|
||||||
return runners, nil
|
return runners, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetRunnerGroupsFromRepository(ctx context.Context, org, repo string, potentialEnterpriseGroups []string, potentialOrgGroups []string) ([]string, []string, error) {
|
||||||
|
|
||||||
|
var enterpriseRunnerGroups []string
|
||||||
|
var orgRunnerGroups []string
|
||||||
|
|
||||||
|
if org != "" {
|
||||||
|
runnerGroups, err := c.getOrganizationRunnerGroups(ctx, org, repo)
|
||||||
|
if err != nil {
|
||||||
|
return enterpriseRunnerGroups, orgRunnerGroups, err
|
||||||
|
}
|
||||||
|
for _, runnerGroup := range runnerGroups {
|
||||||
|
if runnerGroup.GetInherited() { // enterprise runner groups
|
||||||
|
if !containsString(potentialEnterpriseGroups, runnerGroup.GetName()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if runnerGroup.GetVisibility() == "all" {
|
||||||
|
enterpriseRunnerGroups = append(enterpriseRunnerGroups, runnerGroup.GetName())
|
||||||
|
} else {
|
||||||
|
hasAccess, err := c.hasRepoAccessToOrganizationRunnerGroup(ctx, org, runnerGroup.GetID(), repo)
|
||||||
|
if err != nil {
|
||||||
|
return enterpriseRunnerGroups, orgRunnerGroups, err
|
||||||
|
}
|
||||||
|
if hasAccess {
|
||||||
|
enterpriseRunnerGroups = append(enterpriseRunnerGroups, runnerGroup.GetName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // organization runner groups
|
||||||
|
if !containsString(potentialOrgGroups, runnerGroup.GetName()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if runnerGroup.GetVisibility() == "all" {
|
||||||
|
orgRunnerGroups = append(orgRunnerGroups, runnerGroup.GetName())
|
||||||
|
} else {
|
||||||
|
hasAccess, err := c.hasRepoAccessToOrganizationRunnerGroup(ctx, org, runnerGroup.GetID(), repo)
|
||||||
|
if err != nil {
|
||||||
|
return enterpriseRunnerGroups, orgRunnerGroups, err
|
||||||
|
}
|
||||||
|
if hasAccess {
|
||||||
|
orgRunnerGroups = append(orgRunnerGroups, runnerGroup.GetName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enterpriseRunnerGroups, orgRunnerGroups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) hasRepoAccessToOrganizationRunnerGroup(ctx context.Context, org string, runnerGroupId int64, repo string) (bool, error) {
|
||||||
|
opts := github.ListOptions{PerPage: 100}
|
||||||
|
for {
|
||||||
|
list, res, err := c.Client.Actions.ListRepositoryAccessRunnerGroup(ctx, org, runnerGroupId, &opts)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to list repository access for runner group: %w", err)
|
||||||
|
}
|
||||||
|
for _, githubRepo := range list.Repositories {
|
||||||
|
if githubRepo.GetFullName() == repo {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if res.NextPage == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
opts.Page = res.NextPage
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getOrganizationRunnerGroups(ctx context.Context, org, repo string) ([]*github.RunnerGroup, error) {
|
||||||
|
var runnerGroups []*github.RunnerGroup
|
||||||
|
|
||||||
|
opts := github.ListOptions{PerPage: 100}
|
||||||
|
for {
|
||||||
|
list, res, err := c.Client.Actions.ListOrganizationRunnerGroups(ctx, org, &opts)
|
||||||
|
if err != nil {
|
||||||
|
return runnerGroups, fmt.Errorf("failed to list organization runner groups: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
runnerGroups = append(runnerGroups, list.RunnerGroups...)
|
||||||
|
if res.NextPage == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
opts.Page = res.NextPage
|
||||||
|
}
|
||||||
|
|
||||||
|
return runnerGroups, nil
|
||||||
|
}
|
||||||
|
|
||||||
// cleanup removes expired registration tokens.
|
// cleanup removes expired registration tokens.
|
||||||
func (c *Client) cleanup() {
|
func (c *Client) cleanup() {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
|
|
@ -267,8 +354,8 @@ func (c *Client) listRepositoryWorkflowRuns(ctx context.Context, user string, re
|
||||||
return workflowRuns, nil
|
return workflowRuns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validates enterprise, organisation and repo arguments. Both are optional, but at least one should be specified
|
// Validates enterprise, organization and repo arguments. Both are optional, but at least one should be specified
|
||||||
func getEnterpriseOrganisationAndRepo(enterprise, org, repo string) (string, string, string, error) {
|
func getEnterpriseOrganizationAndRepo(enterprise, org, repo string) (string, string, string, error) {
|
||||||
if len(repo) > 0 {
|
if len(repo) > 0 {
|
||||||
owner, repository, err := splitOwnerAndRepo(repo)
|
owner, repository, err := splitOwnerAndRepo(repo)
|
||||||
return "", owner, repository, err
|
return "", owner, repository, err
|
||||||
|
|
@ -345,3 +432,12 @@ func (r *Client) IsRunnerBusy(ctx context.Context, enterprise, org, repo, name s
|
||||||
|
|
||||||
return false, &RunnerNotFound{runnerName: name}
|
return false, &RunnerNotFound{runnerName: name}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func containsString(list []string, value string) bool {
|
||||||
|
for _, item := range list {
|
||||||
|
if item == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue