Skip to content

Commit 39fd60d

Browse files
author
jaime merino
committed
new forgejo scaler with tests \n Signed-off-by: jaime Merino <[email protected]>
1 parent cd595a1 commit 39fd60d

File tree

4 files changed

+554
-2
lines changed

4 files changed

+554
-2
lines changed

pkg/scalers/forgejo/action.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
Copyright 2025 The KEDA Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package forgejo
18+
19+
type CountResponse struct {
20+
Count int64 `json:"count"`
21+
}
22+
23+
type Job struct {
24+
ID int64 `json:"id"`
25+
// the repository id
26+
RepoID int64 `json:"repo_id"`
27+
// the owner id
28+
OwnerID int64 `json:"owner_id"`
29+
// the action run job name
30+
Name string `json:"name"`
31+
// the action run job needed ids
32+
Needs []string `json:"needs"`
33+
// the action run job labels to run on
34+
RunsOn []string `json:"runs_on"`
35+
// the action run job latest task id
36+
TaskID int64 `json:"task_id"`
37+
// the action run job status
38+
Status string `json:"status"`
39+
}
40+
41+
type JobsListResponse struct {
42+
Jobs []Job `json:"body"`
43+
}

pkg/scalers/forgejo_runner_scaler.go

+241
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
package scalers
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/url"
10+
11+
"github.com/go-logr/logr"
12+
v2 "k8s.io/api/autoscaling/v2"
13+
"k8s.io/apimachinery/pkg/api/resource"
14+
"k8s.io/metrics/pkg/apis/external_metrics"
15+
16+
"github.com/kedacore/keda/v2/pkg/scalers/forgejo"
17+
"github.com/kedacore/keda/v2/pkg/scalers/scalersconfig"
18+
kedautil "github.com/kedacore/keda/v2/pkg/util"
19+
)
20+
21+
const (
22+
defaultForgejoJobsLen = 1
23+
24+
adminJobMetricPath = "/api/v1/admin/runners/jobs"
25+
orgJobMetricPath = "/api/v1/orgs/%s/actions/runners/jobs" // /api/v1/orgs/{org}/actions/runners/jobs
26+
repoJobMetricPath = "/api/v1/repos/%s/%s/actions/runners/jobs" // {address}/api/v1/repos/{owner}/{repo}/actions/runners/jobs
27+
UserJobMetricPath = "/api/v1/user/actions/runners/jobs"
28+
)
29+
30+
type forgejoRunnerMetadata struct {
31+
Token string
32+
Address string
33+
MetricPath string
34+
Labels string // comma separated runner labels
35+
Global bool
36+
Owner string
37+
Org string
38+
Repo string
39+
}
40+
41+
// ForgejoRunnerConfig represents the overall configuration.
42+
type ForgejoRunnerConfig struct {
43+
RunnerMeta forgejoRunnerMetadata `yaml:"runner"` // Runner represents the configuration for the runner.
44+
}
45+
46+
func parseForgejoRunnerMetadata(config *scalersconfig.ScalerConfig) (*ForgejoRunnerConfig, error) {
47+
meta := &ForgejoRunnerConfig{}
48+
49+
if val, ok := config.TriggerMetadata["token"]; ok && val != "" {
50+
meta.RunnerMeta.Token = val
51+
} else {
52+
return nil, fmt.Errorf("no token given")
53+
}
54+
55+
if val, ok := config.TriggerMetadata["address"]; ok && val != "" {
56+
meta.RunnerMeta.Address = val
57+
} else {
58+
return nil, fmt.Errorf("no address given")
59+
}
60+
61+
if val, ok := config.TriggerMetadata["labels"]; ok && val != "" {
62+
meta.RunnerMeta.Labels = val
63+
} else {
64+
return nil, fmt.Errorf("no labels given")
65+
}
66+
67+
global := false
68+
if val, ok := config.TriggerMetadata["global"]; ok && val == stringTrue {
69+
global = true
70+
}
71+
72+
meta.RunnerMeta.Global = global
73+
74+
return meta, nil
75+
}
76+
77+
type forgejoRunnerScaler struct {
78+
metricType v2.MetricTargetType
79+
metadata *ForgejoRunnerConfig
80+
client *http.Client
81+
logger logr.Logger
82+
}
83+
84+
// NewForgejoRunnerScaler creates a new Forgejo Runner Scaler
85+
func NewForgejoRunnerScaler(config *scalersconfig.ScalerConfig) (Scaler, error) {
86+
c := kedautil.CreateHTTPClient(config.GlobalHTTPTimeout, false)
87+
88+
metricType, err := GetMetricTargetType(config)
89+
if err != nil {
90+
return nil, fmt.Errorf("error getting scaler metric type: %w", err)
91+
}
92+
93+
meta, err := parseForgejoRunnerMetadata(config)
94+
if err != nil {
95+
return nil, fmt.Errorf("error parsing Forgejo Runner metadata: %w", err)
96+
}
97+
98+
logger := InitializeLogger(config, "forgejo_runner_scaler")
99+
100+
return &forgejoRunnerScaler{
101+
client: c,
102+
metricType: metricType,
103+
metadata: meta,
104+
logger: logger,
105+
}, nil
106+
}
107+
108+
func (s *forgejoRunnerScaler) GetMetricsAndActivity(ctx context.Context, metricName string) ([]external_metrics.ExternalMetricValue, bool, error) {
109+
jobList, err := s.getJobsList(ctx)
110+
if err != nil {
111+
return []external_metrics.ExternalMetricValue{}, false, err
112+
}
113+
114+
metric := GenerateMetricInMili(metricName, float64(len(jobList.Jobs)))
115+
116+
metric.Value.Add(resource.Quantity{})
117+
118+
return []external_metrics.ExternalMetricValue{metric}, true, nil
119+
}
120+
121+
func (s *forgejoRunnerScaler) getJobsList(ctx context.Context) (forgejo.JobsListResponse, error) {
122+
jobList := forgejo.JobsListResponse{}
123+
124+
uri, err := s.getRunnerJobURL()
125+
if err != nil {
126+
return jobList, err
127+
}
128+
129+
req, err := http.NewRequestWithContext(ctx, "GET", uri.String(), nil)
130+
if err != nil {
131+
return jobList, err
132+
}
133+
134+
req.Header.Set("Authorization", fmt.Sprintf("token %s", s.metadata.RunnerMeta.Token))
135+
136+
r, err := s.client.Do(req)
137+
if err != nil {
138+
return jobList, err
139+
}
140+
141+
b, err := io.ReadAll(r.Body)
142+
if err != nil {
143+
return jobList, err
144+
}
145+
_ = r.Body.Close()
146+
147+
if r.StatusCode != 200 {
148+
return jobList,
149+
fmt.Errorf("the Forgejo REST API returned error. url: %s status: %d response: %s",
150+
s.metadata.RunnerMeta.Address,
151+
r.StatusCode,
152+
string(b),
153+
)
154+
}
155+
156+
err = json.Unmarshal(b, &jobList)
157+
if err != nil {
158+
return jobList, err
159+
}
160+
161+
return jobList, nil
162+
}
163+
164+
func (s *forgejoRunnerScaler) GetMetricSpecForScaling(_ context.Context) []v2.MetricSpec {
165+
externalMetric := &v2.ExternalMetricSource{
166+
Metric: v2.MetricIdentifier{
167+
Name: GenerateMetricNameWithIndex(
168+
1,
169+
kedautil.NormalizeString(fmt.Sprintf("forgejo-runner-%s", s.metadata.RunnerMeta.Address)),
170+
),
171+
},
172+
Target: GetMetricTarget(s.metricType, defaultForgejoJobsLen),
173+
}
174+
metricSpec := v2.MetricSpec{External: externalMetric, Type: externalMetricType}
175+
return []v2.MetricSpec{metricSpec}
176+
}
177+
178+
func (s *forgejoRunnerScaler) getRunnerJobURL() (*url.URL, error) {
179+
if s.metadata.RunnerMeta.Owner != "" && s.metadata.RunnerMeta.Repo != "" {
180+
return s.getRepoRunnerJobURL()
181+
}
182+
if s.metadata.RunnerMeta.Org != "" {
183+
return s.getOrgRunnerJobURL()
184+
}
185+
186+
if s.metadata.RunnerMeta.Global {
187+
return s.getGlobalRunnerJobsURL()
188+
}
189+
190+
return s.getUserRunnerJobsURL()
191+
}
192+
193+
func (s *forgejoRunnerScaler) getGlobalRunnerJobsURL() (*url.URL, error) {
194+
return url.Parse(
195+
fmt.Sprintf(
196+
"%s%s?labels=%s",
197+
s.metadata.RunnerMeta.Address,
198+
adminJobMetricPath,
199+
s.metadata.RunnerMeta.Labels,
200+
),
201+
)
202+
}
203+
204+
func (s *forgejoRunnerScaler) getUserRunnerJobsURL() (*url.URL, error) {
205+
return url.Parse(
206+
fmt.Sprintf(
207+
"%s%s?labels=%s",
208+
s.metadata.RunnerMeta.Address,
209+
UserJobMetricPath,
210+
s.metadata.RunnerMeta.Labels,
211+
),
212+
)
213+
}
214+
215+
func (s *forgejoRunnerScaler) getOrgRunnerJobURL() (*url.URL, error) {
216+
orgJobPath := fmt.Sprintf(orgJobMetricPath, s.metadata.RunnerMeta.Org)
217+
return url.Parse(
218+
fmt.Sprintf(
219+
"%s%s?labels=%s",
220+
s.metadata.RunnerMeta.Address,
221+
orgJobPath,
222+
s.metadata.RunnerMeta.Labels,
223+
),
224+
)
225+
}
226+
227+
func (s *forgejoRunnerScaler) getRepoRunnerJobURL() (*url.URL, error) {
228+
repoJobPath := fmt.Sprintf(repoJobMetricPath, s.metadata.RunnerMeta.Owner, s.metadata.RunnerMeta.Repo)
229+
return url.Parse(
230+
fmt.Sprintf(
231+
"%s%s?labels=%s",
232+
s.metadata.RunnerMeta.Address,
233+
repoJobPath,
234+
s.metadata.RunnerMeta.Labels,
235+
),
236+
)
237+
}
238+
239+
func (s *forgejoRunnerScaler) Close(_ context.Context) error {
240+
return nil
241+
}

0 commit comments

Comments
 (0)