mirror of
https://github.com/actions/actions-runner-controller.git
synced 2025-12-10 19:50:30 +00:00
Compare commits
8 Commits
gha-runner
...
gha-runner
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d72774753c | ||
|
|
f7b6ad901d | ||
|
|
728f05c844 | ||
|
|
c00465973e | ||
|
|
5f23afaad3 | ||
|
|
47dfed3ced | ||
|
|
1f9b7541e6 | ||
|
|
a029b705cd |
2
.github/workflows/gha-e2e-tests.yaml
vendored
2
.github/workflows/gha-e2e-tests.yaml
vendored
@@ -16,7 +16,7 @@ env:
|
|||||||
TARGET_ORG: actions-runner-controller
|
TARGET_ORG: actions-runner-controller
|
||||||
TARGET_REPO: arc_e2e_test_dummy
|
TARGET_REPO: arc_e2e_test_dummy
|
||||||
IMAGE_NAME: "arc-test-image"
|
IMAGE_NAME: "arc-test-image"
|
||||||
IMAGE_VERSION: "0.8.0"
|
IMAGE_VERSION: "0.8.2"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
# This will make sure we only apply the concurrency limits on pull requests
|
# This will make sure we only apply the concurrency limits on pull requests
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -6,7 +6,7 @@ endif
|
|||||||
DOCKER_USER ?= $(shell echo ${DOCKER_IMAGE_NAME} | cut -d / -f1)
|
DOCKER_USER ?= $(shell echo ${DOCKER_IMAGE_NAME} | cut -d / -f1)
|
||||||
VERSION ?= dev
|
VERSION ?= dev
|
||||||
COMMIT_SHA = $(shell git rev-parse HEAD)
|
COMMIT_SHA = $(shell git rev-parse HEAD)
|
||||||
RUNNER_VERSION ?= 2.311.0
|
RUNNER_VERSION ?= 2.312.0
|
||||||
TARGETPLATFORM ?= $(shell arch)
|
TARGETPLATFORM ?= $(shell arch)
|
||||||
RUNNER_NAME ?= ${DOCKER_USER}/actions-runner
|
RUNNER_NAME ?= ${DOCKER_USER}/actions-runner
|
||||||
RUNNER_TAG ?= ${VERSION}
|
RUNNER_TAG ?= ${VERSION}
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ type: application
|
|||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# 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.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 0.8.0
|
version: 0.8.2
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# This is the version number of the application being deployed. This version number should be
|
||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
appVersion: "0.8.0"
|
appVersion: "0.8.2"
|
||||||
|
|
||||||
home: https://github.com/actions/actions-runner-controller
|
home: https://github.com/actions/actions-runner-controller
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ type: application
|
|||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# 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.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 0.8.0
|
version: 0.8.2
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# This is the version number of the application being deployed. This version number should be
|
||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
appVersion: "0.8.0"
|
appVersion: "0.8.2"
|
||||||
|
|
||||||
home: https://github.com/actions/actions-runner-controller
|
home: https://github.com/actions/actions-runner-controller
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ type Listener interface {
|
|||||||
//go:generate mockery --name Worker --output ./mocks --outpkg mocks --case underscore
|
//go:generate mockery --name Worker --output ./mocks --outpkg mocks --case underscore
|
||||||
type Worker interface {
|
type Worker interface {
|
||||||
HandleJobStarted(ctx context.Context, jobInfo *actions.JobStarted) error
|
HandleJobStarted(ctx context.Context, jobInfo *actions.JobStarted) error
|
||||||
HandleDesiredRunnerCount(ctx context.Context, desiredRunnerCount int) error
|
HandleDesiredRunnerCount(ctx context.Context, count int) (int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(config config.Config) (*App, error) {
|
func New(config config.Config) (*App, error) {
|
||||||
|
|||||||
@@ -15,18 +15,28 @@ type Worker struct {
|
|||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleDesiredRunnerCount provides a mock function with given fields: ctx, desiredRunnerCount
|
// HandleDesiredRunnerCount provides a mock function with given fields: ctx, count
|
||||||
func (_m *Worker) HandleDesiredRunnerCount(ctx context.Context, desiredRunnerCount int) error {
|
func (_m *Worker) HandleDesiredRunnerCount(ctx context.Context, count int) (int, error) {
|
||||||
ret := _m.Called(ctx, desiredRunnerCount)
|
ret := _m.Called(ctx, count)
|
||||||
|
|
||||||
var r0 error
|
var r0 int
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, int) error); ok {
|
var r1 error
|
||||||
r0 = rf(ctx, desiredRunnerCount)
|
if rf, ok := ret.Get(0).(func(context.Context, int) (int, error)); ok {
|
||||||
|
return rf(ctx, count)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
|
||||||
|
r0 = rf(ctx, count)
|
||||||
} else {
|
} else {
|
||||||
r0 = ret.Error(0)
|
r0 = ret.Get(0).(int)
|
||||||
}
|
}
|
||||||
|
|
||||||
return r0
|
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
|
||||||
|
r1 = rf(ctx, count)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleJobStarted provides a mock function with given fields: ctx, jobInfo
|
// HandleJobStarted provides a mock function with given fields: ctx, jobInfo
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/actions/actions-runner-controller/build"
|
"github.com/actions/actions-runner-controller/build"
|
||||||
@@ -101,7 +103,7 @@ func (c *Config) Logger() (logr.Logger, error) {
|
|||||||
return logger, nil
|
return logger, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) ActionsClient(logger logr.Logger) (*actions.Client, error) {
|
func (c *Config) ActionsClient(logger logr.Logger, clientOptions ...actions.ClientOption) (*actions.Client, error) {
|
||||||
var creds actions.ActionsAuth
|
var creds actions.ActionsAuth
|
||||||
switch c.Token {
|
switch c.Token {
|
||||||
case "":
|
case "":
|
||||||
@@ -114,9 +116,9 @@ func (c *Config) ActionsClient(logger logr.Logger) (*actions.Client, error) {
|
|||||||
creds.Token = c.Token
|
creds.Token = c.Token
|
||||||
}
|
}
|
||||||
|
|
||||||
options := []actions.ClientOption{
|
options := append([]actions.ClientOption{
|
||||||
actions.WithLogger(logger),
|
actions.WithLogger(logger),
|
||||||
}
|
}, clientOptions...)
|
||||||
|
|
||||||
if c.ServerRootCA != "" {
|
if c.ServerRootCA != "" {
|
||||||
systemPool, err := x509.SystemCertPool()
|
systemPool, err := x509.SystemCertPool()
|
||||||
@@ -132,6 +134,11 @@ func (c *Config) ActionsClient(logger logr.Logger) (*actions.Client, error) {
|
|||||||
options = append(options, actions.WithRootCAs(pool))
|
options = append(options, actions.WithRootCAs(pool))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proxyFunc := httpproxy.FromEnvironment().ProxyFunc()
|
||||||
|
options = append(options, actions.WithProxy(func(req *http.Request) (*url.URL, error) {
|
||||||
|
return proxyFunc(req.URL)
|
||||||
|
}))
|
||||||
|
|
||||||
client, err := actions.NewClient(c.ConfigureUrl, &creds, options...)
|
client, err := actions.NewClient(c.ConfigureUrl, &creds, options...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create actions client: %w", err)
|
return nil, fmt.Errorf("failed to create actions client: %w", err)
|
||||||
|
|||||||
161
cmd/ghalistener/config/config_client_test.go
Normal file
161
cmd/ghalistener/config/config_client_test.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/actions/actions-runner-controller/cmd/ghalistener/config"
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions/testserver"
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCustomerServerRootCA(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
certsFolder := filepath.Join(
|
||||||
|
"../../../",
|
||||||
|
"github",
|
||||||
|
"actions",
|
||||||
|
"testdata",
|
||||||
|
)
|
||||||
|
certPath := filepath.Join(certsFolder, "server.crt")
|
||||||
|
keyPath := filepath.Join(certsFolder, "server.key")
|
||||||
|
|
||||||
|
serverCalledSuccessfully := false
|
||||||
|
|
||||||
|
server := testserver.NewUnstarted(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
serverCalledSuccessfully = true
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"count": 0}`))
|
||||||
|
}))
|
||||||
|
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
server.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||||
|
server.StartTLS()
|
||||||
|
|
||||||
|
var certsString string
|
||||||
|
rootCA, err := os.ReadFile(filepath.Join(certsFolder, "rootCA.crt"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
certsString = string(rootCA)
|
||||||
|
|
||||||
|
intermediate, err := os.ReadFile(filepath.Join(certsFolder, "intermediate.pem"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
certsString = certsString + string(intermediate)
|
||||||
|
|
||||||
|
config := config.Config{
|
||||||
|
ConfigureUrl: server.ConfigURLForOrg("myorg"),
|
||||||
|
ServerRootCA: certsString,
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := config.ActionsClient(logr.Discard())
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = client.GetRunnerScaleSet(ctx, 1, "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, serverCalledSuccessfully)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProxySettings(t *testing.T) {
|
||||||
|
t.Run("http", func(t *testing.T) {
|
||||||
|
wentThroughProxy := false
|
||||||
|
|
||||||
|
proxy := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||||
|
wentThroughProxy = true
|
||||||
|
}))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
proxy.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
prevProxy := os.Getenv("http_proxy")
|
||||||
|
os.Setenv("http_proxy", proxy.URL)
|
||||||
|
defer os.Setenv("http_proxy", prevProxy)
|
||||||
|
|
||||||
|
config := config.Config{
|
||||||
|
ConfigureUrl: "https://github.com/org/repo",
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := config.ActionsClient(logr.Discard())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, wentThroughProxy)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("https", func(t *testing.T) {
|
||||||
|
wentThroughProxy := false
|
||||||
|
|
||||||
|
proxy := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||||
|
wentThroughProxy = true
|
||||||
|
}))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
proxy.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
prevProxy := os.Getenv("https_proxy")
|
||||||
|
os.Setenv("https_proxy", proxy.URL)
|
||||||
|
defer os.Setenv("https_proxy", prevProxy)
|
||||||
|
|
||||||
|
config := config.Config{
|
||||||
|
ConfigureUrl: "https://github.com/org/repo",
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := config.ActionsClient(logr.Discard(), actions.WithRetryMax(0))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.Do(req)
|
||||||
|
// proxy doesn't support https
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, wentThroughProxy)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no_proxy", func(t *testing.T) {
|
||||||
|
wentThroughProxy := false
|
||||||
|
|
||||||
|
proxy := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||||
|
wentThroughProxy = true
|
||||||
|
}))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
proxy.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
prevProxy := os.Getenv("http_proxy")
|
||||||
|
os.Setenv("http_proxy", proxy.URL)
|
||||||
|
defer os.Setenv("http_proxy", prevProxy)
|
||||||
|
|
||||||
|
prevNoProxy := os.Getenv("no_proxy")
|
||||||
|
os.Setenv("no_proxy", "example.com")
|
||||||
|
defer os.Setenv("no_proxy", prevNoProxy)
|
||||||
|
|
||||||
|
config := config.Config{
|
||||||
|
ConfigureUrl: "https://github.com/org/repo",
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := config.ActionsClient(logr.Discard())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, wentThroughProxy)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ type Client interface {
|
|||||||
DeleteMessage(ctx context.Context, messageQueueUrl, messageQueueAccessToken string, messageId int64) error
|
DeleteMessage(ctx context.Context, messageQueueUrl, messageQueueAccessToken string, messageId int64) error
|
||||||
AcquireJobs(ctx context.Context, runnerScaleSetId int, messageQueueAccessToken string, requestIds []int64) ([]int64, error)
|
AcquireJobs(ctx context.Context, runnerScaleSetId int, messageQueueAccessToken string, requestIds []int64) ([]int64, error)
|
||||||
RefreshMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) (*actions.RunnerScaleSetSession, error)
|
RefreshMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) (*actions.RunnerScaleSetSession, error)
|
||||||
|
DeleteMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -113,7 +114,7 @@ func New(config Config) (*Listener, error) {
|
|||||||
//go:generate mockery --name Handler --output ./mocks --outpkg mocks --case underscore
|
//go:generate mockery --name Handler --output ./mocks --outpkg mocks --case underscore
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
HandleJobStarted(ctx context.Context, jobInfo *actions.JobStarted) error
|
HandleJobStarted(ctx context.Context, jobInfo *actions.JobStarted) error
|
||||||
HandleDesiredRunnerCount(ctx context.Context, desiredRunnerCount int) error
|
HandleDesiredRunnerCount(ctx context.Context, count int) (int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen listens for incoming messages and handles them using the provided handler.
|
// Listen listens for incoming messages and handles them using the provided handler.
|
||||||
@@ -126,6 +127,12 @@ func (l *Listener) Listen(ctx context.Context, handler Handler) error {
|
|||||||
return fmt.Errorf("createSession failed: %w", err)
|
return fmt.Errorf("createSession failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := l.deleteMessageSession(); err != nil {
|
||||||
|
l.logger.Error(err, "failed to delete message session")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
initialMessage := &actions.RunnerScaleSetMessage{
|
initialMessage := &actions.RunnerScaleSetMessage{
|
||||||
MessageId: 0,
|
MessageId: 0,
|
||||||
MessageType: "RunnerScaleSetJobMessages",
|
MessageType: "RunnerScaleSetJobMessages",
|
||||||
@@ -133,28 +140,21 @@ func (l *Listener) Listen(ctx context.Context, handler Handler) error {
|
|||||||
Body: "",
|
Body: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.session.Statistics.TotalAvailableJobs > 0 || l.session.Statistics.TotalAssignedJobs > 0 {
|
if l.session.Statistics == nil {
|
||||||
acquirableJobs, err := l.client.GetAcquirableJobs(ctx, l.scaleSetID)
|
return fmt.Errorf("session statistics is nil")
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to call GetAcquirableJobs: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
acquirableJobsJson, err := json.Marshal(acquirableJobs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal acquirable jobs: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
initialMessage.Body = string(acquirableJobsJson)
|
|
||||||
}
|
}
|
||||||
|
l.metrics.PublishStatistics(initialMessage.Statistics)
|
||||||
|
|
||||||
if err := handler.HandleDesiredRunnerCount(ctx, initialMessage.Statistics.TotalAssignedJobs); err != nil {
|
desiredRunners, err := handler.HandleDesiredRunnerCount(ctx, initialMessage.Statistics.TotalAssignedJobs)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("handling initial message failed: %w", err)
|
return fmt.Errorf("handling initial message failed: %w", err)
|
||||||
}
|
}
|
||||||
|
l.metrics.PublishDesiredRunners(desiredRunners)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return fmt.Errorf("context cancelled: %w", ctx.Err())
|
return ctx.Err()
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,29 +167,54 @@ func (l *Listener) Listen(ctx context.Context, handler Handler) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
statistics, jobsStarted, err := l.parseMessage(ctx, msg)
|
// New context is created to avoid cancelation during message handling.
|
||||||
if err != nil {
|
if err := l.handleMessage(context.Background(), handler, msg); err != nil {
|
||||||
return fmt.Errorf("failed to parse message: %w", err)
|
return fmt.Errorf("failed to handle message: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
l.lastMessageID = msg.MessageId
|
|
||||||
|
|
||||||
if err := l.deleteLastMessage(ctx); err != nil {
|
|
||||||
return fmt.Errorf("failed to delete message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, jobStarted := range jobsStarted {
|
|
||||||
if err := handler.HandleJobStarted(ctx, jobStarted); err != nil {
|
|
||||||
return fmt.Errorf("failed to handle job started: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := handler.HandleDesiredRunnerCount(ctx, statistics.TotalAssignedJobs); err != nil {
|
|
||||||
return fmt.Errorf("failed to handle desired runner count: %w", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Listener) handleMessage(ctx context.Context, handler Handler, msg *actions.RunnerScaleSetMessage) error {
|
||||||
|
parsedMsg, err := l.parseMessage(ctx, msg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse message: %w", err)
|
||||||
|
}
|
||||||
|
l.metrics.PublishStatistics(parsedMsg.statistics)
|
||||||
|
|
||||||
|
if len(parsedMsg.jobsAvailable) > 0 {
|
||||||
|
acquiredJobIDs, err := l.acquireAvailableJobs(ctx, parsedMsg.jobsAvailable)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to acquire jobs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.logger.Info("Jobs are acquired", "count", len(acquiredJobIDs), "requestIds", fmt.Sprint(acquiredJobIDs))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, jobCompleted := range parsedMsg.jobsCompleted {
|
||||||
|
l.metrics.PublishJobCompleted(jobCompleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.lastMessageID = msg.MessageId
|
||||||
|
|
||||||
|
if err := l.deleteLastMessage(ctx); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, jobStarted := range parsedMsg.jobsStarted {
|
||||||
|
if err := handler.HandleJobStarted(ctx, jobStarted); err != nil {
|
||||||
|
return fmt.Errorf("failed to handle job started: %w", err)
|
||||||
|
}
|
||||||
|
l.metrics.PublishJobStarted(jobStarted)
|
||||||
|
}
|
||||||
|
|
||||||
|
desiredRunners, err := handler.HandleDesiredRunnerCount(ctx, parsedMsg.statistics.TotalAssignedJobs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to handle desired runner count: %w", err)
|
||||||
|
}
|
||||||
|
l.metrics.PublishDesiredRunners(desiredRunners)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (l *Listener) createSession(ctx context.Context) error {
|
func (l *Listener) createSession(ctx context.Context) error {
|
||||||
var session *actions.RunnerScaleSetSession
|
var session *actions.RunnerScaleSetSession
|
||||||
var retries int
|
var retries int
|
||||||
@@ -271,48 +296,57 @@ func (l *Listener) deleteLastMessage(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Listener) parseMessage(ctx context.Context, msg *actions.RunnerScaleSetMessage) (*actions.RunnerScaleSetStatistic, []*actions.JobStarted, error) {
|
type parsedMessage struct {
|
||||||
|
statistics *actions.RunnerScaleSetStatistic
|
||||||
|
jobsStarted []*actions.JobStarted
|
||||||
|
jobsAvailable []*actions.JobAvailable
|
||||||
|
jobsCompleted []*actions.JobCompleted
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Listener) parseMessage(ctx context.Context, msg *actions.RunnerScaleSetMessage) (*parsedMessage, error) {
|
||||||
|
if msg.MessageType != "RunnerScaleSetJobMessages" {
|
||||||
|
l.logger.Info("Skipping message", "messageType", msg.MessageType)
|
||||||
|
return nil, fmt.Errorf("invalid message type: %s", msg.MessageType)
|
||||||
|
}
|
||||||
|
|
||||||
l.logger.Info("Processing message", "messageId", msg.MessageId, "messageType", msg.MessageType)
|
l.logger.Info("Processing message", "messageId", msg.MessageId, "messageType", msg.MessageType)
|
||||||
if msg.Statistics == nil {
|
if msg.Statistics == nil {
|
||||||
return nil, nil, fmt.Errorf("invalid message: statistics is nil")
|
return nil, fmt.Errorf("invalid message: statistics is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
l.logger.Info("New runner scale set statistics.", "statistics", msg.Statistics)
|
l.logger.Info("New runner scale set statistics.", "statistics", msg.Statistics)
|
||||||
|
|
||||||
if msg.MessageType != "RunnerScaleSetJobMessages" {
|
|
||||||
l.logger.Info("Skipping message", "messageType", msg.MessageType)
|
|
||||||
return nil, nil, fmt.Errorf("invalid message type: %s", msg.MessageType)
|
|
||||||
}
|
|
||||||
|
|
||||||
var batchedMessages []json.RawMessage
|
var batchedMessages []json.RawMessage
|
||||||
if len(msg.Body) > 0 {
|
if len(msg.Body) > 0 {
|
||||||
if err := json.Unmarshal([]byte(msg.Body), &batchedMessages); err != nil {
|
if err := json.Unmarshal([]byte(msg.Body), &batchedMessages); err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to unmarshal batched messages: %w", err)
|
return nil, fmt.Errorf("failed to unmarshal batched messages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var availableJobs []int64
|
parsedMsg := &parsedMessage{
|
||||||
var startedJobs []*actions.JobStarted
|
statistics: msg.Statistics,
|
||||||
|
}
|
||||||
|
|
||||||
for _, msg := range batchedMessages {
|
for _, msg := range batchedMessages {
|
||||||
var messageType actions.JobMessageType
|
var messageType actions.JobMessageType
|
||||||
if err := json.Unmarshal(msg, &messageType); err != nil {
|
if err := json.Unmarshal(msg, &messageType); err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to decode job message type: %w", err)
|
return nil, fmt.Errorf("failed to decode job message type: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch messageType.MessageType {
|
switch messageType.MessageType {
|
||||||
case messageTypeJobAvailable:
|
case messageTypeJobAvailable:
|
||||||
var jobAvailable actions.JobAvailable
|
var jobAvailable actions.JobAvailable
|
||||||
if err := json.Unmarshal(msg, &jobAvailable); err != nil {
|
if err := json.Unmarshal(msg, &jobAvailable); err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to decode job available: %w", err)
|
return nil, fmt.Errorf("failed to decode job available: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.logger.Info("Job available message received", "jobId", jobAvailable.RunnerRequestId)
|
l.logger.Info("Job available message received", "jobId", jobAvailable.RunnerRequestId)
|
||||||
availableJobs = append(availableJobs, jobAvailable.RunnerRequestId)
|
parsedMsg.jobsAvailable = append(parsedMsg.jobsAvailable, &jobAvailable)
|
||||||
|
|
||||||
case messageTypeJobAssigned:
|
case messageTypeJobAssigned:
|
||||||
var jobAssigned actions.JobAssigned
|
var jobAssigned actions.JobAssigned
|
||||||
if err := json.Unmarshal(msg, &jobAssigned); err != nil {
|
if err := json.Unmarshal(msg, &jobAssigned); err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to decode job assigned: %w", err)
|
return nil, fmt.Errorf("failed to decode job assigned: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.logger.Info("Job assigned message received", "jobId", jobAssigned.RunnerRequestId)
|
l.logger.Info("Job assigned message received", "jobId", jobAssigned.RunnerRequestId)
|
||||||
@@ -320,41 +354,37 @@ func (l *Listener) parseMessage(ctx context.Context, msg *actions.RunnerScaleSet
|
|||||||
case messageTypeJobStarted:
|
case messageTypeJobStarted:
|
||||||
var jobStarted actions.JobStarted
|
var jobStarted actions.JobStarted
|
||||||
if err := json.Unmarshal(msg, &jobStarted); err != nil {
|
if err := json.Unmarshal(msg, &jobStarted); err != nil {
|
||||||
return nil, nil, fmt.Errorf("could not decode job started message. %w", err)
|
return nil, fmt.Errorf("could not decode job started message. %w", err)
|
||||||
}
|
}
|
||||||
l.logger.Info("Job started message received.", "RequestId", jobStarted.RunnerRequestId, "RunnerId", jobStarted.RunnerId)
|
l.logger.Info("Job started message received.", "RequestId", jobStarted.RunnerRequestId, "RunnerId", jobStarted.RunnerId)
|
||||||
startedJobs = append(startedJobs, &jobStarted)
|
parsedMsg.jobsStarted = append(parsedMsg.jobsStarted, &jobStarted)
|
||||||
|
|
||||||
case messageTypeJobCompleted:
|
case messageTypeJobCompleted:
|
||||||
var jobCompleted actions.JobCompleted
|
var jobCompleted actions.JobCompleted
|
||||||
if err := json.Unmarshal(msg, &jobCompleted); err != nil {
|
if err := json.Unmarshal(msg, &jobCompleted); err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to decode job completed: %w", err)
|
return nil, fmt.Errorf("failed to decode job completed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.logger.Info("Job completed message received.", "RequestId", jobCompleted.RunnerRequestId, "Result", jobCompleted.Result, "RunnerId", jobCompleted.RunnerId, "RunnerName", jobCompleted.RunnerName)
|
l.logger.Info("Job completed message received.", "RequestId", jobCompleted.RunnerRequestId, "Result", jobCompleted.Result, "RunnerId", jobCompleted.RunnerId, "RunnerName", jobCompleted.RunnerName)
|
||||||
|
parsedMsg.jobsCompleted = append(parsedMsg.jobsCompleted, &jobCompleted)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
l.logger.Info("unknown job message type.", "messageType", messageType.MessageType)
|
l.logger.Info("unknown job message type.", "messageType", messageType.MessageType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
l.logger.Info("Available jobs.", "count", len(availableJobs), "requestIds", fmt.Sprint(availableJobs))
|
return parsedMsg, nil
|
||||||
if len(availableJobs) > 0 {
|
|
||||||
acquired, err := l.acquireAvailableJobs(ctx, availableJobs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
l.logger.Info("Jobs are acquired", "count", len(acquired), "requestIds", fmt.Sprint(acquired))
|
|
||||||
}
|
|
||||||
|
|
||||||
return msg.Statistics, startedJobs, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Listener) acquireAvailableJobs(ctx context.Context, availableJobs []int64) ([]int64, error) {
|
func (l *Listener) acquireAvailableJobs(ctx context.Context, jobsAvailable []*actions.JobAvailable) ([]int64, error) {
|
||||||
l.logger.Info("Acquiring jobs")
|
ids := make([]int64, 0, len(jobsAvailable))
|
||||||
|
for _, job := range jobsAvailable {
|
||||||
|
ids = append(ids, job.RunnerRequestId)
|
||||||
|
}
|
||||||
|
|
||||||
ids, err := l.client.AcquireJobs(ctx, l.scaleSetID, l.session.MessageQueueAccessToken, availableJobs)
|
l.logger.Info("Acquiring jobs", "count", len(ids), "requestIds", fmt.Sprint(ids))
|
||||||
|
|
||||||
|
ids, err := l.client.AcquireJobs(ctx, l.scaleSetID, l.session.MessageQueueAccessToken, ids)
|
||||||
if err == nil { // if NO errors
|
if err == nil { // if NO errors
|
||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
@@ -368,7 +398,7 @@ func (l *Listener) acquireAvailableJobs(ctx context.Context, availableJobs []int
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ids, err = l.client.AcquireJobs(ctx, l.scaleSetID, l.session.MessageQueueAccessToken, availableJobs)
|
ids, err = l.client.AcquireJobs(ctx, l.scaleSetID, l.session.MessageQueueAccessToken, ids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to acquire jobs after session refresh: %w", err)
|
return nil, fmt.Errorf("failed to acquire jobs after session refresh: %w", err)
|
||||||
}
|
}
|
||||||
@@ -386,3 +416,16 @@ func (l *Listener) refreshSession(ctx context.Context) error {
|
|||||||
l.session = session
|
l.session = session
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Listener) deleteMessageSession() error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
l.logger.Info("Deleting message session")
|
||||||
|
|
||||||
|
if err := l.client.DeleteMessageSession(ctx, l.session.RunnerScaleSet.Id, l.session.SessionId); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete message session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package listener
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -9,7 +10,6 @@ import (
|
|||||||
|
|
||||||
listenermocks "github.com/actions/actions-runner-controller/cmd/ghalistener/listener/mocks"
|
listenermocks "github.com/actions/actions-runner-controller/cmd/ghalistener/listener/mocks"
|
||||||
"github.com/actions/actions-runner-controller/cmd/ghalistener/metrics"
|
"github.com/actions/actions-runner-controller/cmd/ghalistener/metrics"
|
||||||
metricsmocks "github.com/actions/actions-runner-controller/cmd/ghalistener/metrics/mocks"
|
|
||||||
"github.com/actions/actions-runner-controller/github/actions"
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -38,22 +38,6 @@ func TestNew(t *testing.T) {
|
|||||||
assert.NotNil(t, l)
|
assert.NotNil(t, l)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("SetStaticMetrics", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
metrics := metricsmocks.NewPublisher(t)
|
|
||||||
|
|
||||||
metrics.On("PublishStatic", mock.Anything, mock.Anything).Once()
|
|
||||||
|
|
||||||
config := Config{
|
|
||||||
Client: listenermocks.NewClient(t),
|
|
||||||
ScaleSetID: 1,
|
|
||||||
Metrics: metrics,
|
|
||||||
}
|
|
||||||
l, err := New(config)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.NotNil(t, l)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListener_createSession(t *testing.T) {
|
func TestListener_createSession(t *testing.T) {
|
||||||
@@ -435,6 +419,8 @@ func TestListener_Listen(t *testing.T) {
|
|||||||
Statistics: &actions.RunnerScaleSetStatistic{},
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
}
|
}
|
||||||
client.On("CreateMessageSession", ctx, mock.Anything, mock.Anything).Return(session, nil).Once()
|
client.On("CreateMessageSession", ctx, mock.Anything, mock.Anything).Return(session, nil).Once()
|
||||||
|
client.On("DeleteMessageSession", mock.Anything, session.RunnerScaleSet.Id, session.SessionId).Return(nil).Once()
|
||||||
|
|
||||||
config.Client = client
|
config.Client = client
|
||||||
|
|
||||||
l, err := New(config)
|
l, err := New(config)
|
||||||
@@ -443,7 +429,7 @@ func TestListener_Listen(t *testing.T) {
|
|||||||
var called bool
|
var called bool
|
||||||
handler := listenermocks.NewHandler(t)
|
handler := listenermocks.NewHandler(t)
|
||||||
handler.On("HandleDesiredRunnerCount", mock.Anything, mock.Anything).
|
handler.On("HandleDesiredRunnerCount", mock.Anything, mock.Anything).
|
||||||
Return(nil).
|
Return(0, nil).
|
||||||
Run(
|
Run(
|
||||||
func(mock.Arguments) {
|
func(mock.Arguments) {
|
||||||
called = true
|
called = true
|
||||||
@@ -456,6 +442,64 @@ func TestListener_Listen(t *testing.T) {
|
|||||||
assert.True(t, errors.Is(err, context.Canceled))
|
assert.True(t, errors.Is(err, context.Canceled))
|
||||||
assert.True(t, called)
|
assert.True(t, called)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("CancelContextAfterGetMessage", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
config := Config{
|
||||||
|
ScaleSetID: 1,
|
||||||
|
Metrics: metrics.Discard,
|
||||||
|
}
|
||||||
|
|
||||||
|
client := listenermocks.NewClient(t)
|
||||||
|
uuid := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &uuid,
|
||||||
|
OwnerName: "example",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{},
|
||||||
|
MessageQueueUrl: "https://example.com",
|
||||||
|
MessageQueueAccessToken: "1234567890",
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
|
}
|
||||||
|
client.On("CreateMessageSession", ctx, mock.Anything, mock.Anything).Return(session, nil).Once()
|
||||||
|
client.On("DeleteMessageSession", mock.Anything, session.RunnerScaleSet.Id, session.SessionId).Return(nil).Once()
|
||||||
|
|
||||||
|
msg := &actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "RunnerScaleSetJobMessages",
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
|
}
|
||||||
|
client.On("GetMessage", ctx, mock.Anything, mock.Anything, mock.Anything).
|
||||||
|
Return(msg, nil).
|
||||||
|
Run(
|
||||||
|
func(mock.Arguments) {
|
||||||
|
cancel()
|
||||||
|
},
|
||||||
|
).
|
||||||
|
Once()
|
||||||
|
|
||||||
|
// Ensure delete message is called with background context
|
||||||
|
client.On("DeleteMessage", context.Background(), mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
||||||
|
|
||||||
|
config.Client = client
|
||||||
|
|
||||||
|
handler := listenermocks.NewHandler(t)
|
||||||
|
handler.On("HandleDesiredRunnerCount", mock.Anything, mock.Anything).
|
||||||
|
Return(0, nil).
|
||||||
|
Once()
|
||||||
|
|
||||||
|
handler.On("HandleDesiredRunnerCount", mock.Anything, mock.Anything).
|
||||||
|
Return(0, nil).
|
||||||
|
Once()
|
||||||
|
|
||||||
|
l, err := New(config)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
err = l.Listen(ctx, handler)
|
||||||
|
assert.ErrorIs(t, context.Canceled, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListener_acquireAvailableJobs(t *testing.T) {
|
func TestListener_acquireAvailableJobs(t *testing.T) {
|
||||||
@@ -489,7 +533,24 @@ func TestListener_acquireAvailableJobs(t *testing.T) {
|
|||||||
Statistics: &actions.RunnerScaleSetStatistic{},
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = l.acquireAvailableJobs(ctx, []int64{1, 2, 3})
|
availableJobs := []*actions.JobAvailable{
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
RunnerRequestId: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
RunnerRequestId: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
RunnerRequestId: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = l.acquireAvailableJobs(ctx, availableJobs)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -523,9 +584,26 @@ func TestListener_acquireAvailableJobs(t *testing.T) {
|
|||||||
Statistics: &actions.RunnerScaleSetStatistic{},
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
}
|
}
|
||||||
|
|
||||||
acquiredJobIDs, err := l.acquireAvailableJobs(ctx, []int64{1, 2, 3})
|
availableJobs := []*actions.JobAvailable{
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
RunnerRequestId: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
RunnerRequestId: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
RunnerRequestId: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
acquiredJobIDs, err := l.acquireAvailableJobs(ctx, availableJobs)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, jobIDs, acquiredJobIDs)
|
assert.Equal(t, []int64{1, 2, 3}, acquiredJobIDs)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("RefreshAndSucceeds", func(t *testing.T) {
|
t.Run("RefreshAndSucceeds", func(t *testing.T) {
|
||||||
@@ -555,6 +633,23 @@ func TestListener_acquireAvailableJobs(t *testing.T) {
|
|||||||
|
|
||||||
// Second call to AcquireJobs will succeed
|
// Second call to AcquireJobs will succeed
|
||||||
want := []int64{1, 2, 3}
|
want := []int64{1, 2, 3}
|
||||||
|
availableJobs := []*actions.JobAvailable{
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
RunnerRequestId: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
RunnerRequestId: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
RunnerRequestId: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
client.On("AcquireJobs", ctx, mock.Anything, mock.Anything, mock.Anything).Return(want, nil).Once()
|
client.On("AcquireJobs", ctx, mock.Anything, mock.Anything, mock.Anything).Return(want, nil).Once()
|
||||||
|
|
||||||
config.Client = client
|
config.Client = client
|
||||||
@@ -567,7 +662,7 @@ func TestListener_acquireAvailableJobs(t *testing.T) {
|
|||||||
RunnerScaleSet: &actions.RunnerScaleSet{},
|
RunnerScaleSet: &actions.RunnerScaleSet{},
|
||||||
}
|
}
|
||||||
|
|
||||||
got, err := l.acquireAvailableJobs(ctx, want)
|
got, err := l.acquireAvailableJobs(ctx, availableJobs)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, want, got)
|
assert.Equal(t, want, got)
|
||||||
})
|
})
|
||||||
@@ -606,8 +701,165 @@ func TestListener_acquireAvailableJobs(t *testing.T) {
|
|||||||
RunnerScaleSet: &actions.RunnerScaleSet{},
|
RunnerScaleSet: &actions.RunnerScaleSet{},
|
||||||
}
|
}
|
||||||
|
|
||||||
got, err := l.acquireAvailableJobs(ctx, []int64{1, 2, 3})
|
availableJobs := []*actions.JobAvailable{
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
RunnerRequestId: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
RunnerRequestId: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
RunnerRequestId: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := l.acquireAvailableJobs(ctx, availableJobs)
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
assert.Nil(t, got)
|
assert.Nil(t, got)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListener_parseMessage(t *testing.T) {
|
||||||
|
t.Run("FailOnEmptyStatistics", func(t *testing.T) {
|
||||||
|
msg := &actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "RunnerScaleSetJobMessages",
|
||||||
|
Statistics: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
l := &Listener{}
|
||||||
|
parsedMsg, err := l.parseMessage(context.Background(), msg)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, parsedMsg)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FailOnIncorrectMessageType", func(t *testing.T) {
|
||||||
|
msg := &actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "RunnerMessages", // arbitrary message type
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
|
}
|
||||||
|
|
||||||
|
l := &Listener{}
|
||||||
|
parsedMsg, err := l.parseMessage(context.Background(), msg)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, parsedMsg)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ParseAll", func(t *testing.T) {
|
||||||
|
msg := &actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "RunnerScaleSetJobMessages",
|
||||||
|
Body: "",
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{
|
||||||
|
TotalAvailableJobs: 1,
|
||||||
|
TotalAcquiredJobs: 2,
|
||||||
|
TotalAssignedJobs: 3,
|
||||||
|
TotalRunningJobs: 4,
|
||||||
|
TotalRegisteredRunners: 5,
|
||||||
|
TotalBusyRunners: 6,
|
||||||
|
TotalIdleRunners: 7,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var batchedMessages []any
|
||||||
|
jobsAvailable := []*actions.JobAvailable{
|
||||||
|
{
|
||||||
|
AcquireJobUrl: "https://github.com/example",
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
JobMessageType: actions.JobMessageType{
|
||||||
|
MessageType: messageTypeJobAvailable,
|
||||||
|
},
|
||||||
|
RunnerRequestId: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AcquireJobUrl: "https://github.com/example",
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
JobMessageType: actions.JobMessageType{
|
||||||
|
MessageType: messageTypeJobAvailable,
|
||||||
|
},
|
||||||
|
RunnerRequestId: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, msg := range jobsAvailable {
|
||||||
|
batchedMessages = append(batchedMessages, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
jobsAssigned := []*actions.JobAssigned{
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
JobMessageType: actions.JobMessageType{
|
||||||
|
MessageType: messageTypeJobAssigned,
|
||||||
|
},
|
||||||
|
RunnerRequestId: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
JobMessageType: actions.JobMessageType{
|
||||||
|
MessageType: messageTypeJobAssigned,
|
||||||
|
},
|
||||||
|
RunnerRequestId: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, msg := range jobsAssigned {
|
||||||
|
batchedMessages = append(batchedMessages, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
jobsStarted := []*actions.JobStarted{
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
JobMessageType: actions.JobMessageType{
|
||||||
|
MessageType: messageTypeJobStarted,
|
||||||
|
},
|
||||||
|
RunnerRequestId: 5,
|
||||||
|
},
|
||||||
|
RunnerId: 2,
|
||||||
|
RunnerName: "runner2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, msg := range jobsStarted {
|
||||||
|
batchedMessages = append(batchedMessages, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
jobsCompleted := []*actions.JobCompleted{
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
JobMessageType: actions.JobMessageType{
|
||||||
|
MessageType: messageTypeJobCompleted,
|
||||||
|
},
|
||||||
|
RunnerRequestId: 6,
|
||||||
|
},
|
||||||
|
Result: "success",
|
||||||
|
RunnerId: 1,
|
||||||
|
RunnerName: "runner1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, msg := range jobsCompleted {
|
||||||
|
batchedMessages = append(batchedMessages, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(batchedMessages)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
msg.Body = string(b)
|
||||||
|
|
||||||
|
l := &Listener{}
|
||||||
|
parsedMsg, err := l.parseMessage(context.Background(), msg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, msg.Statistics, parsedMsg.statistics)
|
||||||
|
assert.Equal(t, jobsAvailable, parsedMsg.jobsAvailable)
|
||||||
|
assert.Equal(t, jobsStarted, parsedMsg.jobsStarted)
|
||||||
|
assert.Equal(t, jobsCompleted, parsedMsg.jobsCompleted)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
205
cmd/ghalistener/listener/metrics_test.go
Normal file
205
cmd/ghalistener/listener/metrics_test.go
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
package listener
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
listenermocks "github.com/actions/actions-runner-controller/cmd/ghalistener/listener/mocks"
|
||||||
|
metricsmocks "github.com/actions/actions-runner-controller/cmd/ghalistener/metrics/mocks"
|
||||||
|
"github.com/actions/actions-runner-controller/github/actions"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInitialMetrics(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("SetStaticMetrics", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
metrics := metricsmocks.NewPublisher(t)
|
||||||
|
|
||||||
|
minRunners := 5
|
||||||
|
maxRunners := 10
|
||||||
|
metrics.On("PublishStatic", minRunners, maxRunners).Once()
|
||||||
|
|
||||||
|
config := Config{
|
||||||
|
Client: listenermocks.NewClient(t),
|
||||||
|
ScaleSetID: 1,
|
||||||
|
Metrics: metrics,
|
||||||
|
MinRunners: minRunners,
|
||||||
|
MaxRunners: maxRunners,
|
||||||
|
}
|
||||||
|
l, err := New(config)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, l)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InitialMessageStatistics", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
sessionStatistics := &actions.RunnerScaleSetStatistic{
|
||||||
|
TotalAvailableJobs: 1,
|
||||||
|
TotalAcquiredJobs: 2,
|
||||||
|
TotalAssignedJobs: 3,
|
||||||
|
TotalRunningJobs: 4,
|
||||||
|
TotalRegisteredRunners: 5,
|
||||||
|
TotalBusyRunners: 6,
|
||||||
|
TotalIdleRunners: 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid := uuid.New()
|
||||||
|
session := &actions.RunnerScaleSetSession{
|
||||||
|
SessionId: &uuid,
|
||||||
|
OwnerName: "example",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{},
|
||||||
|
MessageQueueUrl: "https://example.com",
|
||||||
|
MessageQueueAccessToken: "1234567890",
|
||||||
|
Statistics: sessionStatistics,
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics := metricsmocks.NewPublisher(t)
|
||||||
|
metrics.On("PublishStatic", mock.Anything, mock.Anything).Once()
|
||||||
|
metrics.On("PublishStatistics", sessionStatistics).Once()
|
||||||
|
metrics.On("PublishDesiredRunners", sessionStatistics.TotalAssignedJobs).
|
||||||
|
Run(
|
||||||
|
func(mock.Arguments) {
|
||||||
|
cancel()
|
||||||
|
},
|
||||||
|
).Once()
|
||||||
|
|
||||||
|
config := Config{
|
||||||
|
Client: listenermocks.NewClient(t),
|
||||||
|
ScaleSetID: 1,
|
||||||
|
Metrics: metrics,
|
||||||
|
}
|
||||||
|
|
||||||
|
client := listenermocks.NewClient(t)
|
||||||
|
client.On("CreateMessageSession", mock.Anything, mock.Anything, mock.Anything).Return(session, nil).Once()
|
||||||
|
client.On("DeleteMessageSession", mock.Anything, session.RunnerScaleSet.Id, session.SessionId).Return(nil).Once()
|
||||||
|
config.Client = client
|
||||||
|
|
||||||
|
handler := listenermocks.NewHandler(t)
|
||||||
|
handler.On("HandleDesiredRunnerCount", mock.Anything, sessionStatistics.TotalAssignedJobs).
|
||||||
|
Return(sessionStatistics.TotalAssignedJobs, nil).
|
||||||
|
Once()
|
||||||
|
|
||||||
|
l, err := New(config)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, l)
|
||||||
|
|
||||||
|
assert.ErrorIs(t, context.Canceled, l.Listen(ctx, handler))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleMessageMetrics(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
msg := &actions.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "RunnerScaleSetJobMessages",
|
||||||
|
Body: "",
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{
|
||||||
|
TotalAvailableJobs: 1,
|
||||||
|
TotalAcquiredJobs: 2,
|
||||||
|
TotalAssignedJobs: 3,
|
||||||
|
TotalRunningJobs: 4,
|
||||||
|
TotalRegisteredRunners: 5,
|
||||||
|
TotalBusyRunners: 6,
|
||||||
|
TotalIdleRunners: 7,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var batchedMessages []any
|
||||||
|
jobsStarted := []*actions.JobStarted{
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
JobMessageType: actions.JobMessageType{
|
||||||
|
MessageType: messageTypeJobStarted,
|
||||||
|
},
|
||||||
|
RunnerRequestId: 8,
|
||||||
|
},
|
||||||
|
RunnerId: 3,
|
||||||
|
RunnerName: "runner3",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, msg := range jobsStarted {
|
||||||
|
batchedMessages = append(batchedMessages, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
jobsCompleted := []*actions.JobCompleted{
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
JobMessageType: actions.JobMessageType{
|
||||||
|
MessageType: messageTypeJobCompleted,
|
||||||
|
},
|
||||||
|
RunnerRequestId: 6,
|
||||||
|
},
|
||||||
|
Result: "success",
|
||||||
|
RunnerId: 1,
|
||||||
|
RunnerName: "runner1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JobMessageBase: actions.JobMessageBase{
|
||||||
|
JobMessageType: actions.JobMessageType{
|
||||||
|
MessageType: messageTypeJobCompleted,
|
||||||
|
},
|
||||||
|
RunnerRequestId: 7,
|
||||||
|
},
|
||||||
|
Result: "success",
|
||||||
|
RunnerId: 2,
|
||||||
|
RunnerName: "runner2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, msg := range jobsCompleted {
|
||||||
|
batchedMessages = append(batchedMessages, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(batchedMessages)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
msg.Body = string(b)
|
||||||
|
|
||||||
|
desiredResult := 4
|
||||||
|
|
||||||
|
metrics := metricsmocks.NewPublisher(t)
|
||||||
|
metrics.On("PublishStatic", 0, 0).Once()
|
||||||
|
metrics.On("PublishStatistics", msg.Statistics).Once()
|
||||||
|
metrics.On("PublishJobCompleted", jobsCompleted[0]).Once()
|
||||||
|
metrics.On("PublishJobCompleted", jobsCompleted[1]).Once()
|
||||||
|
metrics.On("PublishJobStarted", jobsStarted[0]).Once()
|
||||||
|
metrics.On("PublishDesiredRunners", desiredResult).Once()
|
||||||
|
|
||||||
|
handler := listenermocks.NewHandler(t)
|
||||||
|
handler.On("HandleJobStarted", mock.Anything, jobsStarted[0]).Return(nil).Once()
|
||||||
|
handler.On("HandleDesiredRunnerCount", mock.Anything, mock.Anything).Return(desiredResult, nil).Once()
|
||||||
|
|
||||||
|
client := listenermocks.NewClient(t)
|
||||||
|
client.On("DeleteMessage", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
||||||
|
|
||||||
|
config := Config{
|
||||||
|
Client: listenermocks.NewClient(t),
|
||||||
|
ScaleSetID: 1,
|
||||||
|
Metrics: metrics,
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := New(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
l.client = client
|
||||||
|
l.session = &actions.RunnerScaleSetSession{
|
||||||
|
OwnerName: "",
|
||||||
|
RunnerScaleSet: &actions.RunnerScaleSet{},
|
||||||
|
MessageQueueUrl: "",
|
||||||
|
MessageQueueAccessToken: "",
|
||||||
|
Statistics: &actions.RunnerScaleSetStatistic{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = l.handleMessage(context.Background(), handler, msg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
@@ -83,6 +83,20 @@ func (_m *Client) DeleteMessage(ctx context.Context, messageQueueUrl string, mes
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteMessageSession provides a mock function with given fields: ctx, runnerScaleSetId, sessionId
|
||||||
|
func (_m *Client) DeleteMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) error {
|
||||||
|
ret := _m.Called(ctx, runnerScaleSetId, sessionId)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int, *uuid.UUID) error); ok {
|
||||||
|
r0 = rf(ctx, runnerScaleSetId, sessionId)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// GetAcquirableJobs provides a mock function with given fields: ctx, runnerScaleSetId
|
// GetAcquirableJobs provides a mock function with given fields: ctx, runnerScaleSetId
|
||||||
func (_m *Client) GetAcquirableJobs(ctx context.Context, runnerScaleSetId int) (*actions.AcquirableJobList, error) {
|
func (_m *Client) GetAcquirableJobs(ctx context.Context, runnerScaleSetId int) (*actions.AcquirableJobList, error) {
|
||||||
ret := _m.Called(ctx, runnerScaleSetId)
|
ret := _m.Called(ctx, runnerScaleSetId)
|
||||||
|
|||||||
@@ -15,18 +15,28 @@ type Handler struct {
|
|||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleDesiredRunnerCount provides a mock function with given fields: ctx, desiredRunnerCount
|
// HandleDesiredRunnerCount provides a mock function with given fields: ctx, count
|
||||||
func (_m *Handler) HandleDesiredRunnerCount(ctx context.Context, desiredRunnerCount int) error {
|
func (_m *Handler) HandleDesiredRunnerCount(ctx context.Context, count int) (int, error) {
|
||||||
ret := _m.Called(ctx, desiredRunnerCount)
|
ret := _m.Called(ctx, count)
|
||||||
|
|
||||||
var r0 error
|
var r0 int
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, int) error); ok {
|
var r1 error
|
||||||
r0 = rf(ctx, desiredRunnerCount)
|
if rf, ok := ret.Get(0).(func(context.Context, int) (int, error)); ok {
|
||||||
|
return rf(ctx, count)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
|
||||||
|
r0 = rf(ctx, count)
|
||||||
} else {
|
} else {
|
||||||
r0 = ret.Error(0)
|
r0 = ret.Get(0).(int)
|
||||||
}
|
}
|
||||||
|
|
||||||
return r0
|
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
|
||||||
|
r1 = rf(ctx, count)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleJobStarted provides a mock function with given fields: ctx, jobInfo
|
// HandleJobStarted provides a mock function with given fields: ctx, jobInfo
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/actions/actions-runner-controller/logging"
|
"github.com/actions/actions-runner-controller/logging"
|
||||||
jsonpatch "github.com/evanphx/json-patch"
|
jsonpatch "github.com/evanphx/json-patch"
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
@@ -141,6 +142,10 @@ func (w *Worker) HandleJobStarted(ctx context.Context, jobInfo *actions.JobStart
|
|||||||
Do(ctx).
|
Do(ctx).
|
||||||
Into(patchedStatus)
|
Into(patchedStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if kerrors.IsNotFound(err) {
|
||||||
|
w.logger.Info("Ephemeral runner not found, skipping patching of ephemeral runner status", "runnerName", jobInfo.RunnerName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return fmt.Errorf("could not patch ephemeral runner status, patch JSON: %s, error: %w", string(mergePatch), err)
|
return fmt.Errorf("could not patch ephemeral runner status, patch JSON: %s, error: %w", string(mergePatch), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +161,7 @@ func (w *Worker) HandleJobStarted(ctx context.Context, jobInfo *actions.JobStart
|
|||||||
// The function then scales the ephemeral runner set by applying the merge patch.
|
// The function then scales the ephemeral runner set by applying the merge patch.
|
||||||
// Finally, it logs the scaled ephemeral runner set details and returns nil if successful.
|
// Finally, it logs the scaled ephemeral runner set details and returns nil if successful.
|
||||||
// If any error occurs during the process, it returns an error with a descriptive message.
|
// If any error occurs during the process, it returns an error with a descriptive message.
|
||||||
func (w *Worker) HandleDesiredRunnerCount(ctx context.Context, count int) error {
|
func (w *Worker) HandleDesiredRunnerCount(ctx context.Context, count int) (int, error) {
|
||||||
// Max runners should always be set by the resource builder either to the configured value,
|
// Max runners should always be set by the resource builder either to the configured value,
|
||||||
// or the maximum int32 (resourcebuilder.newAutoScalingListener()).
|
// or the maximum int32 (resourcebuilder.newAutoScalingListener()).
|
||||||
targetRunnerCount := min(w.config.MinRunners+count, w.config.MaxRunners)
|
targetRunnerCount := min(w.config.MinRunners+count, w.config.MaxRunners)
|
||||||
@@ -171,7 +176,7 @@ func (w *Worker) HandleDesiredRunnerCount(ctx context.Context, count int) error
|
|||||||
|
|
||||||
if targetRunnerCount == w.lastPatch {
|
if targetRunnerCount == w.lastPatch {
|
||||||
w.logger.Info("Skipping patching of EphemeralRunnerSet as the desired count has not changed", logValues...)
|
w.logger.Info("Skipping patching of EphemeralRunnerSet as the desired count has not changed", logValues...)
|
||||||
return nil
|
return targetRunnerCount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
original, err := json.Marshal(
|
original, err := json.Marshal(
|
||||||
@@ -182,7 +187,7 @@ func (w *Worker) HandleDesiredRunnerCount(ctx context.Context, count int) error
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal empty ephemeral runner set: %w", err)
|
return 0, fmt.Errorf("failed to marshal empty ephemeral runner set: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
patch, err := json.Marshal(
|
patch, err := json.Marshal(
|
||||||
@@ -194,12 +199,12 @@ func (w *Worker) HandleDesiredRunnerCount(ctx context.Context, count int) error
|
|||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.logger.Error(err, "could not marshal patch ephemeral runner set")
|
w.logger.Error(err, "could not marshal patch ephemeral runner set")
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mergePatch, err := jsonpatch.CreateMergePatch(original, patch)
|
mergePatch, err := jsonpatch.CreateMergePatch(original, patch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create merge patch json for ephemeral runner set: %w", err)
|
return 0, fmt.Errorf("failed to create merge patch json for ephemeral runner set: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.logger.Info("Created merge patch json for EphemeralRunnerSet update", "json", string(mergePatch))
|
w.logger.Info("Created merge patch json for EphemeralRunnerSet update", "json", string(mergePatch))
|
||||||
@@ -217,7 +222,7 @@ func (w *Worker) HandleDesiredRunnerCount(ctx context.Context, count int) error
|
|||||||
Do(ctx).
|
Do(ctx).
|
||||||
Into(patchedEphemeralRunnerSet)
|
Into(patchedEphemeralRunnerSet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not patch ephemeral runner set , patch JSON: %s, error: %w", string(mergePatch), err)
|
return 0, fmt.Errorf("could not patch ephemeral runner set , patch JSON: %s, error: %w", string(mergePatch), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.logger.Info("Ephemeral runner set scaled.",
|
w.logger.Info("Ephemeral runner set scaled.",
|
||||||
@@ -225,5 +230,5 @@ func (w *Worker) HandleDesiredRunnerCount(ctx context.Context, count int) error
|
|||||||
"name", w.config.EphemeralRunnerSetName,
|
"name", w.config.EphemeralRunnerSetName,
|
||||||
"replicas", patchedEphemeralRunnerSet.Spec.Replicas,
|
"replicas", patchedEphemeralRunnerSet.Spec.Replicas,
|
||||||
)
|
)
|
||||||
return nil
|
return targetRunnerCount, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ func (b *resourceBuilder) newScaleSetListenerPod(autoscalingListener *v1alpha1.A
|
|||||||
ports = append(ports, port)
|
ports = append(ports, port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
terminationGracePeriodSeconds := int64(60)
|
||||||
podSpec := corev1.PodSpec{
|
podSpec := corev1.PodSpec{
|
||||||
ServiceAccountName: serviceAccount.Name,
|
ServiceAccountName: serviceAccount.Name,
|
||||||
Containers: []corev1.Container{
|
Containers: []corev1.Container{
|
||||||
@@ -256,8 +257,9 @@ func (b *resourceBuilder) newScaleSetListenerPod(autoscalingListener *v1alpha1.A
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ImagePullSecrets: autoscalingListener.Spec.ImagePullSecrets,
|
ImagePullSecrets: autoscalingListener.Spec.ImagePullSecrets,
|
||||||
RestartPolicy: corev1.RestartPolicyNever,
|
RestartPolicy: corev1.RestartPolicyNever,
|
||||||
|
TerminationGracePeriodSeconds: &terminationGracePeriodSeconds,
|
||||||
}
|
}
|
||||||
|
|
||||||
labels := make(map[string]string, len(autoscalingListener.Labels))
|
labels := make(map[string]string, len(autoscalingListener.Labels))
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# About ARC
|
# About ARC
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This documentation covers the legacy mode of ARC (resources in the `actions.summerwind.net` namespace). If you're looking for documentation on the newer autoscaling runner scale sets, it is available in [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller). To understand why these resources are considered legacy (and the benefits of using the newer autoscaling runner scale sets), read [this discussion (#2775)](https://github.com/actions/actions-runner-controller/discussions/2775).
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
This document provides a high-level overview of Actions Runner Controller (ARC). ARC enables running Github Actions Runners on Kubernetes (K8s) clusters.
|
This document provides a high-level overview of Actions Runner Controller (ARC). ARC enables running Github Actions Runners on Kubernetes (K8s) clusters.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Authenticating to the GitHub API
|
# Authenticating to the GitHub API
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This documentation covers the legacy mode of ARC (resources in the `actions.summerwind.net` namespace). If you're looking for documentation on the newer autoscaling runner scale sets, it is available in [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller). To understand why these resources are considered legacy (and the benefits of using the newer autoscaling runner scale sets), read [this discussion (#2775)](https://github.com/actions/actions-runner-controller/discussions/2775).
|
||||||
|
|
||||||
## Setting Up Authentication with GitHub API
|
## Setting Up Authentication with GitHub API
|
||||||
|
|
||||||
There are two ways for actions-runner-controller to authenticate with the GitHub API (only 1 can be configured at a time however):
|
There are two ways for actions-runner-controller to authenticate with the GitHub API (only 1 can be configured at a time however):
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Automatically scaling runners
|
# Automatically scaling runners
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This documentation covers the legacy mode of ARC (resources in the `actions.summerwind.net` namespace). If you're looking for documentation on the newer autoscaling runner scale sets, it is available in [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller). To understand why these resources are considered legacy (and the benefits of using the newer autoscaling runner scale sets), read [this discussion (#2775)](https://github.com/actions/actions-runner-controller/discussions/2775).
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
> If you are using controller version < [v0.22.0](https://github.com/actions/actions-runner-controller/releases/tag/v0.22.0) and you are not using GHES, and so you can't set your rate limit budget, it is recommended that you use 100 replicas or fewer to prevent being rate limited.
|
> If you are using controller version < [v0.22.0](https://github.com/actions/actions-runner-controller/releases/tag/v0.22.0) and you are not using GHES, and so you can't set your rate limit budget, it is recommended that you use 100 replicas or fewer to prevent being rate limited.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Adding ARC runners to a repository, organization, or enterprise
|
# Adding ARC runners to a repository, organization, or enterprise
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This documentation covers the legacy mode of ARC (resources in the `actions.summerwind.net` namespace). If you're looking for documentation on the newer autoscaling runner scale sets, it is available in [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller). To understand why these resources are considered legacy (and the benefits of using the newer autoscaling runner scale sets), read [this discussion (#2775)](https://github.com/actions/actions-runner-controller/discussions/2775).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
[GitHub self-hosted runners can be deployed at various levels in a management hierarchy](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#about-self-hosted-runners):
|
[GitHub self-hosted runners can be deployed at various levels in a management hierarchy](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#about-self-hosted-runners):
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Configuring Windows runners
|
# Configuring Windows runners
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This documentation covers the legacy mode of ARC (resources in the `actions.summerwind.net` namespace). If you're looking for documentation on the newer autoscaling runner scale sets, it is available in [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller). To understand why these resources are considered legacy (and the benefits of using the newer autoscaling runner scale sets), read [this discussion (#2775)](https://github.com/actions/actions-runner-controller/discussions/2775).
|
||||||
|
|
||||||
## Setting up Windows Runners
|
## Setting up Windows Runners
|
||||||
|
|
||||||
The main two steps in enabling Windows self-hosted runners are:
|
The main two steps in enabling Windows self-hosted runners are:
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Deploying alternative runners
|
# Deploying alternative runners
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This documentation covers the legacy mode of ARC (resources in the `actions.summerwind.net` namespace). If you're looking for documentation on the newer autoscaling runner scale sets, it is available in [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller). To understand why these resources are considered legacy (and the benefits of using the newer autoscaling runner scale sets), read [this discussion (#2775)](https://github.com/actions/actions-runner-controller/discussions/2775).
|
||||||
|
|
||||||
## Alternative Runners
|
## Alternative Runners
|
||||||
|
|
||||||
ARC also offers a few alternative runner options
|
ARC also offers a few alternative runner options
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Deploying ARC runners
|
# Deploying ARC runners
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This documentation covers the legacy mode of ARC (resources in the `actions.summerwind.net` namespace). If you're looking for documentation on the newer autoscaling runner scale sets, it is available in [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller). To understand why these resources are considered legacy (and the benefits of using the newer autoscaling runner scale sets), read [this discussion (#2775)](https://github.com/actions/actions-runner-controller/discussions/2775).
|
||||||
|
|
||||||
## Deploying runners with RunnerDeployments
|
## Deploying runners with RunnerDeployments
|
||||||
|
|
||||||
In our previous examples we were deploying a single runner via the `RunnerDeployment` kind, the amount of runners deployed can be statically set via the `replicas:` field, we can increase this value to deploy additional sets of runners instead:
|
In our previous examples we were deploying a single runner via the `RunnerDeployment` kind, the amount of runners deployed can be statically set via the `replicas:` field, we can increase this value to deploy additional sets of runners instead:
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ You can follow [this troubleshooting guide](https://docs.github.com/en/actions/h
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### v0.8.2
|
||||||
|
1. Add listener graceful termination period and background context after the message is received [#3187](https://github.com/actions/actions-runner-controller/pull/3187)
|
||||||
|
1. Publish metrics in the new ghalistener [#3193](https://github.com/actions/actions-runner-controller/pull/3193)
|
||||||
|
1. Delete message session when listener.Listen returns [#3240](https://github.com/actions/actions-runner-controller/pull/3240)
|
||||||
|
|
||||||
|
### v0.8.1
|
||||||
|
1. Fix proxy issue in new listener client [#3181](https://github.com/actions/actions-runner-controller/pull/3181)
|
||||||
|
|
||||||
### v0.8.0
|
### v0.8.0
|
||||||
1. Change listener container name [#3167](https://github.com/actions/actions-runner-controller/pull/3167)
|
1. Change listener container name [#3167](https://github.com/actions/actions-runner-controller/pull/3167)
|
||||||
1. Fix empty env and volumeMounts object on default setup [#3166](https://github.com/actions/actions-runner-controller/pull/3166)
|
1. Fix empty env and volumeMounts object on default setup [#3166](https://github.com/actions/actions-runner-controller/pull/3166)
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Installing ARC
|
# Installing ARC
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This documentation covers the legacy mode of ARC (resources in the `actions.summerwind.net` namespace). If you're looking for documentation on the newer autoscaling runner scale sets, it is available in [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller). To understand why these resources are considered legacy (and the benefits of using the newer autoscaling runner scale sets), read [this discussion (#2775)](https://github.com/actions/actions-runner-controller/discussions/2775).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
By default, actions-runner-controller uses [cert-manager](https://cert-manager.io/docs/installation/kubernetes/) for certificate management of Admission Webhook. Make sure you have already installed cert-manager before you install. The installation instructions for the cert-manager can be found below.
|
By default, actions-runner-controller uses [cert-manager](https://cert-manager.io/docs/installation/kubernetes/) for certificate management of Admission Webhook. Make sure you have already installed cert-manager before you install. The installation instructions for the cert-manager can be found below.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Managing access with runner groups
|
# Managing access with runner groups
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This documentation covers the legacy mode of ARC (resources in the `actions.summerwind.net` namespace). If you're looking for documentation on the newer autoscaling runner scale sets, it is available in [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller). To understand why these resources are considered legacy (and the benefits of using the newer autoscaling runner scale sets), read [this discussion (#2775)](https://github.com/actions/actions-runner-controller/discussions/2775).
|
||||||
|
|
||||||
## Runner Groups
|
## Runner Groups
|
||||||
|
|
||||||
Runner groups can be used to limit which repositories are able to use the GitHub Runner at an organization level. Runner groups have to be [created in GitHub first](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/managing-access-to-self-hosted-runners-using-groups) before they can be referenced.
|
Runner groups can be used to limit which repositories are able to use the GitHub Runner at an organization level. Runner groups have to be [created in GitHub first](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/managing-access-to-self-hosted-runners-using-groups) before they can be referenced.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Monitoring and troubleshooting
|
# Monitoring and troubleshooting
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This documentation covers the legacy mode of ARC (resources in the `actions.summerwind.net` namespace). If you're looking for documentation on the newer autoscaling runner scale sets, it is available in [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller). To understand why these resources are considered legacy (and the benefits of using the newer autoscaling runner scale sets), read [this discussion (#2775)](https://github.com/actions/actions-runner-controller/discussions/2775).
|
||||||
|
|
||||||
## Metrics
|
## Metrics
|
||||||
|
|
||||||
The controller also exposes Prometheus metrics on a `/metrics` endpoint. By default this is on port `8443` behind an RBAC proxy.
|
The controller also exposes Prometheus metrics on a `/metrics` endpoint. By default this is on port `8443` behind an RBAC proxy.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Actions Runner Controller Quickstart
|
# Actions Runner Controller Quickstart
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This documentation covers the legacy mode of ARC (resources in the `actions.summerwind.net` namespace). If you're looking for documentation on the newer autoscaling runner scale sets, it is available in [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller). To understand why these resources are considered legacy (and the benefits of using the newer autoscaling runner scale sets), read [this discussion (#2775)](https://github.com/actions/actions-runner-controller/discussions/2775).
|
||||||
|
|
||||||
GitHub Actions automates the deployment of code to different environments, including production. The environments contain the `GitHub Runner` software which executes the automation. `GitHub Runner` can be run in GitHub-hosted cloud or self-hosted environments. Self-hosted environments offer more control of hardware, operating system, and software tools. They can be run on physical machines, virtual machines, or in a container. Containerized environments are lightweight, loosely coupled, highly efficient and can be managed centrally. However, they are not straightforward to use.
|
GitHub Actions automates the deployment of code to different environments, including production. The environments contain the `GitHub Runner` software which executes the automation. `GitHub Runner` can be run in GitHub-hosted cloud or self-hosted environments. Self-hosted environments offer more control of hardware, operating system, and software tools. They can be run on physical machines, virtual machines, or in a container. Containerized environments are lightweight, loosely coupled, highly efficient and can be managed centrally. However, they are not straightforward to use.
|
||||||
|
|
||||||
`Actions Runner Controller (ARC)` makes it simpler to run self hosted environments on Kubernetes(K8s) cluster.
|
`Actions Runner Controller (ARC)` makes it simpler to run self hosted environments on Kubernetes(K8s) cluster.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Using ARC across organizations
|
# Using ARC across organizations
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This documentation covers the legacy mode of ARC (resources in the `actions.summerwind.net` namespace). If you're looking for documentation on the newer autoscaling runner scale sets, it is available in [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller). To understand why these resources are considered legacy (and the benefits of using the newer autoscaling runner scale sets), read [this discussion (#2775)](https://github.com/actions/actions-runner-controller/discussions/2775).
|
||||||
|
|
||||||
## Multitenancy
|
## Multitenancy
|
||||||
|
|
||||||
> This feature requires controller version => [v0.26.0](https://github.com/actions/actions-runner-controller/releases/tag/v0.26.0)
|
> This feature requires controller version => [v0.26.0](https://github.com/actions/actions-runner-controller/releases/tag/v0.26.0)
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Using ARC runners in a workflow
|
# Using ARC runners in a workflow
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This documentation covers the legacy mode of ARC (resources in the `actions.summerwind.net` namespace). If you're looking for documentation on the newer autoscaling runner scale sets, it is available in [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller). To understand why these resources are considered legacy (and the benefits of using the newer autoscaling runner scale sets), read [this discussion (#2775)](https://github.com/actions/actions-runner-controller/discussions/2775).
|
||||||
|
|
||||||
## Runner Labels
|
## Runner Labels
|
||||||
|
|
||||||
To run a workflow job on a self-hosted runner, you can use the following syntax in your workflow:
|
To run a workflow job on a self-hosted runner, you can use the following syntax in your workflow:
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Using custom volumes
|
# Using custom volumes
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This documentation covers the legacy mode of ARC (resources in the `actions.summerwind.net` namespace). If you're looking for documentation on the newer autoscaling runner scale sets, it is available in [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller). To understand why these resources are considered legacy (and the benefits of using the newer autoscaling runner scale sets), read [this discussion (#2775)](https://github.com/actions/actions-runner-controller/discussions/2775).
|
||||||
|
|
||||||
## Custom Volume mounts
|
## Custom Volume mounts
|
||||||
|
|
||||||
You can configure your own custom volume mounts. For example to have the work/docker data in memory or on NVME SSD, for
|
You can configure your own custom volume mounts. For example to have the work/docker data in memory or on NVME SSD, for
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Using entrypoint features
|
# Using entrypoint features
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> This documentation covers the legacy mode of ARC (resources in the `actions.summerwind.net` namespace). If you're looking for documentation on the newer autoscaling runner scale sets, it is available in [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller). To understand why these resources are considered legacy (and the benefits of using the newer autoscaling runner scale sets), read [this discussion (#2775)](https://github.com/actions/actions-runner-controller/discussions/2775).
|
||||||
|
|
||||||
## Runner Entrypoint Features
|
## Runner Entrypoint Features
|
||||||
|
|
||||||
> Environment variable values must all be strings
|
> Environment variable values must all be strings
|
||||||
|
|||||||
@@ -640,8 +640,11 @@ func (c *Client) doSessionRequest(ctx context.Context, method, path string, requ
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode == expectedResponseStatusCode && responseUnmarshalTarget != nil {
|
if resp.StatusCode == expectedResponseStatusCode {
|
||||||
return json.NewDecoder(resp.Body).Decode(responseUnmarshalTarget)
|
if responseUnmarshalTarget != nil {
|
||||||
|
return json.NewDecoder(resp.Body).Decode(responseUnmarshalTarget)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
|
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
|
||||||
|
|||||||
34
github/actions/testdata/rootCA.crt
vendored
34
github/actions/testdata/rootCA.crt
vendored
@@ -1,18 +1,20 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIC6jCCAdICCQCoZFduxPa/eDANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJV
|
MIIDVTCCAj2gAwIBAgIUOo9VGKll71GYjunZhdMQhS5rP+gwDQYJKoZIhvcNAQEL
|
||||||
UzEnMCUGA1UEAwweYWN0aW9ucy1ydW5uZXItY29udHJvbGxlci10ZXN0MCAXDTIz
|
BQAwOTESMBAGA1UEAwwJbG9jYWxob3N0MQswCQYDVQQGEwJVUzEWMBQGA1UEBwwN
|
||||||
MDExOTE2NTAwMVoYDzIwNTAwNjA1MTY1MDAxWjA2MQswCQYDVQQGEwJVUzEnMCUG
|
U2FuIEZyYW5zaXNjbzAgFw0yNDAxMjIxMjUyNTdaGA8yMDUxMDYwODEyNTI1N1ow
|
||||||
A1UEAwweYWN0aW9ucy1ydW5uZXItY29udHJvbGxlci10ZXN0MIIBIjANBgkqhkiG
|
OTESMBAGA1UEAwwJbG9jYWxob3N0MQswCQYDVQQGEwJVUzEWMBQGA1UEBwwNU2Fu
|
||||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAykHCU0I/pdzhnQBwr2N+7so66LPq0cxc8JJL
|
IEZyYW5zaXNjbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALmyQRuC
|
||||||
S2mmk7gg+NWhTZzoci6aYXNRKCyH6B2Wmy7Qveku2wqT2+/4JBMYgTWH5bF7yt76
|
S13Iat5jMun5zg8tn4E3RZ4x5KWPvRiR9RRX4zo5f/ytmnFVGkSnDhXJkuHRzwWl
|
||||||
LB+x9YruSgH/pBN2WI4vRU87NOAU8F0o0U/Lp5vAJoRo+ePPvcHu0OY1WF+QnEX+
|
KjtdW23uUaBfNbJR55O0qUnZWAMNKO1Afm68Tfg+91a5X+KpwGiHfIGZs7UCERYg
|
||||||
xtp6gJFGf5DT4U9upwEgQjKgvKFEoB5KNeH1qr2fS2yA2vhm6Uhm+1i/KUQUZ49K
|
6O2iqHQMLCOL/Ytpd6NBF+QFK9klRbfncBJmCR6FEpw1/bGr7HwlldfkPkpHNWUG
|
||||||
GvFK8TQQT4HXft8rPLP5M9OitdqVU8SX0dQoXZ4M41/qydycHOvApj0LlH/XsicZ
|
cIqytYBvzo2T2cUyrTysKtATcRg/4Fp0DAZocYfzT6/gL2yWhLwnmxqU7Gbxvrd2
|
||||||
x0mkF90hD+9VRqeYFe562NI4NHR7FGP7HKPWibNjXKC2w+z+aQIDAQABMA0GCSqG
|
6ejFitgxwoM/3rKWuXds7tFMeiKUu2RovGkvDkMEieJWwTufPBJjkIklW5S4iMMi
|
||||||
SIb3DQEBCwUAA4IBAQBxaOCnmakd1PPp+pH40OjUktKG1nqM2tGqP0o3Bk7huB2y
|
hJnDIn+Ag1nbVHcCAwEAAaNTMFEwHQYDVR0OBBYEFK33e+IWho6FKn4GaxRb2cmv
|
||||||
jXIDi9ETuTeqqHONwwgsKOVY3J+Zt5R+teBSC0qUnypODzu+9v8Xa4Is9G9GyT5S
|
mmxjMB8GA1UdIwQYMBaAFK33e+IWho6FKn4GaxRb2cmvmmxjMA8GA1UdEwEB/wQF
|
||||||
erjpPcJjQnvZyMHLH9DGGWE9UCyqKIqmaEc9bwr2oz1+a0rsaS3ZdIFlQibBHij5
|
MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHZ/Z3CSrPoWb02+iu1cUN8nlQBtAsxI
|
||||||
tdJcnzXfN4T4GIbYXKMCOYDy/5CiNJ26l/pQNpO9JCzsEmngw0ooS0Bi8EcTCgB6
|
oR3nqhUSEA/9oyyXJt8NIIXauACyYzmNXG87aKQZvVzUEQM0aK4MBq+Pg0Zdnvns
|
||||||
dsHl0w8va3l1kvxWWIlNTGwrAEpRbXmL01hAqx2yCiaFPVZ/eRNWmBWO4LpW4ouK
|
8QtBvdro7jInHhfn4uS8X21Fa1gYZ0d0C6UHIXUeD9KSEOAX1JT+3VP/7FNIDzns
|
||||||
YOaA+X7geM6XVFlZE3cP58AxYKWHGAThxkZbD5cu
|
2ddSxzcji3eVFkDR4/1vRMTng/kiP5vFz1St1op2EYDT+v6PVr9ew3NWUf/w7fgP
|
||||||
|
sRRyx3qi7m8SRHc7FwDLk+6/zc1/14YIiX9PrvVmnJj0yULSHiBu4cQccKE2ibos
|
||||||
|
ZeUPfZL8Kl+hs/MtXG/XlYBbApm69eo7EEGHAS/2DIq2yPgsQrGMYkA=
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|||||||
41
github/actions/testdata/server.crt
vendored
41
github/actions/testdata/server.crt
vendored
@@ -1,22 +1,23 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIDnTCCAoWgAwIBAgIJAJskDVhiEY6fMA0GCSqGSIb3DQEBCwUAMDYxCzAJBgNV
|
MIIDyDCCArCgAwIBAgIUKCU/uCdz/9EcfzL6wd7ubSPrsxIwDQYJKoZIhvcNAQEL
|
||||||
BAYTAlVTMScwJQYDVQQDDB5hY3Rpb25zLXJ1bm5lci1jb250cm9sbGVyLXRlc3Qw
|
BQAwOTESMBAGA1UEAwwJbG9jYWxob3N0MQswCQYDVQQGEwJVUzEWMBQGA1UEBwwN
|
||||||
HhcNMjMwMTE5MTY1MTE0WhcNMjQwMTE5MTY1MTE0WjBaMQswCQYDVQQGEwJVUzEi
|
U2FuIEZyYW5zaXNjbzAgFw0yNDAxMjIxMjU0MTRaGA8yMDUxMDYwODEyNTQxNFow
|
||||||
MCAGA1UECgwZYWN0aW9ucy1ydW5uZXItY29udHJvbGxlcjEnMCUGA1UECwweYWN0
|
gYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1T
|
||||||
aW9ucy1ydW5uZXItY29udHJvbGxlciB0ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOC
|
YW4gRnJhbnNpc2NvMRMwEQYDVQQKDApHaXRIdWJUZXN0MSMwIQYDVQQLDBpHaXRI
|
||||||
AQ8AMIIBCgKCAQEAzOTt1/VjuaHzn+b7jLeufW3rxLHFKQV+LiUiT389rbFGY+DN
|
dWJUZXN0IEFjdGlvbnMgUnVudGltZTESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjAN
|
||||||
CC+Nzx+DbFBpKcX/scseVhFzlXlrESWWZ4h7LGMXRsTDKs91F1RMuFCd8eIEwbuV
|
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArVQ7yHHAxehcsOW8NNEplrEF/48n
|
||||||
civR44IqT5r/0hlMOWemd3Fh/c8KF+9dWQ0q0T3tvlVzEbWNRTVAXTT4JzizqNd1
|
9+XCc4ZWu0LdPdKAjcwMSAddHvLZVp5OUNRTUKgwWfL5DyGFnAhSZ31Ag3FHyoOB
|
||||||
1hhnuV/KjhiptPC/8jQ4D9ocZKM8a1pM9O2z3bnmH7VTQJkhjxE7gefQTPQRmvKk
|
C5BQSBEd+xsO1Gflt8Pm0A7TN2jzlVx7rq1j7kZ25AZY9oJ6ipK4Hf4mYbfSR5cl
|
||||||
C7uqvfk2NHTTnKiLfkE10JhLTa0VND2aofNWCybGTyHNNCNlepakoP3KyFC2LjPR
|
M2WKBPGk9JbYmI7l0t3IYLm954xxfNtPxr1tEAwk75UAKNWXBwqkR31+madOaFsU
|
||||||
oR5iwSnCRDu1z8tDWW+rIa3pfxdQ8LnH4J4CDwIDAQABo4GJMIGGMFAGA1UdIwRJ
|
9LJT4aeFJoFs+95tQzvAymGwlE+w6aWiz0WecLSzf8ZgXcRqmQkh1EcP6/2cu5MA
|
||||||
MEehOqQ4MDYxCzAJBgNVBAYTAlVTMScwJQYDVQQDDB5hY3Rpb25zLXJ1bm5lci1j
|
CMRJcNly421DYUEbofgoZ8OetkqtFcYk+RyjUBhkQWi8AAQLKJ4q7VZKqwIDAQAB
|
||||||
b250cm9sbGVyLXRlc3SCCQCoZFduxPa/eDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE
|
o3YwdDAfBgNVHSMEGDAWgBSt93viFoaOhSp+BmsUW9nJr5psYzAJBgNVHRMEAjAA
|
||||||
8DAaBgNVHREEEzARhwR/AAABgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEB
|
MAsGA1UdDwQEAwIE8DAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwHQYDVR0O
|
||||||
ALdl0ytjellmhtjbXkUZKAl/R2ZXMAVxIOtb4qiN6OOwOMK4p2Wt26p34bQa2JD0
|
BBYEFM4ELRkBcflqUtQ/GQK86CjBqjTUMA0GCSqGSIb3DQEBCwUAA4IBAQCMkiid
|
||||||
t0qvesI7spQzQObNMdT6NZJl8Ul0ABuzti/Esvmby+VfsFPasCQVXx+jqGhERqXc
|
7v2jsSWc8nGOM4Z6vEJ912mKpyyfpWSpM8SxCCxzUrbMrpFx8LB4rmeziy6hNEA0
|
||||||
SeZFIVWVACyfAc1dkqfGwehSrY62eBlY2PJ1JezagW6aLAnV6Si+96++mkALJDdX
|
yv+h9qiu9l/vVzVc3Q9HA3linEPXqnlUEXd7PV/G/IFoYKFrXi/H+zda9G0Nqt1A
|
||||||
MZhhSqjxM+Nnmhpy4My6oHVrdYWHcuVhzlEmNaMtmJCYuihIyD2Usn32xJK1k89d
|
oOKM3t9fsff8KDaRQ2sdSUEjqtAlfg6bbBwO66CICXLU+VUH7hOVghT23UJVvwNY
|
||||||
WgEOPCk+ZDAligPlGZS201fsznJk5uIjmxPjjFlJLXotBs8H7j0cQ2JkV5YHsHCk
|
Dvkha9TYR+aawRypLoTfT5ZtLp/0A9P+liqo6F5Xm0M89bYLXNPl1fPzY3Ihi5Jd
|
||||||
EYf5EJ0ZKtZbwRFeRC1Ajxg=
|
b6/mttpY9gxTfbw67m2Epfmt1NdOHkY7ac/Hr6pt/YyMBrPz9Z3eZxIXUIVDo/Nh
|
||||||
|
4O2g9RoFFN4m3A+d
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
|||||||
55
github/actions/testdata/server.key
vendored
55
github/actions/testdata/server.key
vendored
@@ -1,27 +1,28 @@
|
|||||||
-----BEGIN RSA PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
MIIEowIBAAKCAQEAzOTt1/VjuaHzn+b7jLeufW3rxLHFKQV+LiUiT389rbFGY+DN
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCtVDvIccDF6Fyw
|
||||||
CC+Nzx+DbFBpKcX/scseVhFzlXlrESWWZ4h7LGMXRsTDKs91F1RMuFCd8eIEwbuV
|
5bw00SmWsQX/jyf35cJzhla7Qt090oCNzAxIB10e8tlWnk5Q1FNQqDBZ8vkPIYWc
|
||||||
civR44IqT5r/0hlMOWemd3Fh/c8KF+9dWQ0q0T3tvlVzEbWNRTVAXTT4JzizqNd1
|
CFJnfUCDcUfKg4ELkFBIER37Gw7UZ+W3w+bQDtM3aPOVXHuurWPuRnbkBlj2gnqK
|
||||||
1hhnuV/KjhiptPC/8jQ4D9ocZKM8a1pM9O2z3bnmH7VTQJkhjxE7gefQTPQRmvKk
|
krgd/iZht9JHlyUzZYoE8aT0ltiYjuXS3chgub3njHF820/GvW0QDCTvlQAo1ZcH
|
||||||
C7uqvfk2NHTTnKiLfkE10JhLTa0VND2aofNWCybGTyHNNCNlepakoP3KyFC2LjPR
|
CqRHfX6Zp05oWxT0slPhp4UmgWz73m1DO8DKYbCUT7DppaLPRZ5wtLN/xmBdxGqZ
|
||||||
oR5iwSnCRDu1z8tDWW+rIa3pfxdQ8LnH4J4CDwIDAQABAoIBAC5rr3c+IVntV0Tj
|
CSHURw/r/Zy7kwAIxElw2XLjbUNhQRuh+Chnw562Sq0VxiT5HKNQGGRBaLwABAso
|
||||||
EBrRgrboMIJfxEuG8w+BWkSoj1DK2SfHxqwUGgzTFvNzRGAye7vMSRM24Pj8iUVZ
|
nirtVkqrAgMBAAECggEAR+/t4ANWPs1xqvmuYz1sRV6zXp3LuNdjHQ9kb9QQftgf
|
||||||
Pro2MbHcwWlHKvCID/85GiioGyCyFGHQHgu/4c2pr+xZMZxoHtzintRw28KlJaRG
|
ArrtXfewbmfcTFbnqiR1b8ReTPbK57zB90B88vbJD8S0RxjNNj9vEnoIN2/Dd+Sn
|
||||||
lt+WHB1L6pE0yt04RMlpRyvW1GIODtEh1wya61Aa2xZMJxgbNWv89znLI2f3ForY
|
Mt3brf55K0Yj0pnPu2+7Sel07q6zvZvpwBmk0M3qoCPq4kuY5Pv/jI2+KMVyn94A
|
||||||
QR/he8hQtfJQeH+mv2SvJ1bopkJ58ZObKapuJAWCSxzVRj/yol1MqfUDBy4NrJfY
|
Dc3J6xdKqLNsw7nhUDELHn8DrKQgqucTzi4goJo8Lwc9I8lanTfmbiXj1wYo3nhr
|
||||||
F5UP0BSmnED1EdIXeC0duo5RyiSfHqqJlcKR+zlepOb4pr4I1H8P6AIJ9iiunxUJ
|
5DgVcPUceZnsrDNnfkwOaaXKAGUCTi3PWieKq6Cm22oh53s1WS5NJDuk/1NvvfV+
|
||||||
h9i+YAECgYEA7JgrH5sjStcHdwUgEPN4E1MI3WRwDnnuVpzeSUH7tRqK4lsdIKmF
|
+6dyhfmW/jkHHMelox91n1qmLMYnq+GhoK6szapqAQKBgQDLRWZH17zdTNALQzks
|
||||||
u/ss3TMwTgC8XR4JJbVp+6ea54zpVjtBGwShMSvn2+e7OHHg1YoVqBIgRpL+/C4m
|
RbZU9abe+UQV1O5ywdL+4F444IPY2f3gxhEWyL+xAF66ZG0+NA/EO9n7FPqAbgyA
|
||||||
wfon2EglQ0IjscUtKuAR/UyhU6vZtkYRUKeXRKisW4yoobdob0Y4lakCgYEA3bMl
|
Atz0LT7W6o9/AveqBSNs73zxGo7OYlBDq81nCgMzU11nvfTmydJhaMC+6Zyh0Bbc
|
||||||
BfszC5c0RXI5vsBUBvr9gXMY/8tacM7H8i3vT7JqoYJG6GGST0/HeK+La3w2zryx
|
vzIbygpDOL7tg4AyyEcLUNA7BwKBgQDaSnmwMCEdcTENwzVd1mOZdnXRTBPz0u0t
|
||||||
Q8IL6uLy/HZvTHZ+BSp4KzwwgDUIk0jm/JcvzD2ZhJHoAo4aQTc6QI2ZNgjGVwCb
|
aCK5voL99L0+8HyKjtUBtWbBgUxCz7/+mfoNCU+QUHCJksm9vN1m5Zq4r0aEHE36
|
||||||
nJ0Niaxc4CdSUEAUHH1bCXk/e2stcnieFuiiPPcCgYAIxrA60OdjPEyzloYU+uMG
|
7lYAAeWnltg+OHWqGcSHRZ/zHHs8c/azemvRaTZnZ++meVkfd07jsd+yIYt/G3La
|
||||||
XHskszgQ4Wb84X7BWug6VIy4Tsbq0j76tRt57Q8qpY5XKekO9AbFZfcyBaEWKMaG
|
KV9t86V2PQKBgEfNdfm+vVo2ve6cil+XKHcOZymwR1qm4qvqx4t82guhUzGQn1t8
|
||||||
eQp9p3JHTvY75sV/Rkr9XAbEd2lr805OvbfCpxJyxz5JttWxFHS2X6RQVTyTLVAx
|
26B+vSfbB5szylsErOUWd0N3/5zKQuQdHsuqB96G8LVe6PlH42GhnzLTvMoudEfT
|
||||||
HLZYvqT+FF6g+QuvrPwmWQKBgAQspVvReQqU1EUie3feAzcGbtOLKUNXvuI04orq
|
MjVJliPVONNiiFXVyNjb1eoaP1fxV4IWj669Sa7BJsBjiS9nC6F1pHiVAoGBALBT
|
||||||
1oC3qU5VN6SUgb7Aj87z7zoc4qNN5kCSXMsVbuHWEQ5thL3wKMcXoQoo9Xpgewjy
|
fFxPZFBuAFvHlTIJXUa3I5A+zdckSCVnerVjKFiO+tb+VvttSK4qo6gnEzzcp4+3
|
||||||
h9Herw9R9/5kUpY7xfsFL4dW7vUga82tH14iQrVtyBz+t+I5cgdhoxJd2EM5hjCE
|
PP6OyNAfyee2xHMZPhZB3WrVWjaYznylTJ6Q6bsn4+DOpm0Sh2dlXEB6fylj2qE7
|
||||||
PNnNAoGBALPjmvEZ1HJdCOxY/AisziVtOFc6Glk/KhpSIT7WE1me4qLQFmrsHIDQ
|
gCAVxrZchH6Kgu0h6H2QTsuKwS2ZNHr49HbSWpNZAoGBAMrEMiyKYWKgiejs69pj
|
||||||
kZ8Sb1f3PQ4T4vHGrtl8qh144MJPI1Nb8klzdlD1xeypGpgXoQb5fsC17g1fgczp
|
idKifoCDI+Hu1WD/eViUm2OuOfdW9fIBHoeuKmOBKGYIqx5yEbFhXoJmTtJ1aSa1
|
||||||
TGzq3pvnlGnrgVmnfrWQCHXDLzXtLqM/Pu84guPFftJQ+++yy0np
|
+N+0NBzv9+1W5EII0voELevxLvjeaejcUgLNabGIj1xIcPzaEKTS+Vv2Hn6nffWR
|
||||||
-----END RSA PRIVATE KEY-----
|
yKlIixoSTJ+oJShyT9DZyZAd
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ DIND_ROOTLESS_RUNNER_NAME ?= ${DOCKER_USER}/actions-runner-dind-rootless
|
|||||||
OS_IMAGE ?= ubuntu-22.04
|
OS_IMAGE ?= ubuntu-22.04
|
||||||
TARGETPLATFORM ?= $(shell arch)
|
TARGETPLATFORM ?= $(shell arch)
|
||||||
|
|
||||||
RUNNER_VERSION ?= 2.311.0
|
RUNNER_VERSION ?= 2.312.0
|
||||||
RUNNER_CONTAINER_HOOKS_VERSION ?= 0.5.0
|
RUNNER_CONTAINER_HOOKS_VERSION ?= 0.5.0
|
||||||
DOCKER_VERSION ?= 24.0.7
|
DOCKER_VERSION ?= 24.0.7
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
RUNNER_VERSION=2.311.0
|
RUNNER_VERSION=2.312.0
|
||||||
RUNNER_CONTAINER_HOOKS_VERSION=0.5.0
|
RUNNER_CONTAINER_HOOKS_VERSION=0.5.0
|
||||||
@@ -36,7 +36,7 @@ var (
|
|||||||
|
|
||||||
testResultCMNamePrefix = "test-result-"
|
testResultCMNamePrefix = "test-result-"
|
||||||
|
|
||||||
RunnerVersion = "2.311.0"
|
RunnerVersion = "2.312.0"
|
||||||
RunnerContainerHooksVersion = "0.5.0"
|
RunnerContainerHooksVersion = "0.5.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user