Introduce new preview auto-scaling mode for ARC. (#2153)

Co-authored-by: Cory Miller <[email protected]>
Co-authored-by: Nikola Jokic <[email protected]>
Co-authored-by: Ava Stancu <[email protected]>
Co-authored-by: Ferenc Hammerl <[email protected]>
Co-authored-by: Francesco Renzi <[email protected]>
Co-authored-by: Bassem Dghaidi <[email protected]>
This commit is contained in:
Tingluo Huang
2023-01-17 12:06:20 -05:00
committed by GitHub
co-authored by Cory Miller Nikola Jokic Ava Stancu Ferenc Hammerl Francesco Renzi Bassem Dghaidi
parent 619667fc3b
commit 622eaa34f8
75 changed files with 26094 additions and 354 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,75 @@
package actions_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-retryablehttp"
"github.com/stretchr/testify/assert"
)
func TestGenerateJitRunnerConfig(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
t.Run("Get JIT Config for Runner", func(t *testing.T) {
name := "Get JIT Config for Runner"
want := &actions.RunnerScaleSetJitRunnerConfig{}
response := []byte(`{"count":1,"value":[{"id":1,"name":"scale-set-name"}]}`)
runnerSettings := &actions.RunnerScaleSetJitRunnerSetting{}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write(response)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
got, err := actionsClient.GenerateJitRunnerConfig(context.Background(), runnerSettings, 1)
if err != nil {
t.Fatalf("GenerateJitRunnerConfig got unexepected error, %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("GenerateJitRunnerConfig(%v) mismatch (-want +got):\n%s", name, diff)
}
})
t.Run("Default retries on server error", func(t *testing.T) {
runnerSettings := &actions.RunnerScaleSetJitRunnerSetting{}
retryClient := retryablehttp.NewClient()
retryClient.RetryWaitMax = 1 * time.Millisecond
retryClient.RetryMax = 1
actualRetry := 0
expectedRetry := retryClient.RetryMax + 1
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
httpClient := retryClient.StandardClient()
actionsClient := actions.Client{
Client: httpClient,
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, _ = actionsClient.GenerateJitRunnerConfig(context.Background(), runnerSettings, 1)
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
})
}
@@ -0,0 +1,144 @@
package actions_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-retryablehttp"
"github.com/stretchr/testify/assert"
)
func TestAcquireJobs(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
t.Run("Acquire Job", func(t *testing.T) {
name := "Acquire Job"
want := []int64{1}
response := []byte(`{"value": [1]}`)
session := &actions.RunnerScaleSetSession{
RunnerScaleSet: &actions.RunnerScaleSet{Id: 1},
MessageQueueAccessToken: "abc",
}
requestIDs := want
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write(response)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
}
got, err := actionsClient.AcquireJobs(context.Background(), session.RunnerScaleSet.Id, session.MessageQueueAccessToken, requestIDs)
if err != nil {
t.Fatalf("CreateRunnerScaleSet got unexepected error, %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("GetRunnerScaleSet(%v) mismatch (-want +got):\n%s", name, diff)
}
})
t.Run("Default retries on server error", func(t *testing.T) {
session := &actions.RunnerScaleSetSession{
RunnerScaleSet: &actions.RunnerScaleSet{Id: 1},
MessageQueueAccessToken: "abc",
}
var requestIDs []int64 = []int64{1}
retryClient := retryablehttp.NewClient()
retryClient.RetryWaitMax = 1 * time.Millisecond
retryClient.RetryMax = 1
actualRetry := 0
expectedRetry := retryClient.RetryMax + 1
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
httpClient := retryClient.StandardClient()
actionsClient := actions.Client{
Client: httpClient,
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
}
_, _ = actionsClient.AcquireJobs(context.Background(), session.RunnerScaleSet.Id, session.MessageQueueAccessToken, requestIDs)
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
})
}
func TestGetAcquirableJobs(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
t.Run("Acquire Job", func(t *testing.T) {
name := "Acquire Job"
want := &actions.AcquirableJobList{}
response := []byte(`{"count": 0}`)
runnerScaleSet := &actions.RunnerScaleSet{Id: 1}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write(response)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
got, err := actionsClient.GetAcquirableJobs(context.Background(), runnerScaleSet.Id)
if err != nil {
t.Fatalf("GetAcquirableJobs got unexepected error, %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("GetAcquirableJobs(%v) mismatch (-want +got):\n%s", name, diff)
}
})
t.Run("Default retries on server error", func(t *testing.T) {
runnerScaleSet := &actions.RunnerScaleSet{Id: 1}
retryClient := retryablehttp.NewClient()
retryClient.RetryWaitMax = 1 * time.Millisecond
retryClient.RetryMax = 1
actualRetry := 0
expectedRetry := retryClient.RetryMax + 1
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
httpClient := retryClient.StandardClient()
actionsClient := actions.Client{
Client: httpClient,
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, _ = actionsClient.GetAcquirableJobs(context.Background(), runnerScaleSet.Id)
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
})
}
@@ -0,0 +1,269 @@
package actions_test
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-retryablehttp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetMessage(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
runnerScaleSetMessage := &actions.RunnerScaleSetMessage{
MessageId: 1,
MessageType: "rssType",
}
t.Run("Get Runner Scale Set Message", func(t *testing.T) {
want := runnerScaleSetMessage
response := []byte(`{"messageId":1,"messageType":"rssType"}`)
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write(response)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
got, err := actionsClient.GetMessage(context.Background(), s.URL, token, 0)
if err != nil {
t.Fatalf("GetMessage got unexepected error, %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("GetMessage mismatch (-want +got):\n%s", diff)
}
})
t.Run("Default retries on server error", func(t *testing.T) {
retryClient := retryablehttp.NewClient()
retryClient.RetryWaitMax = 1 * time.Nanosecond
retryClient.RetryMax = 1
actualRetry := 0
expectedRetry := retryClient.RetryMax + 1
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
httpClient := retryClient.StandardClient()
actionsClient := actions.Client{
Client: httpClient,
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, _ = actionsClient.GetMessage(context.Background(), s.URL, token, 0)
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
})
t.Run("Custom retries on server error", func(t *testing.T) {
actualRetry := 0
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
retryMax := 1
retryWaitMax := 1 * time.Nanosecond
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
RetryMax: &retryMax,
RetryWaitMax: &retryWaitMax,
}
_, _ = actionsClient.GetMessage(context.Background(), s.URL, token, 0)
expectedRetry := retryMax + 1
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
},
)
t.Run("Message token expired", func(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, err := actionsClient.GetMessage(context.Background(), s.URL, token, 0)
if err == nil {
t.Fatalf("GetMessage did not get exepected error, ")
}
var expectedErr *actions.MessageQueueTokenExpiredError
require.True(t, errors.As(err, &expectedErr))
},
)
t.Run("Status code not found", func(t *testing.T) {
want := actions.ActionsError{
Message: "Request returned status: 404 Not Found",
StatusCode: 404,
}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, err := actionsClient.GetMessage(context.Background(), s.URL, token, 0)
if err == nil {
t.Fatalf("GetMessage did not get exepected error, ")
}
if diff := cmp.Diff(want.Error(), err.Error()); diff != "" {
t.Errorf("GetMessage mismatch (-want +got):\n%s", diff)
}
},
)
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "text/plain")
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, err := actionsClient.GetMessage(context.Background(), s.URL, token, 0)
if err == nil {
t.Fatalf("GetMessage did not get exepected error,")
}
},
)
}
func TestDeleteMessage(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
runnerScaleSetMessage := &actions.RunnerScaleSetMessage{
MessageId: 1,
MessageType: "rssType",
}
t.Run("Delete existing message", func(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
err := actionsClient.DeleteMessage(context.Background(), s.URL, token, runnerScaleSetMessage.MessageId)
if err != nil {
t.Fatalf("DeleteMessage got unexepected error, %v", err)
}
},
)
t.Run("Message token expired", func(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
err := actionsClient.DeleteMessage(context.Background(), s.URL, token, 0)
if err == nil {
t.Fatalf("DeleteMessage did not get exepected error, ")
}
var expectedErr *actions.MessageQueueTokenExpiredError
require.True(t, errors.As(err, &expectedErr))
},
)
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "text/plain")
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
err := actionsClient.DeleteMessage(context.Background(), s.URL, token, runnerScaleSetMessage.MessageId)
if err == nil {
t.Fatalf("DeleteMessage did not get exepected error")
}
var expectedErr *actions.ActionsError
require.True(t, errors.As(err, &expectedErr))
},
)
t.Run("Default retries on server error", func(t *testing.T) {
actualRetry := 0
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
retryClient := retryablehttp.NewClient()
retryMax := 1
retryClient.RetryWaitMax = time.Nanosecond
retryClient.RetryMax = retryMax
httpClient := retryClient.StandardClient()
actionsClient := actions.Client{
Client: httpClient,
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_ = actionsClient.DeleteMessage(context.Background(), s.URL, token, runnerScaleSetMessage.MessageId)
expectedRetry := retryMax + 1
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
},
)
t.Run("No message found", func(t *testing.T) {
want := (*actions.RunnerScaleSetMessage)(nil)
rsl, err := json.Marshal(want)
if err != nil {
t.Fatalf("%v", err)
}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write(rsl)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
err = actionsClient.DeleteMessage(context.Background(), s.URL, token, runnerScaleSetMessage.MessageId+1)
var expectedErr *actions.ActionsError
require.True(t, errors.As(err, &expectedErr))
},
)
}
@@ -0,0 +1,244 @@
package actions_test
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestCreateMessageSession(t *testing.T) {
t.Run("CreateMessageSession unmarshals correctly", func(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
owner := "foo"
runnerScaleSet := actions.RunnerScaleSet{
Id: 1,
Name: "ScaleSet",
CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
RunnerSetting: actions.RunnerSetting{},
}
want := &actions.RunnerScaleSetSession{
OwnerName: "foo",
RunnerScaleSet: &actions.RunnerScaleSet{
Id: 1,
Name: "ScaleSet",
},
MessageQueueUrl: "http://fake.actions.github.com/123",
MessageQueueAccessToken: "fake.jwt.here",
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
resp := []byte(`{
"ownerName": "foo",
"runnerScaleSet": {
"id": 1,
"name": "ScaleSet"
},
"messageQueueUrl": "http://fake.actions.github.com/123",
"messageQueueAccessToken": "fake.jwt.here"
}`)
w.Write(resp)
}))
defer srv.Close()
retryMax := 1
retryWaitMax := 1 * time.Microsecond
actionsClient := actions.Client{
ActionsServiceURL: &srv.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
RetryMax: &retryMax,
RetryWaitMax: &retryWaitMax,
}
got, err := actionsClient.CreateMessageSession(context.Background(), runnerScaleSet.Id, owner)
if err != nil {
t.Fatalf("CreateMessageSession got unexpected error: %v", err)
}
if diff := cmp.Diff(got, want); diff != "" {
t.Fatalf("CreateMessageSession got unexpected diff: -want +got: %v", diff)
}
})
t.Run("CreateMessageSession unmarshals errors into ActionsError", func(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
owner := "foo"
runnerScaleSet := actions.RunnerScaleSet{
Id: 1,
Name: "ScaleSet",
CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
RunnerSetting: actions.RunnerSetting{},
}
want := &actions.ActionsError{
ExceptionName: "CSharpExceptionNameHere",
Message: "could not do something",
StatusCode: http.StatusBadRequest,
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
resp := []byte(`{"typeName": "CSharpExceptionNameHere","message": "could not do something"}`)
w.Write(resp)
}))
defer srv.Close()
retryMax := 1
retryWaitMax := 1 * time.Microsecond
actionsClient := actions.Client{
ActionsServiceURL: &srv.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
RetryMax: &retryMax,
RetryWaitMax: &retryWaitMax,
}
got, err := actionsClient.CreateMessageSession(context.Background(), runnerScaleSet.Id, owner)
if err == nil {
t.Fatalf("CreateMessageSession did not get expected error: %v", got)
}
errorTypeForComparison := &actions.ActionsError{}
if isActionsError := errors.As(err, &errorTypeForComparison); !isActionsError {
t.Fatalf("CreateMessageSession expected to be able to parse the error into ActionsError type: %v", err)
}
gotErr := err.(*actions.ActionsError)
if diff := cmp.Diff(want, gotErr); diff != "" {
t.Fatalf("CreateMessageSession got unexpected diff: -want +got: %v", diff)
}
})
t.Run("CreateMessageSession call is retried the correct amount of times", func(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
owner := "foo"
runnerScaleSet := actions.RunnerScaleSet{
Id: 1,
Name: "ScaleSet",
CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
RunnerSetting: actions.RunnerSetting{},
}
gotRetries := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
gotRetries++
}))
defer srv.Close()
retryMax := 3
retryWaitMax, err := time.ParseDuration("1µs")
if err != nil {
t.Fatalf("%v", err)
}
wantRetries := retryMax + 1
actionsClient := actions.Client{
ActionsServiceURL: &srv.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
RetryMax: &retryMax,
RetryWaitMax: &retryWaitMax,
}
_, _ = actionsClient.CreateMessageSession(context.Background(), runnerScaleSet.Id, owner)
assert.Equalf(t, gotRetries, wantRetries, "CreateMessageSession got unexpected retry count: got=%v, want=%v", gotRetries, wantRetries)
})
}
func TestDeleteMessageSession(t *testing.T) {
t.Run("DeleteMessageSession call is retried the correct amount of times", func(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
runnerScaleSet := actions.RunnerScaleSet{
Id: 1,
Name: "ScaleSet",
CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
RunnerSetting: actions.RunnerSetting{},
}
gotRetries := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
gotRetries++
}))
defer srv.Close()
retryMax := 3
retryWaitMax, err := time.ParseDuration("1µs")
if err != nil {
t.Fatalf("%v", err)
}
wantRetries := retryMax + 1
actionsClient := actions.Client{
ActionsServiceURL: &srv.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
RetryMax: &retryMax,
RetryWaitMax: &retryWaitMax,
}
sessionId := uuid.New()
_ = actionsClient.DeleteMessageSession(context.Background(), runnerScaleSet.Id, &sessionId)
assert.Equalf(t, gotRetries, wantRetries, "CreateMessageSession got unexpected retry count: got=%v, want=%v", gotRetries, wantRetries)
})
}
func TestRefreshMessageSession(t *testing.T) {
t.Run("RefreshMessageSession call is retried the correct amount of times", func(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
runnerScaleSet := actions.RunnerScaleSet{
Id: 1,
Name: "ScaleSet",
CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
RunnerSetting: actions.RunnerSetting{},
}
gotRetries := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
gotRetries++
}))
defer srv.Close()
retryMax := 3
retryWaitMax, err := time.ParseDuration("1µs")
if err != nil {
t.Fatalf("%v", err)
}
wantRetries := retryMax + 1
actionsClient := actions.Client{
ActionsServiceURL: &srv.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
RetryMax: &retryMax,
RetryWaitMax: &retryWaitMax,
}
sessionId := uuid.New()
_, _ = actionsClient.RefreshMessageSession(context.Background(), runnerScaleSet.Id, &sessionId)
assert.Equalf(t, gotRetries, wantRetries, "CreateMessageSession got unexpected retry count: got=%v, want=%v", gotRetries, wantRetries)
})
}
@@ -0,0 +1,858 @@
package actions_test
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-retryablehttp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetRunnerScaleSet(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
scaleSetName := "ScaleSet"
runnerScaleSet := actions.RunnerScaleSet{Id: 1, Name: scaleSetName}
t.Run("Get existing scale set", func(t *testing.T) {
want := &runnerScaleSet
runnerScaleSetsResp := []byte(`{"count":1,"value":[{"id":1,"name":"ScaleSet"}]}`)
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write(runnerScaleSetsResp)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
got, err := actionsClient.GetRunnerScaleSet(context.Background(), scaleSetName)
if err != nil {
t.Fatalf("CreateRunnerScaleSet got unexepected error, %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("GetRunnerScaleSet(%v) mismatch (-want +got):\n%s", scaleSetName, diff)
}
},
)
t.Run("GetRunnerScaleSet calls correct url", func(t *testing.T) {
runnerScaleSetsResp := []byte(`{"count":1,"value":[{"id":1,"name":"ScaleSet"}]}`)
url := url.URL{}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(runnerScaleSetsResp)
url = *r.URL
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, err := actionsClient.GetRunnerScaleSet(context.Background(), scaleSetName)
if err != nil {
t.Fatalf("CreateRunnerScaleSet got unexepected error, %v", err)
}
u := url.String()
expectedUrl := fmt.Sprintf("/_apis/runtime/runnerscalesets?name=%s&api-version=6.0-preview", scaleSetName)
assert.Equal(t, expectedUrl, u)
},
)
t.Run("Status code not found", func(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, err := actionsClient.GetRunnerScaleSet(context.Background(), scaleSetName)
if err == nil {
t.Fatalf("GetRunnerScaleSet did not get exepected error, ")
}
},
)
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "text/plain")
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, err := actionsClient.GetRunnerScaleSet(context.Background(), scaleSetName)
if err == nil {
t.Fatalf("GetRunnerScaleSet did not get exepected error,")
}
},
)
t.Run("Default retries on server error", func(t *testing.T) {
actualRetry := 0
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
retryClient := retryablehttp.NewClient()
retryMax := 1
retryWaitMax, err := time.ParseDuration("1µs")
if err != nil {
t.Fatalf("%v", err)
}
retryClient.RetryWaitMax = retryWaitMax
retryClient.RetryMax = retryMax
httpClient := retryClient.StandardClient()
actionsClient := actions.Client{
Client: httpClient,
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, _ = actionsClient.GetRunnerScaleSet(context.Background(), scaleSetName)
expectedRetry := retryMax + 1
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
},
)
t.Run("Custom retries on server error", func(t *testing.T) {
actualRetry := 0
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
retryMax := 1
retryWaitMax, err := time.ParseDuration("1µs")
if err != nil {
t.Fatalf("%v", err)
}
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
RetryMax: &retryMax,
RetryWaitMax: &retryWaitMax,
}
_, _ = actionsClient.GetRunnerScaleSet(context.Background(), scaleSetName)
expectedRetry := retryMax + 1
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
},
)
t.Run("RunnerScaleSet count is zero", func(t *testing.T) {
want := (*actions.RunnerScaleSet)(nil)
runnerScaleSetsResp := []byte(`{"count":0,"value":[{"id":1,"name":"ScaleSet"}]}`)
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write(runnerScaleSetsResp)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
got, _ := actionsClient.GetRunnerScaleSet(context.Background(), scaleSetName)
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("GetRunnerScaleSet(%v) mismatch (-want +got):\n%s", scaleSetName, diff)
}
},
)
t.Run("Multiple runner scale sets found", func(t *testing.T) {
wantErr := fmt.Errorf("multiple runner scale sets found with name %s", scaleSetName)
runnerScaleSetsResp := []byte(`{"count":2,"value":[{"id":1,"name":"ScaleSet"}]}`)
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write(runnerScaleSetsResp)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, err := actionsClient.GetRunnerScaleSet(context.Background(), scaleSetName)
if err == nil {
t.Fatalf("GetRunnerScaleSet did not get exepected error, %v", wantErr)
}
if diff := cmp.Diff(wantErr.Error(), err.Error()); diff != "" {
t.Errorf("GetRunnerScaleSet(%v) mismatch (-want +got):\n%s", scaleSetName, diff)
}
},
)
}
func TestGetRunnerScaleSetById(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)
runnerScaleSet := actions.RunnerScaleSet{Id: 1, Name: "ScaleSet", CreatedOn: scaleSetCreationDateTime, RunnerSetting: actions.RunnerSetting{}}
t.Run("Get existing scale set by Id", func(t *testing.T) {
want := &runnerScaleSet
rsl, err := json.Marshal(want)
if err != nil {
t.Fatalf("%v", err)
}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write(rsl)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
got, err := actionsClient.GetRunnerScaleSetById(context.Background(), runnerScaleSet.Id)
if err != nil {
t.Fatalf("GetRunnerScaleSetById got unexepected error, %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("GetRunnerScaleSetById(%d) mismatch (-want +got):\n%s", runnerScaleSet.Id, diff)
}
},
)
t.Run("GetRunnerScaleSetById calls correct url", func(t *testing.T) {
rsl, err := json.Marshal(&runnerScaleSet)
if err != nil {
t.Fatalf("%v", err)
}
url := url.URL{}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(rsl)
url = *r.URL
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, err = actionsClient.GetRunnerScaleSetById(context.Background(), runnerScaleSet.Id)
if err != nil {
t.Fatalf("GetRunnerScaleSetById got unexepected error, %v", err)
}
u := url.String()
expectedUrl := fmt.Sprintf("/_apis/runtime/runnerscalesets/%d?api-version=6.0-preview", runnerScaleSet.Id)
assert.Equal(t, expectedUrl, u)
},
)
t.Run("Status code not found", func(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, err := actionsClient.GetRunnerScaleSetById(context.Background(), runnerScaleSet.Id)
if err == nil {
t.Fatalf("GetRunnerScaleSetById did not get exepected error, ")
}
},
)
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "text/plain")
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, err := actionsClient.GetRunnerScaleSetById(context.Background(), runnerScaleSet.Id)
if err == nil {
t.Fatalf("GetRunnerScaleSetById did not get exepected error,")
}
},
)
t.Run("Default retries on server error", func(t *testing.T) {
actualRetry := 0
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
retryClient := retryablehttp.NewClient()
retryMax := 1
retryWaitMax, err := time.ParseDuration("1µs")
if err != nil {
t.Fatalf("%v", err)
}
retryClient.RetryWaitMax = retryWaitMax
retryClient.RetryMax = retryMax
httpClient := retryClient.StandardClient()
actionsClient := actions.Client{
Client: httpClient,
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, _ = actionsClient.GetRunnerScaleSetById(context.Background(), runnerScaleSet.Id)
expectedRetry := retryMax + 1
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
},
)
t.Run("Custom retries on server error", func(t *testing.T) {
actualRetry := 0
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
retryMax := 1
retryWaitMax, err := time.ParseDuration("1µs")
if err != nil {
t.Fatalf("%v", err)
}
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
RetryMax: &retryMax,
RetryWaitMax: &retryWaitMax,
}
_, _ = actionsClient.GetRunnerScaleSetById(context.Background(), runnerScaleSet.Id)
expectedRetry := retryMax + 1
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
},
)
t.Run("No RunnerScaleSet found", func(t *testing.T) {
want := (*actions.RunnerScaleSet)(nil)
rsl, err := json.Marshal(want)
if err != nil {
t.Fatalf("%v", err)
}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write(rsl)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
got, _ := actionsClient.GetRunnerScaleSetById(context.Background(), runnerScaleSet.Id)
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("GetRunnerScaleSetById(%v) mismatch (-want +got):\n%s", runnerScaleSet.Id, diff)
}
},
)
}
func TestCreateRunnerScaleSet(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)
runnerScaleSet := actions.RunnerScaleSet{Id: 1, Name: "ScaleSet", CreatedOn: scaleSetCreationDateTime, RunnerSetting: actions.RunnerSetting{}}
t.Run("Create runner scale set", func(t *testing.T) {
want := &runnerScaleSet
rsl, err := json.Marshal(want)
if err != nil {
t.Fatalf("%v", err)
}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write(rsl)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
got, err := actionsClient.CreateRunnerScaleSet(context.Background(), &runnerScaleSet)
if err != nil {
t.Fatalf("CreateRunnerScaleSet got exepected error, %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("CreateRunnerScaleSet(%d) mismatch (-want +got):\n%s", runnerScaleSet.Id, diff)
}
},
)
t.Run("CreateRunnerScaleSet calls correct url", func(t *testing.T) {
rsl, err := json.Marshal(&runnerScaleSet)
if err != nil {
t.Fatalf("%v", err)
}
url := url.URL{}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(rsl)
url = *r.URL
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, err = actionsClient.CreateRunnerScaleSet(context.Background(), &runnerScaleSet)
if err != nil {
t.Fatalf("CreateRunnerScaleSet got unexepected error, %v", err)
}
u := url.String()
expectedUrl := "/_apis/runtime/runnerscalesets?api-version=6.0-preview"
assert.Equal(t, expectedUrl, u)
},
)
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "text/plain")
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, err := actionsClient.CreateRunnerScaleSet(context.Background(), &runnerScaleSet)
if err == nil {
t.Fatalf("CreateRunnerScaleSet did not get exepected error, %v", &actions.ActionsError{})
}
var expectedErr *actions.ActionsError
require.True(t, errors.As(err, &expectedErr))
},
)
t.Run("Default retries on server error", func(t *testing.T) {
actualRetry := 0
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
retryClient := retryablehttp.NewClient()
retryMax := 1
retryWaitMax, err := time.ParseDuration("1µs")
if err != nil {
t.Fatalf("%v", err)
}
retryClient.RetryMax = retryMax
retryClient.RetryWaitMax = retryWaitMax
httpClient := retryClient.StandardClient()
actionsClient := actions.Client{
Client: httpClient,
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, _ = actionsClient.CreateRunnerScaleSet(context.Background(), &runnerScaleSet)
expectedRetry := retryMax + 1
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
},
)
t.Run("Custom retries on server error", func(t *testing.T) {
actualRetry := 0
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
retryMax := 1
retryWaitMax, err := time.ParseDuration("1µs")
if err != nil {
t.Fatalf("%v", err)
}
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
RetryMax: &retryMax,
RetryWaitMax: &retryWaitMax,
}
_, _ = actionsClient.CreateRunnerScaleSet(context.Background(), &runnerScaleSet)
expectedRetry := retryMax + 1
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
},
)
}
func TestUpdateRunnerScaleSet(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)
runnerScaleSet := actions.RunnerScaleSet{Id: 1, Name: "ScaleSet", CreatedOn: scaleSetCreationDateTime, RunnerSetting: actions.RunnerSetting{}}
t.Run("Update existing scale set", func(t *testing.T) {
want := &runnerScaleSet
rsl, err := json.Marshal(want)
if err != nil {
t.Fatalf("%v", err)
}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write(rsl)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
got, err := actionsClient.UpdateRunnerScaleSet(context.Background(), runnerScaleSet.Id, want)
if err != nil {
t.Fatalf("UpdateRunnerScaleSet got exepected error, %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("UpdateRunnerScaleSet(%d) mismatch (-want +got):\n%s", runnerScaleSet.Id, diff)
}
},
)
t.Run("UpdateRunnerScaleSet calls correct url", func(t *testing.T) {
rsl, err := json.Marshal(&runnerScaleSet)
if err != nil {
t.Fatalf("%v", err)
}
url := url.URL{}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(rsl)
url = *r.URL
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, err = actionsClient.UpdateRunnerScaleSet(context.Background(), runnerScaleSet.Id, &runnerScaleSet)
if err != nil {
t.Fatalf("UpdateRunnerScaleSet got unexepected error, %v", err)
}
u := url.String()
expectedUrl := fmt.Sprintf("/_apis/runtime/runnerscalesets/%d?api-version=6.0-preview", runnerScaleSet.Id)
assert.Equal(t, expectedUrl, u)
},
)
t.Run("Status code not found", func(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, err := actionsClient.UpdateRunnerScaleSet(context.Background(), runnerScaleSet.Id, &runnerScaleSet)
if err == nil {
t.Fatalf("UpdateRunnerScaleSet did not get exepected error,")
}
var expectedErr *actions.ActionsError
require.True(t, errors.As(err, &expectedErr))
},
)
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "text/plain")
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, err := actionsClient.UpdateRunnerScaleSet(context.Background(), runnerScaleSet.Id, &runnerScaleSet)
if err == nil {
t.Fatalf("UpdateRunnerScaleSet did not get exepected error")
}
var expectedErr *actions.ActionsError
require.True(t, errors.As(err, &expectedErr))
},
)
t.Run("Default retries on server error", func(t *testing.T) {
actualRetry := 0
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
retryClient := retryablehttp.NewClient()
retryMax := 1
retryWaitMax, err := time.ParseDuration("1µs")
if err != nil {
t.Fatalf("%v", err)
}
retryClient.RetryWaitMax = retryWaitMax
retryClient.RetryMax = retryMax
httpClient := retryClient.StandardClient()
actionsClient := actions.Client{
Client: httpClient,
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, _ = actionsClient.UpdateRunnerScaleSet(context.Background(), runnerScaleSet.Id, &runnerScaleSet)
expectedRetry := retryMax + 1
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
},
)
t.Run("Custom retries on server error", func(t *testing.T) {
actualRetry := 0
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
retryMax := 1
retryWaitMax, err := time.ParseDuration("1µs")
if err != nil {
t.Fatalf("%v", err)
}
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
RetryMax: &retryMax,
RetryWaitMax: &retryWaitMax,
}
_, _ = actionsClient.UpdateRunnerScaleSet(context.Background(), runnerScaleSet.Id, &runnerScaleSet)
expectedRetry := retryMax + 1
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
},
)
t.Run("No RunnerScaleSet found", func(t *testing.T) {
want := (*actions.RunnerScaleSet)(nil)
rsl, err := json.Marshal(want)
if err != nil {
t.Fatalf("%v", err)
}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write(rsl)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
got, err := actionsClient.UpdateRunnerScaleSet(context.Background(), runnerScaleSet.Id, &runnerScaleSet)
if err != nil {
t.Fatalf("UpdateRunnerScaleSet got unexepected error, %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("UpdateRunnerScaleSet(%v) mismatch (-want +got):\n%s", runnerScaleSet.Id, diff)
}
},
)
}
func TestDeleteRunnerScaleSet(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)
runnerScaleSet := actions.RunnerScaleSet{Id: 1, Name: "ScaleSet", CreatedOn: scaleSetCreationDateTime, RunnerSetting: actions.RunnerSetting{}}
t.Run("Delete existing scale set", func(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
err := actionsClient.DeleteRunnerScaleSet(context.Background(), runnerScaleSet.Id)
if err != nil {
t.Fatalf("DeleteRunnerScaleSet got unexepected error, %v", err)
}
},
)
t.Run("DeleteRunnerScaleSet calls correct url", func(t *testing.T) {
url := url.URL{}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
url = *r.URL
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
err := actionsClient.DeleteRunnerScaleSet(context.Background(), runnerScaleSet.Id)
if err != nil {
t.Fatalf("DeleteRunnerScaleSet got unexepected error, %v", err)
}
u := url.String()
expectedUrl := fmt.Sprintf("/_apis/runtime/runnerscalesets/%d?api-version=6.0-preview", runnerScaleSet.Id)
assert.Equal(t, expectedUrl, u)
},
)
t.Run("Status code not found", func(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
err := actionsClient.DeleteRunnerScaleSet(context.Background(), runnerScaleSet.Id)
if err == nil {
t.Fatalf("DeleteRunnerScaleSet did not get exepected error, ")
}
var expectedErr *actions.ActionsError
require.True(t, errors.As(err, &expectedErr))
},
)
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "text/plain")
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
err := actionsClient.DeleteRunnerScaleSet(context.Background(), runnerScaleSet.Id)
if err == nil {
t.Fatalf("DeleteRunnerScaleSet did not get exepected error")
}
var expectedErr *actions.ActionsError
require.True(t, errors.As(err, &expectedErr))
},
)
t.Run("Default retries on server error", func(t *testing.T) {
actualRetry := 0
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
retryClient := retryablehttp.NewClient()
retryMax := 1
retryWaitMax, err := time.ParseDuration("1µs")
if err != nil {
t.Fatalf("%v", err)
}
retryClient.RetryWaitMax = retryWaitMax
retryClient.RetryMax = retryMax
httpClient := retryClient.StandardClient()
actionsClient := actions.Client{
Client: httpClient,
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_ = actionsClient.DeleteRunnerScaleSet(context.Background(), runnerScaleSet.Id)
expectedRetry := retryMax + 1
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
},
)
t.Run("Custom retries on server error", func(t *testing.T) {
actualRetry := 0
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
retryMax := 1
retryWaitMax, err := time.ParseDuration("1µs")
if err != nil {
t.Fatalf("%v", err)
}
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
RetryMax: &retryMax,
RetryWaitMax: &retryWaitMax,
}
_ = actionsClient.DeleteRunnerScaleSet(context.Background(), runnerScaleSet.Id)
expectedRetry := retryMax + 1
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
},
)
t.Run("No RunnerScaleSet found", func(t *testing.T) {
want := (*actions.RunnerScaleSet)(nil)
rsl, err := json.Marshal(want)
if err != nil {
t.Fatalf("%v", err)
}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write(rsl)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
err = actionsClient.DeleteRunnerScaleSet(context.Background(), runnerScaleSet.Id)
var expectedErr *actions.ActionsError
require.True(t, errors.As(err, &expectedErr))
},
)
}
+219
View File
@@ -0,0 +1,219 @@
package actions_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-retryablehttp"
"github.com/stretchr/testify/assert"
)
var tokenExpireAt = time.Now().Add(10 * time.Minute)
func TestGetRunner(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
t.Run("Get Runner", func(t *testing.T) {
name := "Get Runner"
var runnerID int64 = 1
want := &actions.RunnerReference{
Id: int(runnerID),
Name: "self-hosted-ubuntu",
}
response := []byte(`{"id": 1, "name": "self-hosted-ubuntu"}`)
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(response)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
got, err := actionsClient.GetRunner(context.Background(), runnerID)
if err != nil {
t.Fatalf("GetRunner got unexepected error, %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("GetRunner(%v) mismatch (-want +got):\n%s", name, diff)
}
})
t.Run("Default retries on server error", func(t *testing.T) {
var runnerID int64 = 1
retryClient := retryablehttp.NewClient()
retryClient.RetryWaitMax = 1 * time.Millisecond
retryClient.RetryMax = 1
actualRetry := 0
expectedRetry := retryClient.RetryMax + 1
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
httpClient := retryClient.StandardClient()
actionsClient := actions.Client{
Client: httpClient,
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, _ = actionsClient.GetRunner(context.Background(), runnerID)
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
})
}
func TestGetRunnerByName(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
t.Run("Get Runner by Name", func(t *testing.T) {
var runnerID int64 = 1
var runnerName string = "self-hosted-ubuntu"
want := &actions.RunnerReference{
Id: int(runnerID),
Name: runnerName,
}
response := []byte(`{"count": 1, "value": [{"id": 1, "name": "self-hosted-ubuntu"}]}`)
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(response)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
got, err := actionsClient.GetRunnerByName(context.Background(), runnerName)
if err != nil {
t.Fatalf("GetRunnerByName got unexepected error, %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("GetRunnerByName(%v) mismatch (-want +got):\n%s", runnerName, diff)
}
})
t.Run("Get Runner by name with not exist runner", func(t *testing.T) {
var runnerName string = "self-hosted-ubuntu"
response := []byte(`{"count": 0, "value": []}`)
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(response)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
got, err := actionsClient.GetRunnerByName(context.Background(), runnerName)
if err != nil {
t.Fatalf("GetRunnerByName got unexepected error, %v", err)
}
if diff := cmp.Diff((*actions.RunnerReference)(nil), got); diff != "" {
t.Errorf("GetRunnerByName(%v) mismatch (-want +got):\n%s", runnerName, diff)
}
})
t.Run("Default retries on server error", func(t *testing.T) {
var runnerName string = "self-hosted-ubuntu"
retryClient := retryablehttp.NewClient()
retryClient.RetryWaitMax = 1 * time.Millisecond
retryClient.RetryMax = 1
actualRetry := 0
expectedRetry := retryClient.RetryMax + 1
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
httpClient := retryClient.StandardClient()
actionsClient := actions.Client{
Client: httpClient,
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_, _ = actionsClient.GetRunnerByName(context.Background(), runnerName)
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
})
}
func TestDeleteRunner(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
t.Run("Delete Runner", func(t *testing.T) {
var runnerID int64 = 1
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
defer s.Close()
actionsClient := actions.Client{
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
if err := actionsClient.RemoveRunner(context.Background(), runnerID); err != nil {
t.Fatalf("RemoveRunner got unexepected error, %v", err)
}
})
t.Run("Default retries on server error", func(t *testing.T) {
var runnerID int64 = 1
retryClient := retryablehttp.NewClient()
retryClient.RetryWaitMax = 1 * time.Millisecond
retryClient.RetryMax = 1
actualRetry := 0
expectedRetry := retryClient.RetryMax + 1
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
actualRetry++
}))
defer s.Close()
httpClient := retryClient.StandardClient()
actionsClient := actions.Client{
Client: httpClient,
ActionsServiceURL: &s.URL,
ActionsServiceAdminToken: &token,
ActionsServiceAdminTokenExpiresAt: &tokenExpireAt,
}
_ = actionsClient.RemoveRunner(context.Background(), runnerID)
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
})
}
+71
View File
@@ -0,0 +1,71 @@
package actions
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
type ActionsError struct {
ExceptionName string `json:"typeName,omitempty"`
Message string `json:"message,omitempty"`
StatusCode int
}
func (e *ActionsError) Error() string {
return fmt.Sprintf("%v - had issue communicating with Actions backend: %v", e.StatusCode, e.Message)
}
func ParseActionsErrorFromResponse(response *http.Response) error {
if response.ContentLength == 0 {
message := "Request returned status: " + response.Status
return &ActionsError{
ExceptionName: "unknown",
Message: message,
StatusCode: response.StatusCode,
}
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
return err
}
body = trimByteOrderMark(body)
contentType, ok := response.Header["Content-Type"]
if ok && len(contentType) > 0 && strings.Contains(contentType[0], "text/plain") {
message := string(body)
statusCode := response.StatusCode
return &ActionsError{
Message: message,
StatusCode: statusCode,
}
}
actionsError := &ActionsError{StatusCode: response.StatusCode}
if err := json.Unmarshal(body, &actionsError); err != nil {
return err
}
return actionsError
}
type MessageQueueTokenExpiredError struct {
msg string
}
func (e *MessageQueueTokenExpiredError) Error() string {
return e.msg
}
type HttpClientSideError struct {
msg string
Code int
}
func (e *HttpClientSideError) Error() string {
return e.msg
}
+235
View File
@@ -0,0 +1,235 @@
package fake
import (
"context"
"time"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/google/uuid"
)
type Option func(*FakeClient)
func WithGetRunnerScaleSetResult(scaleSet *actions.RunnerScaleSet, err error) Option {
return func(f *FakeClient) {
f.getRunnerScaleSetResult.RunnerScaleSet = scaleSet
f.getRunnerScaleSetResult.err = err
}
}
func WithGetRunner(runner *actions.RunnerReference, err error) Option {
return func(f *FakeClient) {
f.getRunnerResult.RunnerReference = runner
f.getRunnerResult.err = err
}
}
var defaultRunnerScaleSet = &actions.RunnerScaleSet{
Id: 1,
Name: "testset",
RunnerGroupId: 1,
RunnerGroupName: "testgroup",
Labels: []actions.Label{{Type: "test", Name: "test"}},
RunnerSetting: actions.RunnerSetting{},
CreatedOn: time.Now(),
RunnerJitConfigUrl: "test.test.test",
Statistics: nil,
}
var defaultRunnerGroup = &actions.RunnerGroup{
ID: 1,
Name: "testgroup",
Size: 1,
IsDefault: true,
}
var sessionID = uuid.New()
var defaultRunnerScaleSetSession = &actions.RunnerScaleSetSession{
SessionId: &sessionID,
OwnerName: "testowner",
RunnerScaleSet: defaultRunnerScaleSet,
MessageQueueUrl: "https://test.url/path",
MessageQueueAccessToken: "faketoken",
Statistics: nil,
}
var defaultAcquirableJob = &actions.AcquirableJob{
AcquireJobUrl: "https://test.url",
MessageType: "",
RunnerRequestId: 1,
RepositoryName: "testrepo",
OwnerName: "testowner",
JobWorkflowRef: "workflowref",
EventName: "testevent",
RequestLabels: []string{"test"},
}
var defaultAcquirableJobList = &actions.AcquirableJobList{
Count: 1,
Jobs: []actions.AcquirableJob{*defaultAcquirableJob},
}
var defaultRunnerReference = &actions.RunnerReference{
Id: 1,
Name: "testrunner",
RunnerScaleSetId: 1,
}
var defaultRunnerScaleSetMessage = &actions.RunnerScaleSetMessage{
MessageId: 1,
MessageType: "test",
Body: "{}",
Statistics: nil,
}
var defaultRunnerScaleSetJitRunnerConfig = &actions.RunnerScaleSetJitRunnerConfig{
Runner: defaultRunnerReference,
EncodedJITConfig: "test",
}
// FakeClient implements actions service
type FakeClient struct {
getRunnerScaleSetResult struct {
*actions.RunnerScaleSet
err error
}
getRunnerScaleSetByIdResult struct {
*actions.RunnerScaleSet
err error
}
getRunnerGroupByNameResult struct {
*actions.RunnerGroup
err error
}
createRunnerScaleSetResult struct {
*actions.RunnerScaleSet
err error
}
createMessageSessionResult struct {
*actions.RunnerScaleSetSession
err error
}
deleteMessageSessionResult struct {
err error
}
refreshMessageSessionResult struct {
*actions.RunnerScaleSetSession
err error
}
acquireJobsResult struct {
ids []int64
err error
}
getAcquirableJobsResult struct {
*actions.AcquirableJobList
err error
}
getMessageResult struct {
*actions.RunnerScaleSetMessage
err error
}
deleteMessageResult struct {
err error
}
generateJitRunnerConfigResult struct {
*actions.RunnerScaleSetJitRunnerConfig
err error
}
getRunnerResult struct {
*actions.RunnerReference
err error
}
getRunnerByNameResult struct {
*actions.RunnerReference
err error
}
removeRunnerResult struct {
err error
}
}
func NewFakeClient(options ...Option) actions.ActionsService {
f := &FakeClient{}
f.applyDefaults()
for _, opt := range options {
opt(f)
}
return f
}
func (f *FakeClient) applyDefaults() {
f.getRunnerScaleSetResult.RunnerScaleSet = defaultRunnerScaleSet
f.getRunnerScaleSetByIdResult.RunnerScaleSet = defaultRunnerScaleSet
f.getRunnerGroupByNameResult.RunnerGroup = defaultRunnerGroup
f.createRunnerScaleSetResult.RunnerScaleSet = defaultRunnerScaleSet
f.createMessageSessionResult.RunnerScaleSetSession = defaultRunnerScaleSetSession
f.refreshMessageSessionResult.RunnerScaleSetSession = defaultRunnerScaleSetSession
f.acquireJobsResult.ids = []int64{1}
f.getAcquirableJobsResult.AcquirableJobList = defaultAcquirableJobList
f.getMessageResult.RunnerScaleSetMessage = defaultRunnerScaleSetMessage
f.generateJitRunnerConfigResult.RunnerScaleSetJitRunnerConfig = defaultRunnerScaleSetJitRunnerConfig
f.getRunnerResult.RunnerReference = defaultRunnerReference
f.getRunnerByNameResult.RunnerReference = defaultRunnerReference
}
func (f *FakeClient) GetRunnerScaleSet(ctx context.Context, runnerScaleSetName string) (*actions.RunnerScaleSet, error) {
return f.getRunnerScaleSetResult.RunnerScaleSet, f.getRunnerScaleSetResult.err
}
func (f *FakeClient) GetRunnerScaleSetById(ctx context.Context, runnerScaleSetId int) (*actions.RunnerScaleSet, error) {
return f.getRunnerScaleSetByIdResult.RunnerScaleSet, f.getRunnerScaleSetResult.err
}
func (f *FakeClient) GetRunnerGroupByName(ctx context.Context, runnerGroup string) (*actions.RunnerGroup, error) {
return f.getRunnerGroupByNameResult.RunnerGroup, f.getRunnerGroupByNameResult.err
}
func (f *FakeClient) CreateRunnerScaleSet(ctx context.Context, runnerScaleSet *actions.RunnerScaleSet) (*actions.RunnerScaleSet, error) {
return f.createRunnerScaleSetResult.RunnerScaleSet, f.createRunnerScaleSetResult.err
}
func (f *FakeClient) CreateMessageSession(ctx context.Context, runnerScaleSetId int, owner string) (*actions.RunnerScaleSetSession, error) {
return f.createMessageSessionResult.RunnerScaleSetSession, f.createMessageSessionResult.err
}
func (f *FakeClient) DeleteMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) error {
return f.deleteMessageSessionResult.err
}
func (f *FakeClient) RefreshMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) (*actions.RunnerScaleSetSession, error) {
return f.refreshMessageSessionResult.RunnerScaleSetSession, f.refreshMessageSessionResult.err
}
func (f *FakeClient) AcquireJobs(ctx context.Context, runnerScaleSetId int, messageQueueAccessToken string, requestIds []int64) ([]int64, error) {
return f.acquireJobsResult.ids, f.acquireJobsResult.err
}
func (f *FakeClient) GetAcquirableJobs(ctx context.Context, runnerScaleSetId int) (*actions.AcquirableJobList, error) {
return f.getAcquirableJobsResult.AcquirableJobList, f.getAcquirableJobsResult.err
}
func (f *FakeClient) GetMessage(ctx context.Context, messageQueueUrl, messageQueueAccessToken string, lastMessageId int64) (*actions.RunnerScaleSetMessage, error) {
return f.getMessageResult.RunnerScaleSetMessage, f.getMessageResult.err
}
func (f *FakeClient) DeleteMessage(ctx context.Context, messageQueueUrl, messageQueueAccessToken string, messageId int64) error {
return f.deleteMessageResult.err
}
func (f *FakeClient) GenerateJitRunnerConfig(ctx context.Context, jitRunnerSetting *actions.RunnerScaleSetJitRunnerSetting, scaleSetId int) (*actions.RunnerScaleSetJitRunnerConfig, error) {
return f.generateJitRunnerConfigResult.RunnerScaleSetJitRunnerConfig, f.generateJitRunnerConfigResult.err
}
func (f *FakeClient) GetRunner(ctx context.Context, runnerId int64) (*actions.RunnerReference, error) {
return f.getRunnerResult.RunnerReference, f.getRunnerResult.err
}
func (f *FakeClient) GetRunnerByName(ctx context.Context, runnerName string) (*actions.RunnerReference, error) {
return f.getRunnerByNameResult.RunnerReference, f.getRunnerByNameResult.err
}
func (f *FakeClient) RemoveRunner(ctx context.Context, runnerId int64) error {
return f.removeRunnerResult.err
}
+43
View File
@@ -0,0 +1,43 @@
package fake
import (
"context"
"github.com/actions/actions-runner-controller/github/actions"
)
type MultiClientOption func(*fakeMultiClient)
func WithDefaultClient(client actions.ActionsService, err error) MultiClientOption {
return func(f *fakeMultiClient) {
f.defaultClient = client
f.defaultErr = err
}
}
type fakeMultiClient struct {
defaultClient actions.ActionsService
defaultErr error
}
func NewMultiClient(opts ...MultiClientOption) actions.MultiClient {
f := &fakeMultiClient{}
for _, opt := range opts {
opt(f)
}
if f.defaultClient == nil {
f.defaultClient = NewFakeClient()
}
return f
}
func (f *fakeMultiClient) GetClientFor(ctx context.Context, githubConfigURL string, creds actions.ActionsAuth, namespace string) (actions.ActionsService, error) {
return f.defaultClient, f.defaultErr
}
func (f *fakeMultiClient) GetClientFromSecret(ctx context.Context, githubConfigURL, namespace string, secretData actions.KubernetesSecretData) (actions.ActionsService, error) {
return f.defaultClient, f.defaultErr
}
+348
View File
@@ -0,0 +1,348 @@
// Code generated by mockery v2.16.0. DO NOT EDIT.
package actions
import (
context "context"
uuid "github.com/google/uuid"
mock "github.com/stretchr/testify/mock"
)
// MockActionsService is an autogenerated mock type for the ActionsService type
type MockActionsService struct {
mock.Mock
}
// AcquireJobs provides a mock function with given fields: ctx, runnerScaleSetId, messageQueueAccessToken, requestIds
func (_m *MockActionsService) AcquireJobs(ctx context.Context, runnerScaleSetId int, messageQueueAccessToken string, requestIds []int64) ([]int64, error) {
ret := _m.Called(ctx, runnerScaleSetId, messageQueueAccessToken, requestIds)
var r0 []int64
if rf, ok := ret.Get(0).(func(context.Context, int, string, []int64) []int64); ok {
r0 = rf(ctx, runnerScaleSetId, messageQueueAccessToken, requestIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int64)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int, string, []int64) error); ok {
r1 = rf(ctx, runnerScaleSetId, messageQueueAccessToken, requestIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateMessageSession provides a mock function with given fields: ctx, runnerScaleSetId, owner
func (_m *MockActionsService) CreateMessageSession(ctx context.Context, runnerScaleSetId int, owner string) (*RunnerScaleSetSession, error) {
ret := _m.Called(ctx, runnerScaleSetId, owner)
var r0 *RunnerScaleSetSession
if rf, ok := ret.Get(0).(func(context.Context, int, string) *RunnerScaleSetSession); ok {
r0 = rf(ctx, runnerScaleSetId, owner)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*RunnerScaleSetSession)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int, string) error); ok {
r1 = rf(ctx, runnerScaleSetId, owner)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateRunnerScaleSet provides a mock function with given fields: ctx, runnerScaleSet
func (_m *MockActionsService) CreateRunnerScaleSet(ctx context.Context, runnerScaleSet *RunnerScaleSet) (*RunnerScaleSet, error) {
ret := _m.Called(ctx, runnerScaleSet)
var r0 *RunnerScaleSet
if rf, ok := ret.Get(0).(func(context.Context, *RunnerScaleSet) *RunnerScaleSet); ok {
r0 = rf(ctx, runnerScaleSet)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*RunnerScaleSet)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *RunnerScaleSet) error); ok {
r1 = rf(ctx, runnerScaleSet)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteMessage provides a mock function with given fields: ctx, messageQueueUrl, messageQueueAccessToken, messageId
func (_m *MockActionsService) DeleteMessage(ctx context.Context, messageQueueUrl string, messageQueueAccessToken string, messageId int64) error {
ret := _m.Called(ctx, messageQueueUrl, messageQueueAccessToken, messageId)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) error); ok {
r0 = rf(ctx, messageQueueUrl, messageQueueAccessToken, messageId)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteMessageSession provides a mock function with given fields: ctx, runnerScaleSetId, sessionId
func (_m *MockActionsService) 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
}
// GenerateJitRunnerConfig provides a mock function with given fields: ctx, jitRunnerSetting, scaleSetId
func (_m *MockActionsService) GenerateJitRunnerConfig(ctx context.Context, jitRunnerSetting *RunnerScaleSetJitRunnerSetting, scaleSetId int) (*RunnerScaleSetJitRunnerConfig, error) {
ret := _m.Called(ctx, jitRunnerSetting, scaleSetId)
var r0 *RunnerScaleSetJitRunnerConfig
if rf, ok := ret.Get(0).(func(context.Context, *RunnerScaleSetJitRunnerSetting, int) *RunnerScaleSetJitRunnerConfig); ok {
r0 = rf(ctx, jitRunnerSetting, scaleSetId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*RunnerScaleSetJitRunnerConfig)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *RunnerScaleSetJitRunnerSetting, int) error); ok {
r1 = rf(ctx, jitRunnerSetting, scaleSetId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAcquirableJobs provides a mock function with given fields: ctx, runnerScaleSetId
func (_m *MockActionsService) GetAcquirableJobs(ctx context.Context, runnerScaleSetId int) (*AcquirableJobList, error) {
ret := _m.Called(ctx, runnerScaleSetId)
var r0 *AcquirableJobList
if rf, ok := ret.Get(0).(func(context.Context, int) *AcquirableJobList); ok {
r0 = rf(ctx, runnerScaleSetId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*AcquirableJobList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, runnerScaleSetId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMessage provides a mock function with given fields: ctx, messageQueueUrl, messageQueueAccessToken, lastMessageId
func (_m *MockActionsService) GetMessage(ctx context.Context, messageQueueUrl string, messageQueueAccessToken string, lastMessageId int64) (*RunnerScaleSetMessage, error) {
ret := _m.Called(ctx, messageQueueUrl, messageQueueAccessToken, lastMessageId)
var r0 *RunnerScaleSetMessage
if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) *RunnerScaleSetMessage); ok {
r0 = rf(ctx, messageQueueUrl, messageQueueAccessToken, lastMessageId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*RunnerScaleSetMessage)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, string, int64) error); ok {
r1 = rf(ctx, messageQueueUrl, messageQueueAccessToken, lastMessageId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRunner provides a mock function with given fields: ctx, runnerId
func (_m *MockActionsService) GetRunner(ctx context.Context, runnerId int64) (*RunnerReference, error) {
ret := _m.Called(ctx, runnerId)
var r0 *RunnerReference
if rf, ok := ret.Get(0).(func(context.Context, int64) *RunnerReference); ok {
r0 = rf(ctx, runnerId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*RunnerReference)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, runnerId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRunnerByName provides a mock function with given fields: ctx, runnerName
func (_m *MockActionsService) GetRunnerByName(ctx context.Context, runnerName string) (*RunnerReference, error) {
ret := _m.Called(ctx, runnerName)
var r0 *RunnerReference
if rf, ok := ret.Get(0).(func(context.Context, string) *RunnerReference); ok {
r0 = rf(ctx, runnerName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*RunnerReference)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, runnerName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRunnerGroupByName provides a mock function with given fields: ctx, runnerGroup
func (_m *MockActionsService) GetRunnerGroupByName(ctx context.Context, runnerGroup string) (*RunnerGroup, error) {
ret := _m.Called(ctx, runnerGroup)
var r0 *RunnerGroup
if rf, ok := ret.Get(0).(func(context.Context, string) *RunnerGroup); ok {
r0 = rf(ctx, runnerGroup)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*RunnerGroup)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, runnerGroup)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRunnerScaleSet provides a mock function with given fields: ctx, runnerScaleSetName
func (_m *MockActionsService) GetRunnerScaleSet(ctx context.Context, runnerScaleSetName string) (*RunnerScaleSet, error) {
ret := _m.Called(ctx, runnerScaleSetName)
var r0 *RunnerScaleSet
if rf, ok := ret.Get(0).(func(context.Context, string) *RunnerScaleSet); ok {
r0 = rf(ctx, runnerScaleSetName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*RunnerScaleSet)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, runnerScaleSetName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRunnerScaleSetById provides a mock function with given fields: ctx, runnerScaleSetId
func (_m *MockActionsService) GetRunnerScaleSetById(ctx context.Context, runnerScaleSetId int) (*RunnerScaleSet, error) {
ret := _m.Called(ctx, runnerScaleSetId)
var r0 *RunnerScaleSet
if rf, ok := ret.Get(0).(func(context.Context, int) *RunnerScaleSet); ok {
r0 = rf(ctx, runnerScaleSetId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*RunnerScaleSet)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, runnerScaleSetId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RefreshMessageSession provides a mock function with given fields: ctx, runnerScaleSetId, sessionId
func (_m *MockActionsService) RefreshMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) (*RunnerScaleSetSession, error) {
ret := _m.Called(ctx, runnerScaleSetId, sessionId)
var r0 *RunnerScaleSetSession
if rf, ok := ret.Get(0).(func(context.Context, int, *uuid.UUID) *RunnerScaleSetSession); ok {
r0 = rf(ctx, runnerScaleSetId, sessionId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*RunnerScaleSetSession)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int, *uuid.UUID) error); ok {
r1 = rf(ctx, runnerScaleSetId, sessionId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RemoveRunner provides a mock function with given fields: ctx, runnerId
func (_m *MockActionsService) RemoveRunner(ctx context.Context, runnerId int64) error {
ret := _m.Called(ctx, runnerId)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, runnerId)
} else {
r0 = ret.Error(0)
}
return r0
}
type mockConstructorTestingTNewMockActionsService interface {
mock.TestingT
Cleanup(func())
}
// NewMockActionsService creates a new instance of MockActionsService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockActionsService(t mockConstructorTestingTNewMockActionsService) *MockActionsService {
mock := &MockActionsService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
+103
View File
@@ -0,0 +1,103 @@
// Code generated by mockery v2.16.0. DO NOT EDIT.
package actions
import (
context "context"
mock "github.com/stretchr/testify/mock"
)
// MockSessionService is an autogenerated mock type for the SessionService type
type MockSessionService struct {
mock.Mock
}
// AcquireJobs provides a mock function with given fields: ctx, requestIds
func (_m *MockSessionService) AcquireJobs(ctx context.Context, requestIds []int64) ([]int64, error) {
ret := _m.Called(ctx, requestIds)
var r0 []int64
if rf, ok := ret.Get(0).(func(context.Context, []int64) []int64); ok {
r0 = rf(ctx, requestIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int64)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, []int64) error); ok {
r1 = rf(ctx, requestIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Close provides a mock function with given fields:
func (_m *MockSessionService) Close() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteMessage provides a mock function with given fields: ctx, messageId
func (_m *MockSessionService) DeleteMessage(ctx context.Context, messageId int64) error {
ret := _m.Called(ctx, messageId)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, messageId)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetMessage provides a mock function with given fields: ctx, lastMessageId
func (_m *MockSessionService) GetMessage(ctx context.Context, lastMessageId int64) (*RunnerScaleSetMessage, error) {
ret := _m.Called(ctx, lastMessageId)
var r0 *RunnerScaleSetMessage
if rf, ok := ret.Get(0).(func(context.Context, int64) *RunnerScaleSetMessage); ok {
r0 = rf(ctx, lastMessageId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*RunnerScaleSetMessage)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, lastMessageId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewMockSessionService interface {
mock.TestingT
Cleanup(func())
}
// NewMockSessionService creates a new instance of MockSessionService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockSessionService(t mockConstructorTestingTNewMockSessionService) *MockSessionService {
mock := &MockSessionService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
+164
View File
@@ -0,0 +1,164 @@
package actions
import (
"context"
"fmt"
"net/url"
"strconv"
"sync"
"github.com/go-logr/logr"
)
type MultiClient interface {
GetClientFor(ctx context.Context, githubConfigURL string, creds ActionsAuth, namespace string) (ActionsService, error)
GetClientFromSecret(ctx context.Context, githubConfigURL, namespace string, secretData KubernetesSecretData) (ActionsService, error)
}
type multiClient struct {
// To lock adding and removing of individual clients.
mu sync.Mutex
clients map[ActionsClientKey]*actionsClientWrapper
logger logr.Logger
userAgent string
}
type GitHubAppAuth struct {
AppID int64
AppInstallationID int64
AppPrivateKey string
}
type ActionsAuth struct {
// GitHub App
AppCreds *GitHubAppAuth
// GitHub PAT
Token string
}
type ActionsClientKey struct {
ActionsURL string
Auth ActionsAuth
Namespace string
}
type actionsClientWrapper struct {
// To lock client usage when tokens are being refreshed.
mu sync.Mutex
client ActionsService
}
func NewMultiClient(userAgent string, logger logr.Logger) MultiClient {
return &multiClient{
mu: sync.Mutex{},
clients: make(map[ActionsClientKey]*actionsClientWrapper),
logger: logger,
userAgent: userAgent,
}
}
func (m *multiClient) GetClientFor(ctx context.Context, githubConfigURL string, creds ActionsAuth, namespace string) (ActionsService, error) {
m.logger.Info("retrieve actions client", "githubConfigURL", githubConfigURL, "namespace", namespace)
parsedGitHubURL, err := url.Parse(githubConfigURL)
if err != nil {
return nil, err
}
if creds.Token == "" && creds.AppCreds == nil {
return nil, fmt.Errorf("no credentials provided. either a PAT or GitHub App credentials should be provided")
}
if creds.Token != "" && creds.AppCreds != nil {
return nil, fmt.Errorf("both PAT and GitHub App credentials provided. should only provide one")
}
key := ActionsClientKey{
ActionsURL: parsedGitHubURL.String(),
Namespace: namespace,
}
if creds.AppCreds != nil {
key.Auth = ActionsAuth{
AppCreds: creds.AppCreds,
}
}
if creds.Token != "" {
key.Auth = ActionsAuth{
Token: creds.Token,
}
}
m.mu.Lock()
defer m.mu.Unlock()
clientWrapper, has := m.clients[key]
if has {
m.logger.Info("using cache client", "githubConfigURL", githubConfigURL, "namespace", namespace)
return clientWrapper.client, nil
}
m.logger.Info("creating new client", "githubConfigURL", githubConfigURL, "namespace", namespace)
client, err := NewClient(ctx, githubConfigURL, &creds, m.userAgent, m.logger)
if err != nil {
return nil, err
}
m.clients[key] = &actionsClientWrapper{
mu: sync.Mutex{},
client: client,
}
m.logger.Info("successfully created new client", "githubConfigURL", githubConfigURL, "namespace", namespace)
return client, nil
}
type KubernetesSecretData map[string][]byte
func (m *multiClient) GetClientFromSecret(ctx context.Context, githubConfigURL, namespace string, secretData KubernetesSecretData) (ActionsService, error) {
if len(secretData) == 0 {
return nil, fmt.Errorf("must provide secret data with either PAT or GitHub App Auth")
}
token := string(secretData["github_token"])
hasToken := len(token) > 0
appID := string(secretData["github_app_id"])
appInstallationID := string(secretData["github_app_installation_id"])
appPrivateKey := string(secretData["github_app_private_key"])
hasGitHubAppAuth := len(appID) > 0 && len(appInstallationID) > 0 && len(appPrivateKey) > 0
if hasToken && hasGitHubAppAuth {
return nil, fmt.Errorf("must provide secret with only PAT or GitHub App Auth to avoid ambiguity in client behavior")
}
if !hasToken && !hasGitHubAppAuth {
return nil, fmt.Errorf("neither PAT nor GitHub App Auth credentials provided in secret")
}
auth := ActionsAuth{}
if hasToken {
auth.Token = token
return m.GetClientFor(ctx, githubConfigURL, auth, namespace)
}
parsedAppID, err := strconv.ParseInt(appID, 10, 64)
if err != nil {
return nil, err
}
parsedAppInstallationID, err := strconv.ParseInt(appInstallationID, 10, 64)
if err != nil {
return nil, err
}
auth.AppCreds = &GitHubAppAuth{AppID: parsedAppID, AppInstallationID: parsedAppInstallationID, AppPrivateKey: appPrivateKey}
return m.GetClientFor(ctx, githubConfigURL, auth, namespace)
}
+163
View File
@@ -0,0 +1,163 @@
package actions
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/actions/actions-runner-controller/logging"
)
func TestAddClient(t *testing.T) {
logger, err := logging.NewLogger(logging.LogLevelDebug, logging.LogFormatText)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: creating logger: %v\n", err)
os.Exit(1)
}
multiClient := NewMultiClient("test-user-agent", logger).(*multiClient)
ctx := context.Background()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "actions/runners/registration-token") {
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
token := "abc-123"
rt := &registrationToken{Token: &token}
if err := json.NewEncoder(w).Encode(rt); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
if strings.HasSuffix(r.URL.Path, "actions/runner-registration") {
w.Header().Set("Content-Type", "application/json")
url := "actions.github.com/abc"
jwt := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
adminConnInfo := &ActionsServiceAdminConnection{ActionsServiceUrl: &url, AdminToken: &jwt}
if err := json.NewEncoder(w).Encode(adminConnInfo); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
if strings.HasSuffix(r.URL.Path, "/access_tokens") {
w.Header().Set("Content-Type", "application/vnd.github+json")
t, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z07:00")
accessToken := &accessToken{
Token: "abc-123",
ExpiresAt: t,
}
if err := json.NewEncoder(w).Encode(accessToken); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}))
defer srv.Close()
want := 1
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{Token: "PAT"}, "namespace"); err != nil {
t.Fatal(err)
}
want++ // New repo
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/actions", srv.URL), ActionsAuth{Token: "PAT"}, "namespace"); err != nil {
t.Fatal(err)
}
// Repeat
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{Token: "PAT"}, "namespace"); err != nil {
t.Fatal(err)
}
want++ // New namespace
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{Token: "PAT"}, "other"); err != nil {
t.Fatal(err)
}
want++ // New pat
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{Token: "other"}, "other"); err != nil {
t.Fatal(err)
}
want++ // New org
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github", srv.URL), ActionsAuth{Token: "PAT"}, "other"); err != nil {
t.Fatal(err)
}
// No org, repo, enterprise
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v", srv.URL), ActionsAuth{Token: "PAT"}, "other"); err == nil {
t.Fatal(err)
}
want++ // Test keying on GitHub App
appAuth := &GitHubAppAuth{
AppID: 1,
AppPrivateKey: `-----BEGIN RSA PRIVATE KEY-----
MIICWgIBAAKBgHXfRT9cv9UY9fAAD4+1RshpfSSZe277urfEmPfX3/Og9zJYRk//
CZrJVD1CaBZDiIyQsNEzjta7r4UsqWdFOggiNN2E7ZTFQjMSaFkVgrzHqWuiaCBf
/BjbKPn4SMDmTzHvIe7Nel76hBdCaVgu6mYCW5jmuSH5qz/yR1U1J/WJAgMBAAEC
gYARWGWsSU3BYgbu5lNj5l0gKMXNmPhdAJYdbMTF0/KUu18k/XB7XSBgsre+vALt
I8r4RGKApoGif8P4aPYUyE8dqA1bh0X3Fj1TCz28qoUL5//dA+pigCRS20H7HM3C
ojoqF7+F+4F2sXmzFNd1NgY5RxFPYosTT7OnUiFuu2IisQJBALnMLe09LBnjuHXR
xxR65DDNxWPQLBjW3dL+ubLcwr7922l6ZIQsVjdeE0ItEUVRjjJ9/B/Jq9VJ/Lw4
g9LCkkMCQQCiaM2f7nYmGivPo9hlAbq5lcGJ5CCYFfeeYzTxMqum7Mbqe4kk5lgb
X6gWd0Izg2nGdAEe/97DClO6VpKcPbpDAkBTR/JOJN1fvXMxXJaf13XxakrQMr+R
Yr6LlSInykyAz8lJvlLP7A+5QbHgN9NF/wh+GXqpxPwA3ukqdSqhjhWBAkBn6mDv
HPgR5xrzL6XM8y9TgaOlJAdK6HtYp6d/UOmN0+Butf6JUq07TphRT5tXNJVgemch
O5x/9UKfbrc+KyzbAkAo97TfFC+mZhU1N5fFelaRu4ikPxlp642KRUSkOh8GEkNf
jQ97eJWiWtDcsMUhcZgoB5ydHcFlrBIn6oBcpge5
-----END RSA PRIVATE KEY-----`,
}
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{AppCreds: appAuth}, "other"); err != nil {
t.Fatal(err)
}
// Repeat last to verify GitHub App keys are mapped together
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{AppCreds: appAuth}, "other"); err != nil {
t.Fatal(err)
}
if len(multiClient.clients) != want {
t.Fatalf("GetClientFor: unexpected number of clients: got=%v want=%v", len(multiClient.clients), want)
}
}
func TestCreateJWT(t *testing.T) {
key := `-----BEGIN RSA PRIVATE KEY-----
MIICWgIBAAKBgHXfRT9cv9UY9fAAD4+1RshpfSSZe277urfEmPfX3/Og9zJYRk//
CZrJVD1CaBZDiIyQsNEzjta7r4UsqWdFOggiNN2E7ZTFQjMSaFkVgrzHqWuiaCBf
/BjbKPn4SMDmTzHvIe7Nel76hBdCaVgu6mYCW5jmuSH5qz/yR1U1J/WJAgMBAAEC
gYARWGWsSU3BYgbu5lNj5l0gKMXNmPhdAJYdbMTF0/KUu18k/XB7XSBgsre+vALt
I8r4RGKApoGif8P4aPYUyE8dqA1bh0X3Fj1TCz28qoUL5//dA+pigCRS20H7HM3C
ojoqF7+F+4F2sXmzFNd1NgY5RxFPYosTT7OnUiFuu2IisQJBALnMLe09LBnjuHXR
xxR65DDNxWPQLBjW3dL+ubLcwr7922l6ZIQsVjdeE0ItEUVRjjJ9/B/Jq9VJ/Lw4
g9LCkkMCQQCiaM2f7nYmGivPo9hlAbq5lcGJ5CCYFfeeYzTxMqum7Mbqe4kk5lgb
X6gWd0Izg2nGdAEe/97DClO6VpKcPbpDAkBTR/JOJN1fvXMxXJaf13XxakrQMr+R
Yr6LlSInykyAz8lJvlLP7A+5QbHgN9NF/wh+GXqpxPwA3ukqdSqhjhWBAkBn6mDv
HPgR5xrzL6XM8y9TgaOlJAdK6HtYp6d/UOmN0+Butf6JUq07TphRT5tXNJVgemch
O5x/9UKfbrc+KyzbAkAo97TfFC+mZhU1N5fFelaRu4ikPxlp642KRUSkOh8GEkNf
jQ97eJWiWtDcsMUhcZgoB5ydHcFlrBIn6oBcpge5
-----END RSA PRIVATE KEY-----`
auth := &GitHubAppAuth{
AppID: 123,
AppPrivateKey: key,
}
jwt, err := createJWTForGitHubApp(auth)
if err != nil {
t.Fatal(err)
}
fmt.Println(jwt)
}
+14
View File
@@ -0,0 +1,14 @@
package actions
import (
"context"
"io"
)
//go:generate mockery --inpackage --name=SessionService
type SessionService interface {
GetMessage(ctx context.Context, lastMessageId int64) (*RunnerScaleSetMessage, error)
DeleteMessage(ctx context.Context, messageId int64) error
AcquireJobs(ctx context.Context, requestIds []int64) ([]int64, error)
io.Closer
}
+153
View File
@@ -0,0 +1,153 @@
package actions
import (
"time"
"github.com/google/uuid"
)
type AcquirableJobList struct {
Count int `json:"count"`
Jobs []AcquirableJob `json:"value"`
}
type AcquirableJob struct {
AcquireJobUrl string `json:"acquireJobUrl"`
MessageType string `json:"messageType"`
RunnerRequestId int64 `json:"runnerRequestId"`
RepositoryName string `json:"repositoryName"`
OwnerName string `json:"ownerName"`
JobWorkflowRef string `json:"jobWorkflowRef"`
EventName string `json:"eventName"`
RequestLabels []string `json:"requestLabels"`
}
type Int64List struct {
Count int `json:"count"`
Value []int64 `json:"value"`
}
type JobAvailable struct {
AcquireJobUrl string `json:"acquireJobUrl"`
JobMessageBase
}
type JobAssigned struct {
JobMessageBase
}
type JobStarted struct {
RunnerId int `json:"runnerId"`
RunnerName string `json:"runnerName"`
JobMessageBase
}
type JobCompleted struct {
Result string `json:"result"`
RunnerId int `json:"runnerId"`
RunnerName string `json:"runnerName"`
JobMessageBase
}
type JobMessageType struct {
MessageType string `json:"messageType"`
}
type JobMessageBase struct {
JobMessageType
RunnerRequestId int64 `json:"runnerRequestId"`
RepositoryName string `json:"repositoryName"`
OwnerName string `json:"ownerName"`
JobWorkflowRef string `json:"jobWorkflowRef"`
JobDisplayName string `json:"jobDisplayName"`
WorkflowRunId int64 `json:"workflowRunId"`
EventName string `json:"eventName"`
RequestLabels []string `json:"requestLabels"`
}
type Label struct {
Type string `json:"type"`
Name string `json:"name"`
}
type RunnerGroup struct {
ID int64 `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
IsDefault bool `json:"isDefaultGroup"`
}
type RunnerGroupList struct {
Count int `json:"count"`
RunnerGroups []RunnerGroup `json:"value"`
}
type RunnerScaleSet struct {
Id int `json:"id,omitempty"`
Name string `json:"name,omitempty"`
RunnerGroupId int `json:"runnerGroupId,omitempty"`
RunnerGroupName string `json:"runnerGroupName,omitempty"`
Labels []Label `json:"labels,omitempty"`
RunnerSetting RunnerSetting `json:"RunnerSetting,omitempty"`
CreatedOn time.Time `json:"createdOn,omitempty"`
RunnerJitConfigUrl string `json:"runnerJitConfigUrl,omitempty"`
Statistics *RunnerScaleSetStatistic `json:"statistics,omitempty"`
}
type RunnerScaleSetJitRunnerSetting struct {
Name string `json:"name"`
WorkFolder string `json:"workFolder"`
}
type RunnerScaleSetMessage struct {
MessageId int64 `json:"messageId"`
MessageType string `json:"messageType"`
Body string `json:"body"`
Statistics *RunnerScaleSetStatistic `json:"statistics"`
}
type runnerScaleSetsResponse struct {
Count int `json:"count"`
RunnerScaleSets []RunnerScaleSet `json:"value"`
}
type RunnerScaleSetSession struct {
SessionId *uuid.UUID `json:"sessionId,omitempty"`
OwnerName string `json:"ownerName,omitempty"`
RunnerScaleSet *RunnerScaleSet `json:"runnerScaleSet,omitempty"`
MessageQueueUrl string `json:"messageQueueUrl,omitempty"`
MessageQueueAccessToken string `json:"messageQueueAccessToken,omitempty"`
Statistics *RunnerScaleSetStatistic `json:"statistics,omitempty"`
}
type RunnerScaleSetStatistic struct {
TotalAvailableJobs int `json:"totalAvailableJobs"`
TotalAcquiredJobs int `json:"totalAcquiredJobs"`
TotalAssignedJobs int `json:"totalAssignedJobs"`
TotalRunningJobs int `json:"totalRunningJobs"`
TotalRegisteredRunners int `json:"totalRegisteredRunners"`
TotalBusyRunners int `json:"totalBusyRunners"`
TotalIdleRunners int `json:"totalIdleRunners"`
}
type RunnerSetting struct {
Ephemeral bool `json:"ephemeral,omitempty"`
IsElastic bool `json:"isElastic,omitempty"`
DisableUpdate bool `json:"disableUpdate,omitempty"`
}
type RunnerReferenceList struct {
Count int `json:"count"`
RunnerReferences []RunnerReference `json:"value"`
}
type RunnerReference struct {
Id int `json:"id"`
Name string `json:"name"`
RunnerScaleSetId int `json:"runnerScaleSetId"`
}
type RunnerScaleSetJitRunnerConfig struct {
Runner *RunnerReference `json:"runner"`
EncodedJITConfig string `json:"encodedJITConfig"`
}