Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gitlab runner scaler #6412

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
85410d6
Add new gitlab-runner-scaler; bootstrap configuration
fira42073 Aug 17, 2024
b922d62
Query gitlab (with pagination) to find out the number of pipelines in…
fira42073 Aug 17, 2024
10ee5bd
Introduce ClusterCloudEventSource (#5816)
SpiritZhou Aug 12, 2024
3975b8c
chore: Prepare v2.15.1 (#6051)
JorTurFer Aug 12, 2024
baef8e8
chore: Enable Az Pipeline (wi) e2e (#6070)
JorTurFer Aug 13, 2024
b186765
change config to declarative format
fira42073 Aug 24, 2024
21af44f
chore: Improve azure e2e coverage (#6090)
JorTurFer Aug 19, 2024
32ea028
Provide CloudEvents around the management of ScaledJobs resources (#6…
SpiritZhou Aug 19, 2024
9c08822
Refactor IBM MQ scaler and remove and deprecate variables (#6034)
rickbrouwer Aug 20, 2024
d1fbf48
add ignoreNullValues for AWS CloudWatch Scaler (#5635)
robpickerill Aug 20, 2024
a5e169e
Add connection name for the rabbitmq scaler (#6093)
robpickerill Aug 21, 2024
6015f2d
add capability to parse *url.URL
fira42073 Aug 25, 2024
47140fe
fix config parsing panic due to private fields
fira42073 Aug 25, 2024
e501824
add tests
fira42073 Aug 25, 2024
0de7956
fix scaler ordering
fira42073 Aug 25, 2024
5b7a372
Merge branch 'main' into gitlab-runner-scaler
fira42073 Aug 25, 2024
285de8b
add a simple unit test for url.URL parsing; fix broken tests in gitla…
fira42073 Aug 27, 2024
b7e4616
Merge branch 'gitlab-runner-scaler' of github.com:fira42073/keda into…
fira42073 Aug 27, 2024
98ba6d5
Merge branch 'main' into gitlab-runner-scaler
fira42073 Aug 29, 2024
da1677d
Merge branch 'main' into gitlab-runner-scaler
fira42073 Sep 7, 2024
3cfd7bc
Merge branch 'main' into gitlab-runner-scaler
fira42073 Nov 7, 2024
a1991a9
Merge branch 'main' into gitlab-runner-scaler
fira42073 Dec 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions pkg/scalers/gitlab_runner_scaler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package scalers

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"

"github.com/go-logr/logr"
v2 "k8s.io/api/autoscaling/v2"
"k8s.io/metrics/pkg/apis/external_metrics"

"github.com/kedacore/keda/v2/pkg/scalers/scalersconfig"
kedautil "github.com/kedacore/keda/v2/pkg/util"
)

const (
// pipelineWaitingForResourceStatus is the status of the pipelines that are waiting for resources.
pipelineWaitingForResourceStatus = "waiting_for_resource"

// maxGitlabAPIPageCount is the maximum number of pages to query for pipelines.
maxGitlabAPIPageCount = 50
// gitlabAPIPerPage is the number of pipelines to query per page.
gitlabAPIPerPage = "200"
)

type gitlabRunnerScaler struct {
metricType v2.MetricTargetType
metadata *gitlabRunnerMetadata
httpClient *http.Client
logger logr.Logger
}

type gitlabRunnerMetadata struct {
GitLabAPIURL *url.URL `keda:"name=gitlabAPIURL, order=triggerMetadata, default=https://gitlab.com, optional"`
PersonalAccessToken string `keda:"name=personalAccessToken, order=authParams"`
ProjectID string `keda:"name=projectID, order=triggerMetadata"`

TargetPipelineQueueLength int64 `keda:"name=targetPipelineQueueLength, order=triggerMetadata, default=1, optional"`
TriggerIndex int
}

// NewGitLabRunnerScaler creates a new GitLab Runner Scaler
func NewGitLabRunnerScaler(config *scalersconfig.ScalerConfig) (Scaler, error) {
httpClient := kedautil.CreateHTTPClient(config.GlobalHTTPTimeout, false)

metricType, err := GetMetricTargetType(config)
if err != nil {
return nil, fmt.Errorf("error getting scaler metric type: %w", err)
}

meta, err := parseGitLabRunnerMetadata(config)
if err != nil {
return nil, fmt.Errorf("error parsing GitLab Runner metadata: %w", err)
}

return &gitlabRunnerScaler{
metricType: metricType,
metadata: meta,
httpClient: httpClient,
logger: InitializeLogger(config, "gitlab_runner_scaler"),
}, nil
}

func parseGitLabRunnerMetadata(config *scalersconfig.ScalerConfig) (*gitlabRunnerMetadata, error) {
meta := gitlabRunnerMetadata{}

meta.TriggerIndex = config.TriggerIndex
if err := config.TypedConfig(&meta); err != nil {
return nil, fmt.Errorf("error parsing gitlabRunner metadata: %w", err)
}

uri := constructGitlabAPIPipelinesURL(*meta.GitLabAPIURL, meta.ProjectID, pipelineWaitingForResourceStatus)

meta.GitLabAPIURL = &uri

return &meta, nil
}

func (s *gitlabRunnerScaler) GetMetricsAndActivity(ctx context.Context, metricName string) ([]external_metrics.ExternalMetricValue, bool, error) {
queueLen, err := s.getPipelineQueueLength(ctx)

if err != nil {
s.logger.Error(err, "error getting workflow queue length")
return []external_metrics.ExternalMetricValue{}, false, err
}

metric := GenerateMetricInMili(metricName, float64(queueLen))

return []external_metrics.ExternalMetricValue{metric}, queueLen >= s.metadata.TargetPipelineQueueLength, nil
}

func (s *gitlabRunnerScaler) GetMetricSpecForScaling(_ context.Context) []v2.MetricSpec {
externalMetric := &v2.ExternalMetricSource{
Metric: v2.MetricIdentifier{
Name: GenerateMetricNameWithIndex(s.metadata.TriggerIndex, kedautil.NormalizeString(fmt.Sprintf("gitlab-runner-%s", s.metadata.ProjectID))),
},
Target: GetMetricTarget(s.metricType, s.metadata.TargetPipelineQueueLength),
}
metricSpec := v2.MetricSpec{External: externalMetric, Type: externalMetricType}
return []v2.MetricSpec{metricSpec}
}

func (s *gitlabRunnerScaler) Close(_ context.Context) error {
if s.httpClient != nil {
s.httpClient.CloseIdleConnections()
}
return nil
}
func constructGitlabAPIPipelinesURL(baseURL url.URL, projectID string, status string) url.URL {
baseURL.Path = "/api/v4/projects/" + projectID + "/pipelines"

qParams := baseURL.Query()
qParams.Set("status", status)
qParams.Set("per_page", gitlabAPIPerPage)

baseURL.RawQuery = qParams.Encode()

return baseURL
}

// getPipelineCount returns the number of pipelines in the GitLab project (per the page set in url)
func (s *gitlabRunnerScaler) getPipelineCount(ctx context.Context, uri string) (int64, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return 0, fmt.Errorf("creating request: %w", err)
}

req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("PRIVATE-TOKEN", s.metadata.PersonalAccessToken)

res, err := s.httpClient.Do(req)
if err != nil {
return 0, fmt.Errorf("doing request: %w", err)
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return 0, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}

gitlabPipelines := make([]struct{}, 0)
if err := json.NewDecoder(res.Body).Decode(&gitlabPipelines); err != nil {
return 0, fmt.Errorf("decoding response: %w", err)
}

return int64(len(gitlabPipelines)), nil
}

// getPipelineQueueLength returns the number of pipelines in the
// GitLab project that are waiting for resources.
func (s *gitlabRunnerScaler) getPipelineQueueLength(ctx context.Context) (int64, error) {
var count int64

page := 1
for ; page < maxGitlabAPIPageCount; page++ {
pagedURL := pagedURL(*s.metadata.GitLabAPIURL, fmt.Sprint(page))

gitlabPipelinesLen, err := s.getPipelineCount(ctx, pagedURL.String())
if err != nil {
return 0, err
}

if gitlabPipelinesLen == 0 {
break
}

count += gitlabPipelinesLen
}

return count, nil
}

func pagedURL(uri url.URL, page string) url.URL {
qParams := uri.Query()
qParams.Set("page", fmt.Sprint(page))

uri.RawQuery = qParams.Encode()

return uri
}
Loading
Loading