From 8d3a83b07a6094ec7f92ece44f99e05bf9a8d7ab Mon Sep 17 00:00:00 2001 From: Yusuke Kuoka Date: Sun, 14 Mar 2021 10:21:42 +0900 Subject: [PATCH] Add CheckRun.Names scale-up trigger configuration (#390) This allows you to trigger autoscaling depending on check_run names(i.e. actions job names). If you are willing to differentiate scale amount only for a specific job, or want to scale only on a specific job, try this. --- .../horizontalrunnerautoscaler_types.go | 6 + api/v1alpha1/zz_generated.deepcopy.go | 5 + charts/actions-runner-controller/Chart.yaml | 2 +- ...rwind.dev_horizontalrunnerautoscalers.yaml | 11 + ...rwind.dev_horizontalrunnerautoscalers.yaml | 11 + ..._runner_autoscaler_webhook_on_check_run.go | 11 + pkg/actionsglob/README.md | 8 + pkg/actionsglob/actionsglob.go | 78 +++++++ pkg/actionsglob/match_test.go | 198 ++++++++++++++++++ 9 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 pkg/actionsglob/README.md create mode 100644 pkg/actionsglob/actionsglob.go create mode 100644 pkg/actionsglob/match_test.go diff --git a/api/v1alpha1/horizontalrunnerautoscaler_types.go b/api/v1alpha1/horizontalrunnerautoscaler_types.go index 659f773b..f1e1d78e 100644 --- a/api/v1alpha1/horizontalrunnerautoscaler_types.go +++ b/api/v1alpha1/horizontalrunnerautoscaler_types.go @@ -72,6 +72,12 @@ type GitHubEventScaleUpTriggerSpec struct { type CheckRunSpec struct { Types []string `json:"types,omitempty"` Status string `json:"status,omitempty"` + + // Names is a list of GitHub Actions glob patterns. + // Any check_run event whose name matches one of patterns in the list can trigger autoscaling. + // Note that check_run name seem to equal to the job name you've defined in your actions workflow yaml file. + // So it is very likely that you can utilize this to trigger depending on the job. + Names []string `json:"names,omitempty"` } // https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index b2c5ea52..05d077f0 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -66,6 +66,11 @@ func (in *CheckRunSpec) DeepCopyInto(out *CheckRunSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Names != nil { + in, out := &in.Names, &out.Names + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CheckRunSpec. diff --git a/charts/actions-runner-controller/Chart.yaml b/charts/actions-runner-controller/Chart.yaml index 3d6c079c..6e2061ab 100644 --- a/charts/actions-runner-controller/Chart.yaml +++ b/charts/actions-runner-controller/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.8.0 +version: 0.9.0 home: https://github.com/summerwind/actions-runner-controller diff --git a/charts/actions-runner-controller/crds/actions.summerwind.dev_horizontalrunnerautoscalers.yaml b/charts/actions-runner-controller/crds/actions.summerwind.dev_horizontalrunnerautoscalers.yaml index 891a79fc..7c261b3f 100644 --- a/charts/actions-runner-controller/crds/actions.summerwind.dev_horizontalrunnerautoscalers.yaml +++ b/charts/actions-runner-controller/crds/actions.summerwind.dev_horizontalrunnerautoscalers.yaml @@ -148,6 +148,17 @@ spec: checkRun: description: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#check_run properties: + names: + description: Names is a list of GitHub Actions glob patterns. + Any check_run event whose name matches one of patterns + in the list can trigger autoscaling. Note that check_run + name seem to equal to the job name you've defined in + your actions workflow yaml file. So it is very likely + that you can utilize this to trigger depending on the + job. + items: + type: string + type: array status: type: string types: diff --git a/config/crd/bases/actions.summerwind.dev_horizontalrunnerautoscalers.yaml b/config/crd/bases/actions.summerwind.dev_horizontalrunnerautoscalers.yaml index 891a79fc..7c261b3f 100644 --- a/config/crd/bases/actions.summerwind.dev_horizontalrunnerautoscalers.yaml +++ b/config/crd/bases/actions.summerwind.dev_horizontalrunnerautoscalers.yaml @@ -148,6 +148,17 @@ spec: checkRun: description: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#check_run properties: + names: + description: Names is a list of GitHub Actions glob patterns. + Any check_run event whose name matches one of patterns + in the list can trigger autoscaling. Note that check_run + name seem to equal to the job name you've defined in + your actions workflow yaml file. So it is very likely + that you can utilize this to trigger depending on the + job. + items: + type: string + type: array status: type: string types: diff --git a/controllers/horizontal_runner_autoscaler_webhook_on_check_run.go b/controllers/horizontal_runner_autoscaler_webhook_on_check_run.go index 819ad3e1..c798ff92 100644 --- a/controllers/horizontal_runner_autoscaler_webhook_on_check_run.go +++ b/controllers/horizontal_runner_autoscaler_webhook_on_check_run.go @@ -3,6 +3,7 @@ package controllers import ( "github.com/google/go-github/v33/github" "github.com/summerwind/actions-runner-controller/api/v1alpha1" + "github.com/summerwind/actions-runner-controller/pkg/actionsglob" ) func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) MatchCheckRunEvent(event *github.CheckRunEvent) func(scaleUpTrigger v1alpha1.ScaleUpTrigger) bool { @@ -27,6 +28,16 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) MatchCheckRunEvent(ev return false } + if checkRun := event.CheckRun; checkRun != nil && len(cr.Names) > 0 { + for _, pat := range cr.Names { + if r := actionsglob.Match(pat, checkRun.GetName()); r { + return true + } + } + + return false + } + return true } } diff --git a/pkg/actionsglob/README.md b/pkg/actionsglob/README.md new file mode 100644 index 00000000..d24fa837 --- /dev/null +++ b/pkg/actionsglob/README.md @@ -0,0 +1,8 @@ +This package is an implementation of glob that is intended to simulate the behaviour of +https://github.com/actions/toolkit/tree/master/packages/glob in many cases. + +This isn't a complete reimplementation of the referenced nodejs package. + +Differences: + +- This package doesn't implement `**` diff --git a/pkg/actionsglob/actionsglob.go b/pkg/actionsglob/actionsglob.go new file mode 100644 index 00000000..7966de04 --- /dev/null +++ b/pkg/actionsglob/actionsglob.go @@ -0,0 +1,78 @@ +package actionsglob + +import ( + "fmt" + "strings" +) + +func Match(pat string, s string) bool { + if len(pat) == 0 { + panic(fmt.Sprintf("unexpected length of pattern: %d", len(pat))) + } + + var inverse bool + + if pat[0] == '!' { + pat = pat[1:] + inverse = true + } + + tokens := strings.SplitAfter(pat, "*") + + var wildcardInHead bool + + for i := 0; i < len(tokens); i++ { + p := tokens[i] + + if p == "" { + s = "" + break + } + + if p == "*" { + if i == len(tokens)-1 { + s = "" + break + } + + wildcardInHead = true + + continue + } + + wildcardInTail := p[len(p)-1] == '*' + if wildcardInTail { + p = p[:len(p)-1] + } + + subs := strings.SplitN(s, p, 2) + + if len(subs) == 0 { + break + } + + if subs[0] != "" { + if !wildcardInHead { + break + } + } + + if subs[1] != "" { + if !wildcardInTail { + break + } + } + + s = subs[1] + + wildcardInHead = false + } + + r := s == "" + + if inverse { + r = !r + } + + return r +} diff --git a/pkg/actionsglob/match_test.go b/pkg/actionsglob/match_test.go new file mode 100644 index 00000000..c273cf12 --- /dev/null +++ b/pkg/actionsglob/match_test.go @@ -0,0 +1,198 @@ +package actionsglob + +import ( + "testing" +) + +func TestMatch(t *testing.T) { + type testcase struct { + Pattern, Target string + Want bool + } + + run := func(t *testing.T, tc testcase) { + t.Helper() + + got := Match(tc.Pattern, tc.Target) + + if got != tc.Want { + t.Errorf("%s against %s: want %v, got %v", tc.Pattern, tc.Target, tc.Want, got) + } + } + + t.Run("foo == foo", func(t *testing.T) { + run(t, testcase{ + Pattern: "foo", + Target: "foo", + Want: true, + }) + }) + + t.Run("!foo == foo", func(t *testing.T) { + run(t, testcase{ + Pattern: "!foo", + Target: "foo", + Want: false, + }) + }) + + t.Run("foo == foo1", func(t *testing.T) { + run(t, testcase{ + Pattern: "foo", + Target: "foo1", + Want: false, + }) + }) + + t.Run("!foo == foo1", func(t *testing.T) { + run(t, testcase{ + Pattern: "!foo", + Target: "foo1", + Want: true, + }) + }) + + t.Run("*foo == foo", func(t *testing.T) { + run(t, testcase{ + Pattern: "*foo", + Target: "foo", + Want: true, + }) + }) + + t.Run("!*foo == foo", func(t *testing.T) { + run(t, testcase{ + Pattern: "!*foo", + Target: "foo", + Want: false, + }) + }) + + t.Run("*foo == 1foo", func(t *testing.T) { + run(t, testcase{ + Pattern: "*foo", + Target: "1foo", + Want: true, + }) + }) + + t.Run("!*foo == 1foo", func(t *testing.T) { + run(t, testcase{ + Pattern: "!*foo", + Target: "1foo", + Want: false, + }) + }) + + t.Run("*foo == foo1", func(t *testing.T) { + run(t, testcase{ + Pattern: "*foo", + Target: "foo1", + Want: false, + }) + }) + + t.Run("!*foo == foo1", func(t *testing.T) { + run(t, testcase{ + Pattern: "!*foo", + Target: "foo1", + Want: true, + }) + }) + + t.Run("*foo* == foo1", func(t *testing.T) { + run(t, testcase{ + Pattern: "*foo*", + Target: "foo1", + Want: true, + }) + }) + + t.Run("!*foo* == foo1", func(t *testing.T) { + run(t, testcase{ + Pattern: "!*foo*", + Target: "foo1", + Want: false, + }) + }) + + t.Run("*foo == foobar", func(t *testing.T) { + run(t, testcase{ + Pattern: "*foo", + Target: "foobar", + Want: false, + }) + }) + + t.Run("!*foo == foobar", func(t *testing.T) { + run(t, testcase{ + Pattern: "!*foo", + Target: "foobar", + Want: true, + }) + }) + + t.Run("*foo* == foobar", func(t *testing.T) { + run(t, testcase{ + Pattern: "*foo*", + Target: "foobar", + Want: true, + }) + }) + + t.Run("!*foo* == foobar", func(t *testing.T) { + run(t, testcase{ + Pattern: "!*foo*", + Target: "foobar", + Want: false, + }) + }) + + t.Run("foo* == foo", func(t *testing.T) { + run(t, testcase{ + Pattern: "foo*", + Target: "foo", + Want: true, + }) + }) + + t.Run("!foo* == foo", func(t *testing.T) { + run(t, testcase{ + Pattern: "!foo*", + Target: "foo", + Want: false, + }) + }) + + t.Run("foo* == foobar", func(t *testing.T) { + run(t, testcase{ + Pattern: "foo*", + Target: "foobar", + Want: true, + }) + }) + + t.Run("!foo* == foobar", func(t *testing.T) { + run(t, testcase{ + Pattern: "!foo*", + Target: "foobar", + Want: false, + }) + }) + + t.Run("foo (* == foo ( 1 / 2 )", func(t *testing.T) { + run(t, testcase{ + Pattern: "foo (*", + Target: "foo ( 1 / 2 )", + Want: true, + }) + }) + + t.Run("!foo (* == foo ( 1 / 2 )", func(t *testing.T) { + run(t, testcase{ + Pattern: "!foo (*", + Target: "foo ( 1 / 2 )", + Want: false, + }) + }) +}