Initial commit containing ARC github/actions client
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
name: Go
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
# This will make sure we only apply the concurrency limits on pull requests
|
||||||
|
# but not pushes to master branch by making the concurrency group name unique
|
||||||
|
# for pushes
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
fmt:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: "go.mod"
|
||||||
|
cache: false
|
||||||
|
- name: fmt
|
||||||
|
run: go fmt ./...
|
||||||
|
- name: Check diff
|
||||||
|
run: git diff --exit-code
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: "go.mod"
|
||||||
|
cache: false
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9
|
||||||
|
with:
|
||||||
|
only-new-issues: true
|
||||||
|
version: v2.5.0
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: "go.mod"
|
||||||
|
cache: false
|
||||||
|
- name: Run tests
|
||||||
|
run: go test ./...
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
version: "2"
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
linters:
|
||||||
|
settings:
|
||||||
|
errcheck:
|
||||||
|
exclude-functions:
|
||||||
|
- (net/http.ResponseWriter).Write
|
||||||
|
exclusions:
|
||||||
|
presets:
|
||||||
|
- std-error-handling
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package scaleset_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newActionsServer returns a new httptest.Server that handles the
|
||||||
|
// authentication requests neeeded to create a new client. Any requests not
|
||||||
|
// made to the /actions/runners/registration-token or
|
||||||
|
// /actions/runner-registration endpoints will be handled by the provided
|
||||||
|
// handler. The returned server is started and will be automatically closed
|
||||||
|
// when the test ends.
|
||||||
|
func newActionsServer(t *testing.T, handler http.Handler, options ...actionsServerOption) *actionsServer {
|
||||||
|
s := httptest.NewServer(nil)
|
||||||
|
server := &actionsServer{
|
||||||
|
Server: s,
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
server.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
option(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// handle getRunnerRegistrationToken
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/runners/registration-token") {
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
w.Write([]byte(`{"token":"token"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle getActionsServiceAdminConnection
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/actions/runner-registration") {
|
||||||
|
if server.token == "" {
|
||||||
|
server.token = defaultActionsToken(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(`{"url":"` + s.URL + `/tenant/123/","token":"` + server.token + `"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
server.Config.Handler = h
|
||||||
|
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionsServerOption func(*actionsServer)
|
||||||
|
|
||||||
|
type actionsServer struct {
|
||||||
|
*httptest.Server
|
||||||
|
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionsServer) configURLForOrg(org string) string {
|
||||||
|
return s.URL + "/" + org
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultActionsToken(t *testing.T) string {
|
||||||
|
claims := &jwt.RegisteredClaims{
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute)),
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)),
|
||||||
|
Issuer: "123",
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||||
|
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(samplePrivateKey))
|
||||||
|
require.NoError(t, err)
|
||||||
|
tokenString, err := token.SignedString(privateKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return tokenString
|
||||||
|
}
|
||||||
|
|
||||||
|
const samplePrivateKey = `-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQC7tgquvNIp+Ik3
|
||||||
|
rRVZ9r0zJLsSzTHqr2dA6EUUmpRiQ25MzjMqKqu0OBwvh/pZyfjSIkKrhIridNK4
|
||||||
|
DWnPfPWHE2K3Muh0X2sClxtqiiFmXsvbiTzhUm5a+zCcv0pJCWYnKi0HmyXpAXjJ
|
||||||
|
iN8mWliZN896verVYXWrod7EaAnuST4TiJeqZYW4bBBG81fPNc/UP4j6CKAW8nx9
|
||||||
|
HtcX6ApvlHeCLZUTW/qhGLO0nLKoEOr3tXCPW5VjKzlm134Dl+8PN6f1wv6wMAoA
|
||||||
|
lo7Ha5+c74jhPL6gHXg7cRaHQmuJCJrtl8qbLkFAulfkBixBw/6i11xoM/MOC64l
|
||||||
|
TWmXqrxTAgMBAAECgf9zYlxfL+rdHRXCoOm7pUeSPL0dWaPFP12d/Z9LSlDAt/h6
|
||||||
|
Pd+eqYEwhf795SAbJuzNp51Ls6LUGnzmLOdojKwfqJ51ahT1qbcBcMZNOcvtGqZ9
|
||||||
|
xwLG993oyR49C361Lf2r8mKrdrR5/fW0B1+1s6A+eRFivqFOtsOc4V4iMeHYsCVJ
|
||||||
|
hM7yMu0UfpolDJA/CzopsoGq3UuQlibUEUxKULza06aDjg/gBH3PnP+fQ1m0ovDY
|
||||||
|
h0pX6SCq5fXVJFS+Pbpu7j2ePNm3mr0qQhrUONZq0qhGN/piCbBZe1CqWApyO7nA
|
||||||
|
B95VChhL1eYs1BKvQePh12ap83woIUcW2mJF2F0CgYEA+aERTuKWEm+zVNKS9t3V
|
||||||
|
qNhecCOpayKM9OlALIK/9W6KBS+pDsjQQteQAUAItjvLiDjd5KsrtSgjbSgr66IP
|
||||||
|
b615Pakywe5sdnVGzSv+07KMzuFob9Hj6Xv9als9Y2geVhUZB2Frqve/UCjmC56i
|
||||||
|
zuQTSele5QKCSSTFBV3423cCgYEAwIBv9ChsI+mse6vPaqSPpZ2n237anThMcP33
|
||||||
|
aS0luYXqMWXZ0TQ/uSmCElY4G3xqNo8szzfy6u0HpldeUsEUsIcBNUV5kIIb8wKu
|
||||||
|
Zmgcc8gBIjJkyUJI4wuz9G/fegEUj3u6Cttmmj4iWLzCRscRJdfGpqwRIhOGyXb9
|
||||||
|
2Rur5QUCgYAGWIPaH4R1H4XNiDTYNbdyvV1ZOG7cHFq89xj8iK5cjNzRWO7RQ2WX
|
||||||
|
7WbpwTj3ePmpktiBMaDA0C5mXfkP2mTOD/jfCmgR6f+z2zNbj9zAgO93at9+yDUl
|
||||||
|
AFPm2j7rQgBTa+HhACb+h6HDZebDMNsuqzmaTWZuJ+wr89VWV5c17QKBgH3jwNNQ
|
||||||
|
mCAIUidynaulQNfTOZIe7IMC7WK7g9CBmPkx7Y0uiXr6C25hCdJKFllLTP6vNWOy
|
||||||
|
uCcQqf8LhgDiilBDifO3op9xpyuOJlWMYocJVkxx3l2L/rSU07PYcbKNAFAxXuJ4
|
||||||
|
xym51qZnkznMN5ei/CPFxVKeqHgaXDpekVStAoGAV3pSWAKDXY/42XEHixrCTqLW
|
||||||
|
kBxfaf3g7iFnl3u8+7Z/7Cb4ZqFcw0bRJseKuR9mFvBhcZxSErbMDEYrevefU9aM
|
||||||
|
APeCxEyw6hJXgbWKoG7Fw2g2HP3ytCJ4YzH0zNitHjk/1h4BG7z8cEQILCSv5mN2
|
||||||
|
etFcaQuTHEZyRhhJ4BU=
|
||||||
|
-----END PRIVATE KEY-----`
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package scaleset_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/actions/scaleset"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClient_Do(t *testing.T) {
|
||||||
|
t.Run("trims byte order mark from response if present", func(t *testing.T) {
|
||||||
|
t.Run("when there is no body", func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient("https://localhost/org/repo", &scaleset.ActionsAuth{Token: "token"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", server.URL, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, string(body))
|
||||||
|
})
|
||||||
|
|
||||||
|
responses := []string{
|
||||||
|
"\xef\xbb\xbf{\"foo\":\"bar\"}",
|
||||||
|
"{\"foo\":\"bar\"}",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, response := range responses {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write([]byte(response))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient("https://localhost/org/repo", &scaleset.ActionsAuth{Token: "token"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", server.URL, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "{\"foo\":\"bar\"}", string(body))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package scaleset_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/scaleset"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateJitRunnerConfig(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Get JIT Config for Runner", func(t *testing.T) {
|
||||||
|
want := &scaleset.RunnerScaleSetJitRunnerConfig{}
|
||||||
|
response := []byte(`{"count":1,"value":[{"id":1,"name":"scale-set-name"}]}`)
|
||||||
|
|
||||||
|
runnerSettings := &scaleset.RunnerScaleSetJitRunnerSetting{}
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.GenerateJitRunnerConfig(ctx, runnerSettings, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
runnerSettings := &scaleset.RunnerScaleSetJitRunnerSetting{}
|
||||||
|
|
||||||
|
retryMax := 1
|
||||||
|
actualRetry := 0
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(
|
||||||
|
server.configURLForOrg("my-org"),
|
||||||
|
auth,
|
||||||
|
scaleset.WithRetryMax(1),
|
||||||
|
scaleset.WithRetryWaitMax(1*time.Millisecond),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GenerateJitRunnerConfig(ctx, runnerSettings, 1)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package scaleset_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/scaleset"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAcquireJobs(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Acquire Job", func(t *testing.T) {
|
||||||
|
want := []int64{1}
|
||||||
|
response := []byte(`{"value": [1]}`)
|
||||||
|
|
||||||
|
session := &scaleset.RunnerScaleSetSession{
|
||||||
|
RunnerScaleSet: &scaleset.RunnerScaleSet{Id: 1},
|
||||||
|
MessageQueueAccessToken: "abc",
|
||||||
|
}
|
||||||
|
requestIDs := want
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/acquirablejobs") {
|
||||||
|
w.Write([]byte(`{"count": 1}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetAcquirableJobs(ctx, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.AcquireJobs(ctx, session.RunnerScaleSet.Id, session.MessageQueueAccessToken, requestIDs)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
session := &scaleset.RunnerScaleSetSession{
|
||||||
|
RunnerScaleSet: &scaleset.RunnerScaleSet{Id: 1},
|
||||||
|
MessageQueueAccessToken: "abc",
|
||||||
|
}
|
||||||
|
var requestIDs = []int64{1}
|
||||||
|
|
||||||
|
retryMax := 1
|
||||||
|
actualRetry := 0
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/acquirablejobs") {
|
||||||
|
w.Write([]byte(`{"count": 1}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(
|
||||||
|
server.configURLForOrg("my-org"),
|
||||||
|
auth,
|
||||||
|
scaleset.WithRetryMax(retryMax),
|
||||||
|
scaleset.WithRetryWaitMax(1*time.Millisecond),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetAcquirableJobs(ctx, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.AcquireJobs(context.Background(), session.RunnerScaleSet.Id, session.MessageQueueAccessToken, requestIDs)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should return MessageQueueTokenExpiredError when http error is not Unauthorized", func(t *testing.T) {
|
||||||
|
want := []int64{1}
|
||||||
|
|
||||||
|
session := &scaleset.RunnerScaleSetSession{
|
||||||
|
RunnerScaleSet: &scaleset.RunnerScaleSet{Id: 1},
|
||||||
|
MessageQueueAccessToken: "abc",
|
||||||
|
}
|
||||||
|
requestIDs := want
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/acquirablejobs") {
|
||||||
|
w.Write([]byte(`{"count": 1}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
http.Error(w, "Session expired", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetAcquirableJobs(ctx, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.AcquireJobs(ctx, session.RunnerScaleSet.Id, session.MessageQueueAccessToken, requestIDs)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, got)
|
||||||
|
var expectedErr *scaleset.MessageQueueTokenExpiredError
|
||||||
|
assert.True(t, errors.As(err, &expectedErr))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAcquirableJobs(t *testing.T) {
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Acquire Job", func(t *testing.T) {
|
||||||
|
want := &scaleset.AcquirableJobList{}
|
||||||
|
response := []byte(`{"count": 0}`)
|
||||||
|
|
||||||
|
runnerScaleSet := &scaleset.RunnerScaleSet{Id: 1}
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.GetAcquirableJobs(context.Background(), runnerScaleSet.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
runnerScaleSet := &scaleset.RunnerScaleSet{Id: 1}
|
||||||
|
|
||||||
|
retryMax := 1
|
||||||
|
|
||||||
|
actualRetry := 0
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(
|
||||||
|
server.configURLForOrg("my-org"),
|
||||||
|
auth,
|
||||||
|
scaleset.WithRetryMax(retryMax),
|
||||||
|
scaleset.WithRetryWaitMax(1*time.Millisecond),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetAcquirableJobs(context.Background(), runnerScaleSet.Id)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package scaleset_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/actions/scaleset"
|
||||||
|
"github.com/actions/scaleset/testserver"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/net/http/httpproxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientProxy(t *testing.T) {
|
||||||
|
serverCalled := false
|
||||||
|
|
||||||
|
proxy := testserver.New(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
serverCalled = true
|
||||||
|
}))
|
||||||
|
|
||||||
|
proxyConfig := &httpproxy.Config{
|
||||||
|
HTTPProxy: proxy.URL,
|
||||||
|
}
|
||||||
|
proxyFunc := func(req *http.Request) (*url.URL, error) {
|
||||||
|
return proxyConfig.ProxyFunc()(req.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := scaleset.NewClient("http://github.com/org/repo", nil, scaleset.WithProxy(proxyFunc))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = c.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, serverCalled)
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package scaleset_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/scaleset"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetMessage(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
runnerScaleSetMessage := &scaleset.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 := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(s.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.GetMessage(ctx, s.URL, token, 0, 10)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetMessage sets the last message id if not 0", func(t *testing.T) {
|
||||||
|
want := runnerScaleSetMessage
|
||||||
|
response := []byte(`{"messageId":1,"messageType":"rssType"}`)
|
||||||
|
s := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
q := r.URL.Query()
|
||||||
|
assert.Equal(t, "1", q.Get("lastMessageId"))
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(s.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.GetMessage(ctx, s.URL, token, 1, 10)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
retryMax := 1
|
||||||
|
|
||||||
|
actualRetry := 0
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(
|
||||||
|
server.configURLForOrg("my-org"),
|
||||||
|
auth,
|
||||||
|
scaleset.WithRetryMax(retryMax),
|
||||||
|
scaleset.WithRetryWaitMax(1*time.Millisecond),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetMessage(ctx, server.URL, token, 0, 10)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
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) {
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetMessage(ctx, server.URL, token, 0, 10)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
var expectedErr *scaleset.MessageQueueTokenExpiredError
|
||||||
|
require.True(t, errors.As(err, &expectedErr))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Status code not found", func(t *testing.T) {
|
||||||
|
want := scaleset.ActionsError{
|
||||||
|
Err: errors.New("unknown exception"),
|
||||||
|
StatusCode: 404,
|
||||||
|
}
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetMessage(ctx, server.URL, token, 0, 10)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
assert.Equal(t, want.Error(), err.Error())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetMessage(ctx, server.URL, token, 0, 10)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Capacity error handling", func(t *testing.T) {
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
hc := r.Header.Get(scaleset.HeaderScaleSetMaxCapacity)
|
||||||
|
c, err := strconv.Atoi(hc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.GreaterOrEqual(t, c, 0)
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetMessage(ctx, server.URL, token, 0, -1)
|
||||||
|
require.Error(t, err)
|
||||||
|
// Ensure we don't send requests with negative capacity
|
||||||
|
assert.False(t, errors.Is(err, &scaleset.ActionsError{}))
|
||||||
|
|
||||||
|
_, err = client.GetMessage(ctx, server.URL, token, 0, 0)
|
||||||
|
assert.Error(t, err)
|
||||||
|
var expectedErr *scaleset.ActionsError
|
||||||
|
assert.ErrorAs(t, err, &expectedErr)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, expectedErr.StatusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteMessage(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
|
||||||
|
runnerScaleSetMessage := &scaleset.RunnerScaleSetMessage{
|
||||||
|
MessageId: 1,
|
||||||
|
MessageType: "rssType",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Delete existing message", func(t *testing.T) {
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = client.DeleteMessage(ctx, server.URL, token, runnerScaleSetMessage.MessageId)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Message token expired", func(t *testing.T) {
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = client.DeleteMessage(ctx, server.URL, token, 0)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
var expectedErr *scaleset.MessageQueueTokenExpiredError
|
||||||
|
assert.True(t, errors.As(err, &expectedErr))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = client.DeleteMessage(ctx, server.URL, token, runnerScaleSetMessage.MessageId)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
var expectedErr *scaleset.ActionsError
|
||||||
|
assert.True(t, errors.As(err, &expectedErr))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
actualRetry := 0
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
|
||||||
|
retryMax := 1
|
||||||
|
client, err := scaleset.NewClient(
|
||||||
|
server.configURLForOrg("my-org"),
|
||||||
|
auth,
|
||||||
|
scaleset.WithRetryMax(retryMax),
|
||||||
|
scaleset.WithRetryWaitMax(1*time.Nanosecond),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = client.DeleteMessage(ctx, server.URL, token, runnerScaleSetMessage.MessageId)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
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 := (*scaleset.RunnerScaleSetMessage)(nil)
|
||||||
|
rsl, err := json.Marshal(want)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = client.DeleteMessage(ctx, server.URL, token, runnerScaleSetMessage.MessageId+1)
|
||||||
|
var expectedErr *scaleset.ActionsError
|
||||||
|
require.True(t, errors.As(err, &expectedErr))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
package scaleset_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/scaleset"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const exampleRequestID = "5ddf2050-dae0-013c-9159-04421ad31b68"
|
||||||
|
|
||||||
|
func TestCreateMessageSession(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("CreateMessageSession unmarshals correctly", func(t *testing.T) {
|
||||||
|
owner := "foo"
|
||||||
|
runnerScaleSet := scaleset.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
Name: "ScaleSet",
|
||||||
|
CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
RunnerSetting: scaleset.RunnerSetting{},
|
||||||
|
}
|
||||||
|
|
||||||
|
want := &scaleset.RunnerScaleSetSession{
|
||||||
|
OwnerName: "foo",
|
||||||
|
RunnerScaleSet: &scaleset.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
Name: "ScaleSet",
|
||||||
|
},
|
||||||
|
MessageQueueUrl: "http://fake.scaleset.github.com/123",
|
||||||
|
MessageQueueAccessToken: "fake.jwt.here",
|
||||||
|
}
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
resp := []byte(`{
|
||||||
|
"ownerName": "foo",
|
||||||
|
"runnerScaleSet": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "ScaleSet"
|
||||||
|
},
|
||||||
|
"messageQueueUrl": "http://fake.scaleset.github.com/123",
|
||||||
|
"messageQueueAccessToken": "fake.jwt.here"
|
||||||
|
}`)
|
||||||
|
w.Write(resp)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.CreateMessageSession(ctx, runnerScaleSet.Id, owner)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CreateMessageSession unmarshals errors into ActionsError", func(t *testing.T) {
|
||||||
|
owner := "foo"
|
||||||
|
runnerScaleSet := scaleset.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
Name: "ScaleSet",
|
||||||
|
CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
RunnerSetting: scaleset.RunnerSetting{},
|
||||||
|
}
|
||||||
|
|
||||||
|
want := &scaleset.ActionsError{
|
||||||
|
ActivityID: exampleRequestID,
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
Err: &scaleset.ActionsExceptionError{
|
||||||
|
ExceptionName: "CSharpExceptionNameHere",
|
||||||
|
Message: "could not do something",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set(scaleset.HeaderActionsActivityID, exampleRequestID)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
resp := []byte(`{"typeName": "CSharpExceptionNameHere","message": "could not do something"}`)
|
||||||
|
w.Write(resp)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.CreateMessageSession(ctx, runnerScaleSet.Id, owner)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
errorTypeForComparison := &scaleset.ActionsError{}
|
||||||
|
assert.True(
|
||||||
|
t,
|
||||||
|
errors.As(err, &errorTypeForComparison),
|
||||||
|
"CreateMessageSession expected to be able to parse the error into ActionsError type: %v",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Equal(t, want, errorTypeForComparison)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CreateMessageSession call is retried the correct amount of times", func(t *testing.T) {
|
||||||
|
owner := "foo"
|
||||||
|
runnerScaleSet := scaleset.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
Name: "ScaleSet",
|
||||||
|
CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
RunnerSetting: scaleset.RunnerSetting{},
|
||||||
|
}
|
||||||
|
|
||||||
|
gotRetries := 0
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
gotRetries++
|
||||||
|
}))
|
||||||
|
|
||||||
|
retryMax := 3
|
||||||
|
retryWaitMax := 1 * time.Microsecond
|
||||||
|
|
||||||
|
wantRetries := retryMax + 1
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(
|
||||||
|
server.configURLForOrg("my-org"),
|
||||||
|
auth,
|
||||||
|
scaleset.WithRetryMax(retryMax),
|
||||||
|
scaleset.WithRetryWaitMax(retryWaitMax),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.CreateMessageSession(ctx, runnerScaleSet.Id, owner)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Equalf(t, gotRetries, wantRetries, "CreateMessageSession got unexpected retry count: got=%v, want=%v", gotRetries, wantRetries)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteMessageSession(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("DeleteMessageSession call is retried the correct amount of times", func(t *testing.T) {
|
||||||
|
runnerScaleSet := scaleset.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
Name: "ScaleSet",
|
||||||
|
CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
RunnerSetting: scaleset.RunnerSetting{},
|
||||||
|
}
|
||||||
|
|
||||||
|
gotRetries := 0
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
gotRetries++
|
||||||
|
}))
|
||||||
|
|
||||||
|
retryMax := 3
|
||||||
|
retryWaitMax := 1 * time.Microsecond
|
||||||
|
|
||||||
|
wantRetries := retryMax + 1
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(
|
||||||
|
server.configURLForOrg("my-org"),
|
||||||
|
auth,
|
||||||
|
scaleset.WithRetryMax(retryMax),
|
||||||
|
scaleset.WithRetryWaitMax(retryWaitMax),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sessionId := uuid.New()
|
||||||
|
|
||||||
|
err = client.DeleteMessageSession(ctx, runnerScaleSet.Id, &sessionId)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Equalf(t, gotRetries, wantRetries, "CreateMessageSession got unexpected retry count: got=%v, want=%v", gotRetries, wantRetries)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshMessageSession(t *testing.T) {
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("RefreshMessageSession call is retried the correct amount of times", func(t *testing.T) {
|
||||||
|
runnerScaleSet := scaleset.RunnerScaleSet{
|
||||||
|
Id: 1,
|
||||||
|
Name: "ScaleSet",
|
||||||
|
CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
RunnerSetting: scaleset.RunnerSetting{},
|
||||||
|
}
|
||||||
|
|
||||||
|
gotRetries := 0
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
gotRetries++
|
||||||
|
}))
|
||||||
|
|
||||||
|
retryMax := 3
|
||||||
|
retryWaitMax := 1 * time.Microsecond
|
||||||
|
|
||||||
|
wantRetries := retryMax + 1
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(
|
||||||
|
server.configURLForOrg("my-org"),
|
||||||
|
auth,
|
||||||
|
scaleset.WithRetryMax(retryMax),
|
||||||
|
scaleset.WithRetryWaitMax(retryWaitMax),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sessionId := uuid.New()
|
||||||
|
|
||||||
|
_, err = client.RefreshMessageSession(context.Background(), runnerScaleSet.Id, &sessionId)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Equalf(t, gotRetries, wantRetries, "CreateMessageSession got unexpected retry count: got=%v, want=%v", gotRetries, wantRetries)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
package scaleset_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/scaleset"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetRunnerScaleSet(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleSetName := "ScaleSet"
|
||||||
|
runnerScaleSet := scaleset.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"}]}`)
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(runnerScaleSetsResp)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.GetRunnerScaleSet(ctx, 1, scaleSetName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetRunnerScaleSet calls correct url", func(t *testing.T) {
|
||||||
|
runnerScaleSetsResp := []byte(`{"count":1,"value":[{"id":1,"name":"ScaleSet"}]}`)
|
||||||
|
url := url.URL{}
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(runnerScaleSetsResp)
|
||||||
|
url = *r.URL
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedPath := "/tenant/123/_apis/runtime/runnerscalesets"
|
||||||
|
assert.Equal(t, expectedPath, url.Path)
|
||||||
|
assert.Equal(t, scaleSetName, url.Query().Get("name"))
|
||||||
|
assert.Equal(t, "6.0-preview", url.Query().Get("api-version"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Status code not found", func(t *testing.T) {
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
actualRetry := 0
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
|
||||||
|
retryMax := 1
|
||||||
|
retryWaitMax := 1 * time.Microsecond
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(
|
||||||
|
server.configURLForOrg("my-org"),
|
||||||
|
auth,
|
||||||
|
scaleset.WithRetryMax(retryMax),
|
||||||
|
scaleset.WithRetryWaitMax(retryWaitMax),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
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 := (*scaleset.RunnerScaleSet)(nil)
|
||||||
|
runnerScaleSetsResp := []byte(`{"count":0,"value":[{"id":1,"name":"ScaleSet"}]}`)
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(runnerScaleSetsResp)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.GetRunnerScaleSet(ctx, 1, scaleSetName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Multiple runner scale sets found", func(t *testing.T) {
|
||||||
|
reqID := uuid.NewString()
|
||||||
|
wantErr := &scaleset.ActionsError{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
ActivityID: reqID,
|
||||||
|
Err: fmt.Errorf("multiple runner scale sets found with name %q", scaleSetName),
|
||||||
|
}
|
||||||
|
runnerScaleSetsResp := []byte(`{"count":2,"value":[{"id":1,"name":"ScaleSet"}]}`)
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Header().Set(scaleset.HeaderActionsActivityID, reqID)
|
||||||
|
w.Write(runnerScaleSetsResp)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
assert.Equal(t, wantErr.Error(), err.Error())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRunnerScaleSetById(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
runnerScaleSet := scaleset.RunnerScaleSet{Id: 1, Name: "ScaleSet", CreatedOn: scaleSetCreationDateTime, RunnerSetting: scaleset.RunnerSetting{}}
|
||||||
|
|
||||||
|
t.Run("Get existing scale set by Id", func(t *testing.T) {
|
||||||
|
want := &runnerScaleSet
|
||||||
|
rsl, err := json.Marshal(want)
|
||||||
|
require.NoError(t, err)
|
||||||
|
sservere := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(sservere.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.GetRunnerScaleSetById(ctx, runnerScaleSet.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetRunnerScaleSetById calls correct url", func(t *testing.T) {
|
||||||
|
rsl, err := json.Marshal(&runnerScaleSet)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
url := url.URL{}
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
url = *r.URL
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetRunnerScaleSetById(ctx, runnerScaleSet.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedPath := fmt.Sprintf("/tenant/123/_apis/runtime/runnerscalesets/%d", runnerScaleSet.Id)
|
||||||
|
assert.Equal(t, expectedPath, url.Path)
|
||||||
|
assert.Equal(t, "6.0-preview", url.Query().Get("api-version"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Status code not found", func(t *testing.T) {
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetRunnerScaleSetById(ctx, runnerScaleSet.Id)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetRunnerScaleSetById(ctx, runnerScaleSet.Id)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
actualRetry := 0
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
|
||||||
|
retryMax := 1
|
||||||
|
retryWaitMax := 1 * time.Microsecond
|
||||||
|
client, err := scaleset.NewClient(
|
||||||
|
server.configURLForOrg("my-org"),
|
||||||
|
auth,
|
||||||
|
scaleset.WithRetryMax(retryMax),
|
||||||
|
scaleset.WithRetryWaitMax(retryWaitMax),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetRunnerScaleSetById(ctx, runnerScaleSet.Id)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
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 := (*scaleset.RunnerScaleSet)(nil)
|
||||||
|
rsl, err := json.Marshal(want)
|
||||||
|
require.NoError(t, err)
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.GetRunnerScaleSetById(ctx, runnerScaleSet.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateRunnerScaleSet(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
runnerScaleSet := scaleset.RunnerScaleSet{Id: 1, Name: "ScaleSet", CreatedOn: scaleSetCreationDateTime, RunnerSetting: scaleset.RunnerSetting{}}
|
||||||
|
|
||||||
|
t.Run("Create runner scale set", func(t *testing.T) {
|
||||||
|
want := &runnerScaleSet
|
||||||
|
rsl, err := json.Marshal(want)
|
||||||
|
require.NoError(t, err)
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.CreateRunnerScaleSet(ctx, &runnerScaleSet)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CreateRunnerScaleSet calls correct url", func(t *testing.T) {
|
||||||
|
rsl, err := json.Marshal(&runnerScaleSet)
|
||||||
|
require.NoError(t, err)
|
||||||
|
url := url.URL{}
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
url = *r.URL
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.CreateRunnerScaleSet(ctx, &runnerScaleSet)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedPath := "/tenant/123/_apis/runtime/runnerscalesets"
|
||||||
|
assert.Equal(t, expectedPath, url.Path)
|
||||||
|
assert.Equal(t, "6.0-preview", url.Query().Get("api-version"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.CreateRunnerScaleSet(ctx, &runnerScaleSet)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
var expectedErr *scaleset.ActionsError
|
||||||
|
assert.True(t, errors.As(err, &expectedErr))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
actualRetry := 0
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
|
||||||
|
retryMax := 1
|
||||||
|
retryWaitMax := 1 * time.Microsecond
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(
|
||||||
|
server.configURLForOrg("my-org"),
|
||||||
|
auth,
|
||||||
|
scaleset.WithRetryMax(retryMax),
|
||||||
|
scaleset.WithRetryWaitMax(retryWaitMax),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.CreateRunnerScaleSet(ctx, &runnerScaleSet)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
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) {
|
||||||
|
ctx := context.Background()
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
runnerScaleSet := scaleset.RunnerScaleSet{Id: 1, Name: "ScaleSet", RunnerGroupId: 1, RunnerGroupName: "group", CreatedOn: scaleSetCreationDateTime, RunnerSetting: scaleset.RunnerSetting{}}
|
||||||
|
|
||||||
|
t.Run("Update runner scale set", func(t *testing.T) {
|
||||||
|
want := &runnerScaleSet
|
||||||
|
rsl, err := json.Marshal(want)
|
||||||
|
require.NoError(t, err)
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Write(rsl)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.UpdateRunnerScaleSet(ctx, 1, &scaleset.RunnerScaleSet{RunnerGroupId: 1})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateRunnerScaleSet calls correct url", func(t *testing.T) {
|
||||||
|
rsl, err := json.Marshal(&runnerScaleSet)
|
||||||
|
require.NoError(t, err)
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
expectedPath := "/tenant/123/_apis/runtime/runnerscalesets/1"
|
||||||
|
assert.Equal(t, expectedPath, r.URL.Path)
|
||||||
|
assert.Equal(t, http.MethodPatch, r.Method)
|
||||||
|
assert.Equal(t, "6.0-preview", r.URL.Query().Get("api-version"))
|
||||||
|
|
||||||
|
w.Write(rsl)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.UpdateRunnerScaleSet(ctx, 1, &runnerScaleSet)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRunnerScaleSet(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Delete runner scale set", func(t *testing.T) {
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "DELETE", r.Method)
|
||||||
|
assert.Contains(t, r.URL.String(), "/_apis/runtime/runnerscalesets/10?api-version=6.0-preview")
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = client.DeleteRunnerScaleSet(ctx, 10)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Delete calls with error", func(t *testing.T) {
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "DELETE", r.Method)
|
||||||
|
assert.Contains(t, r.URL.String(), "/_apis/runtime/runnerscalesets/10?api-version=6.0-preview")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Write([]byte(`{"message": "test error"}`))
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = client.DeleteRunnerScaleSet(ctx, 10)
|
||||||
|
assert.ErrorContains(t, err, "test error")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
package scaleset_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/scaleset"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetRunner(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Get Runner", func(t *testing.T) {
|
||||||
|
var runnerID int64 = 1
|
||||||
|
want := &scaleset.RunnerReference{
|
||||||
|
Id: int(runnerID),
|
||||||
|
Name: "self-hosted-ubuntu",
|
||||||
|
}
|
||||||
|
response := []byte(`{"id": 1, "name": "self-hosted-ubuntu"}`)
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.GetRunner(ctx, runnerID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
var runnerID int64 = 1
|
||||||
|
retryWaitMax := 1 * time.Millisecond
|
||||||
|
retryMax := 1
|
||||||
|
|
||||||
|
actualRetry := 0
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth, scaleset.WithRetryMax(retryMax), scaleset.WithRetryWaitMax(retryWaitMax))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetRunner(ctx, runnerID)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRunnerByName(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Get Runner by Name", func(t *testing.T) {
|
||||||
|
var runnerID int64 = 1
|
||||||
|
var runnerName = "self-hosted-ubuntu"
|
||||||
|
want := &scaleset.RunnerReference{
|
||||||
|
Id: int(runnerID),
|
||||||
|
Name: runnerName,
|
||||||
|
}
|
||||||
|
response := []byte(`{"count": 1, "value": [{"id": 1, "name": "self-hosted-ubuntu"}]}`)
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.GetRunnerByName(ctx, runnerName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Get Runner by name with not exist runner", func(t *testing.T) {
|
||||||
|
var runnerName = "self-hosted-ubuntu"
|
||||||
|
response := []byte(`{"count": 0, "value": []}`)
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.GetRunnerByName(ctx, runnerName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
var runnerName = "self-hosted-ubuntu"
|
||||||
|
|
||||||
|
retryWaitMax := 1 * time.Millisecond
|
||||||
|
retryMax := 1
|
||||||
|
|
||||||
|
actualRetry := 0
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth, scaleset.WithRetryMax(retryMax), scaleset.WithRetryWaitMax(retryWaitMax))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.GetRunnerByName(ctx, runnerName)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRunner(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Delete Runner", func(t *testing.T) {
|
||||||
|
var runnerID int64 = 1
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = client.RemoveRunner(ctx, runnerID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Default retries on server error", func(t *testing.T) {
|
||||||
|
var runnerID int64 = 1
|
||||||
|
|
||||||
|
retryWaitMax := 1 * time.Millisecond
|
||||||
|
retryMax := 1
|
||||||
|
|
||||||
|
actualRetry := 0
|
||||||
|
expectedRetry := retryMax + 1
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
actualRetry++
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(
|
||||||
|
server.configURLForOrg("my-org"),
|
||||||
|
auth,
|
||||||
|
scaleset.WithRetryMax(retryMax),
|
||||||
|
scaleset.WithRetryWaitMax(retryWaitMax),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = client.RemoveRunner(ctx, runnerID)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRunnerGroupByName(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Get RunnerGroup by Name", func(t *testing.T) {
|
||||||
|
var runnerGroupID int64 = 1
|
||||||
|
var runnerGroupName = "test-runner-group"
|
||||||
|
want := &scaleset.RunnerGroup{
|
||||||
|
ID: runnerGroupID,
|
||||||
|
Name: runnerGroupName,
|
||||||
|
}
|
||||||
|
response := []byte(`{"count": 1, "value": [{"id": 1, "name": "test-runner-group"}]}`)
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.GetRunnerGroupByName(ctx, runnerGroupName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Get RunnerGroup by name with not exist runner group", func(t *testing.T) {
|
||||||
|
var runnerGroupName = "test-runner-group"
|
||||||
|
response := []byte(`{"count": 0, "value": []}`)
|
||||||
|
|
||||||
|
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.configURLForOrg("my-org"), auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := client.GetRunnerGroupByName(ctx, runnerGroupName)
|
||||||
|
assert.ErrorContains(t, err, "no runner group found with name")
|
||||||
|
assert.Nil(t, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
package scaleset_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/scaleset"
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServerWithSelfSignedCertificates(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
// this handler is a very very barebones replica of actions api
|
||||||
|
// used during the creation of a a new client
|
||||||
|
var u string
|
||||||
|
h := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// handle get registration token
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/runners/registration-token") {
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
w.Write([]byte(`{"token":"token"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle getActionsServiceAdminConnection
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/actions/runner-registration") {
|
||||||
|
claims := &jwt.RegisteredClaims{
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Minute)),
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Minute)),
|
||||||
|
Issuer: "123",
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||||
|
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(samplePrivateKey))
|
||||||
|
require.NoError(t, err)
|
||||||
|
tokenString, err := token.SignedString(privateKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
w.Write([]byte(`{"url":"` + u + `","token":"` + tokenString + `"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// default happy response for RemoveRunner
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
certPath := filepath.Join("testdata", "server.crt")
|
||||||
|
keyPath := filepath.Join("testdata", "server.key")
|
||||||
|
|
||||||
|
t.Run("client without ca certs", func(t *testing.T) {
|
||||||
|
server := startNewTLSTestServer(t, certPath, keyPath, http.HandlerFunc(h))
|
||||||
|
u = server.URL
|
||||||
|
configURL := server.URL + "/my-org"
|
||||||
|
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
client, err := scaleset.NewClient(configURL, auth)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, client)
|
||||||
|
|
||||||
|
err = client.RemoveRunner(ctx, 1)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
assert.True(t, errors.As(err, &x509.UnknownAuthorityError{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// on macOS we only get an untyped error from the system verifying the
|
||||||
|
// certificate
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
assert.True(t, strings.HasSuffix(err.Error(), "certificate is not trusted"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("client with ca certs", func(t *testing.T) {
|
||||||
|
server := startNewTLSTestServer(
|
||||||
|
t,
|
||||||
|
certPath,
|
||||||
|
keyPath,
|
||||||
|
http.HandlerFunc(h),
|
||||||
|
)
|
||||||
|
u = server.URL
|
||||||
|
configURL := server.URL + "/my-org"
|
||||||
|
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := os.ReadFile(filepath.Join("testdata", "rootCA.crt"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
require.True(t, pool.AppendCertsFromPEM(cert))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(
|
||||||
|
configURL,
|
||||||
|
auth,
|
||||||
|
scaleset.WithRootCAs(pool),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, client)
|
||||||
|
|
||||||
|
err = client.RemoveRunner(ctx, 1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("client with ca chain certs", func(t *testing.T) {
|
||||||
|
server := startNewTLSTestServer(
|
||||||
|
t,
|
||||||
|
filepath.Join("testdata", "leaf.crt"),
|
||||||
|
filepath.Join("testdata", "leaf.key"),
|
||||||
|
http.HandlerFunc(h),
|
||||||
|
)
|
||||||
|
u = server.URL
|
||||||
|
configURL := server.URL + "/my-org"
|
||||||
|
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := os.ReadFile(filepath.Join("testdata", "intermediate.crt"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
require.True(t, pool.AppendCertsFromPEM(cert))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(
|
||||||
|
configURL,
|
||||||
|
auth,
|
||||||
|
scaleset.WithRootCAs(pool),
|
||||||
|
scaleset.WithRetryMax(0),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, client)
|
||||||
|
|
||||||
|
err = client.RemoveRunner(ctx, 1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("client skipping tls verification", func(t *testing.T) {
|
||||||
|
server := startNewTLSTestServer(t, certPath, keyPath, http.HandlerFunc(h))
|
||||||
|
configURL := server.URL + "/my-org"
|
||||||
|
|
||||||
|
auth := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(configURL, auth, scaleset.WithoutTLSVerify())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, client)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func startNewTLSTestServer(t *testing.T, certPath, keyPath string, handler http.Handler) *httptest.Server {
|
||||||
|
server := httptest.NewUnstartedServer(handler)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
server.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
server.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||||
|
server.StartTLS()
|
||||||
|
|
||||||
|
return server
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package scaleset
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrInvalidGitHubConfigURL = fmt.Errorf("invalid config URL, should point to an enterprise, org, or repository")
|
||||||
|
|
||||||
|
type GitHubScope int
|
||||||
|
|
||||||
|
const (
|
||||||
|
GitHubScopeUnknown GitHubScope = iota
|
||||||
|
GitHubScopeEnterprise
|
||||||
|
GitHubScopeOrganization
|
||||||
|
GitHubScopeRepository
|
||||||
|
)
|
||||||
|
|
||||||
|
type GitHubConfig struct {
|
||||||
|
ConfigURL *url.URL
|
||||||
|
Scope GitHubScope
|
||||||
|
|
||||||
|
Enterprise string
|
||||||
|
Organization string
|
||||||
|
Repository string
|
||||||
|
|
||||||
|
IsHosted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseGitHubConfigFromURL(in string) (*GitHubConfig, error) {
|
||||||
|
u, err := url.Parse(strings.Trim(in, "/"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
isHosted := isHostedGitHubURL(u)
|
||||||
|
|
||||||
|
configURL := &GitHubConfig{
|
||||||
|
ConfigURL: u,
|
||||||
|
IsHosted: isHosted,
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidURLError := fmt.Errorf("%q: %w", u.String(), ErrInvalidGitHubConfigURL)
|
||||||
|
|
||||||
|
pathParts := strings.Split(strings.Trim(u.Path, "/"), "/")
|
||||||
|
|
||||||
|
switch len(pathParts) {
|
||||||
|
case 1: // Organization
|
||||||
|
if pathParts[0] == "" {
|
||||||
|
return nil, invalidURLError
|
||||||
|
}
|
||||||
|
|
||||||
|
configURL.Scope = GitHubScopeOrganization
|
||||||
|
configURL.Organization = pathParts[0]
|
||||||
|
|
||||||
|
case 2: // Repository or enterprise
|
||||||
|
if strings.ToLower(pathParts[0]) == "enterprises" {
|
||||||
|
configURL.Scope = GitHubScopeEnterprise
|
||||||
|
configURL.Enterprise = pathParts[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
configURL.Scope = GitHubScopeRepository
|
||||||
|
configURL.Organization = pathParts[0]
|
||||||
|
configURL.Repository = pathParts[1]
|
||||||
|
default:
|
||||||
|
return nil, invalidURLError
|
||||||
|
}
|
||||||
|
|
||||||
|
return configURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GitHubConfig) GitHubAPIURL(path string) *url.URL {
|
||||||
|
result := &url.URL{
|
||||||
|
Scheme: c.ConfigURL.Scheme,
|
||||||
|
Host: c.ConfigURL.Host, // default for Enterprise mode
|
||||||
|
Path: "/api/v3", // default for Enterprise mode
|
||||||
|
}
|
||||||
|
|
||||||
|
isHosted := isHostedGitHubURL(c.ConfigURL)
|
||||||
|
|
||||||
|
if isHosted {
|
||||||
|
result.Host = fmt.Sprintf("api.%s", c.ConfigURL.Host)
|
||||||
|
result.Path = ""
|
||||||
|
|
||||||
|
if strings.EqualFold("www.github.com", c.ConfigURL.Host) {
|
||||||
|
// re-routing www.github.com to api.github.com
|
||||||
|
result.Host = "api.github.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Path += path
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHostedGitHubURL(u *url.URL) bool {
|
||||||
|
_, forceGhes := os.LookupEnv("GITHUB_ACTIONS_FORCE_GHES")
|
||||||
|
if forceGhes {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.EqualFold(u.Host, "github.com") ||
|
||||||
|
strings.EqualFold(u.Host, "www.github.com") ||
|
||||||
|
strings.EqualFold(u.Host, "github.localhost") ||
|
||||||
|
strings.HasSuffix(u.Host, ".ghe.com")
|
||||||
|
}
|
||||||
+196
@@ -0,0 +1,196 @@
|
|||||||
|
package scaleset_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/actions/scaleset"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGitHubConfig(t *testing.T) {
|
||||||
|
t.Run("when given a valid URL", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
configURL string
|
||||||
|
expected *scaleset.GitHubConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
configURL: "https://github.com/org/repo",
|
||||||
|
expected: &scaleset.GitHubConfig{
|
||||||
|
Scope: scaleset.GitHubScopeRepository,
|
||||||
|
Enterprise: "",
|
||||||
|
Organization: "org",
|
||||||
|
Repository: "repo",
|
||||||
|
IsHosted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configURL: "https://github.com/org/repo/",
|
||||||
|
expected: &scaleset.GitHubConfig{
|
||||||
|
Scope: scaleset.GitHubScopeRepository,
|
||||||
|
Enterprise: "",
|
||||||
|
Organization: "org",
|
||||||
|
Repository: "repo",
|
||||||
|
IsHosted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configURL: "https://github.com/org",
|
||||||
|
expected: &scaleset.GitHubConfig{
|
||||||
|
Scope: scaleset.GitHubScopeOrganization,
|
||||||
|
Enterprise: "",
|
||||||
|
Organization: "org",
|
||||||
|
Repository: "",
|
||||||
|
IsHosted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configURL: "https://github.com/enterprises/my-enterprise",
|
||||||
|
expected: &scaleset.GitHubConfig{
|
||||||
|
Scope: scaleset.GitHubScopeEnterprise,
|
||||||
|
Enterprise: "my-enterprise",
|
||||||
|
Organization: "",
|
||||||
|
Repository: "",
|
||||||
|
IsHosted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configURL: "https://github.com/enterprises/my-enterprise/",
|
||||||
|
expected: &scaleset.GitHubConfig{
|
||||||
|
Scope: scaleset.GitHubScopeEnterprise,
|
||||||
|
Enterprise: "my-enterprise",
|
||||||
|
Organization: "",
|
||||||
|
Repository: "",
|
||||||
|
IsHosted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configURL: "https://www.github.com/org",
|
||||||
|
expected: &scaleset.GitHubConfig{
|
||||||
|
Scope: scaleset.GitHubScopeOrganization,
|
||||||
|
Enterprise: "",
|
||||||
|
Organization: "org",
|
||||||
|
Repository: "",
|
||||||
|
IsHosted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configURL: "https://www.github.com/org/",
|
||||||
|
expected: &scaleset.GitHubConfig{
|
||||||
|
Scope: scaleset.GitHubScopeOrganization,
|
||||||
|
Enterprise: "",
|
||||||
|
Organization: "org",
|
||||||
|
Repository: "",
|
||||||
|
IsHosted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configURL: "https://github.localhost/org",
|
||||||
|
expected: &scaleset.GitHubConfig{
|
||||||
|
Scope: scaleset.GitHubScopeOrganization,
|
||||||
|
Enterprise: "",
|
||||||
|
Organization: "org",
|
||||||
|
Repository: "",
|
||||||
|
IsHosted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configURL: "https://my-ghes.com/org",
|
||||||
|
expected: &scaleset.GitHubConfig{
|
||||||
|
Scope: scaleset.GitHubScopeOrganization,
|
||||||
|
Enterprise: "",
|
||||||
|
Organization: "org",
|
||||||
|
Repository: "",
|
||||||
|
IsHosted: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configURL: "https://my-ghes.com/org/",
|
||||||
|
expected: &scaleset.GitHubConfig{
|
||||||
|
Scope: scaleset.GitHubScopeOrganization,
|
||||||
|
Enterprise: "",
|
||||||
|
Organization: "org",
|
||||||
|
Repository: "",
|
||||||
|
IsHosted: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configURL: "https://my-ghes.ghe.com/org/",
|
||||||
|
expected: &scaleset.GitHubConfig{
|
||||||
|
Scope: scaleset.GitHubScopeOrganization,
|
||||||
|
Enterprise: "",
|
||||||
|
Organization: "org",
|
||||||
|
Repository: "",
|
||||||
|
IsHosted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.configURL, func(t *testing.T) {
|
||||||
|
parsedURL, err := url.Parse(strings.Trim(test.configURL, "/"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
test.expected.ConfigURL = parsedURL
|
||||||
|
|
||||||
|
cfg, err := scaleset.ParseGitHubConfigFromURL(test.configURL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, cfg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("when given an invalid URL", func(t *testing.T) {
|
||||||
|
invalidURLs := []string{
|
||||||
|
"https://github.com/",
|
||||||
|
"https://github.com",
|
||||||
|
"https://github.com/some/random/path",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range invalidURLs {
|
||||||
|
_, err := scaleset.ParseGitHubConfigFromURL(u)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.True(t, errors.Is(err, scaleset.ErrInvalidGitHubConfigURL))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGitHubConfig_GitHubAPIURL(t *testing.T) {
|
||||||
|
t.Run("when hosted", func(t *testing.T) {
|
||||||
|
config, err := scaleset.ParseGitHubConfigFromURL("https://github.com/org/repo")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, config.IsHosted)
|
||||||
|
|
||||||
|
result := config.GitHubAPIURL("/some/path")
|
||||||
|
assert.Equal(t, "https://api.github.com/some/path", result.String())
|
||||||
|
})
|
||||||
|
t.Run("when hosted with ghe.com", func(t *testing.T) {
|
||||||
|
config, err := scaleset.ParseGitHubConfigFromURL("https://github.ghe.com/org/repo")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, config.IsHosted)
|
||||||
|
|
||||||
|
result := config.GitHubAPIURL("/some/path")
|
||||||
|
assert.Equal(t, "https://api.github.ghe.com/some/path", result.String())
|
||||||
|
})
|
||||||
|
t.Run("when not hosted", func(t *testing.T) {
|
||||||
|
config, err := scaleset.ParseGitHubConfigFromURL("https://ghes.com/org/repo")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, config.IsHosted)
|
||||||
|
|
||||||
|
result := config.GitHubAPIURL("/some/path")
|
||||||
|
assert.Equal(t, "https://ghes.com/api/v3/some/path", result.String())
|
||||||
|
})
|
||||||
|
t.Run("when not hosted with ghe.com", func(t *testing.T) {
|
||||||
|
os.Setenv("GITHUB_ACTIONS_FORCE_GHES", "1")
|
||||||
|
defer os.Unsetenv("GITHUB_ACTIONS_FORCE_GHES")
|
||||||
|
config, err := scaleset.ParseGitHubConfigFromURL("https://test.ghe.com/org/repo")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, config.IsHosted)
|
||||||
|
|
||||||
|
result := config.GitHubAPIURL("/some/path")
|
||||||
|
assert.Equal(t, "https://test.ghe.com/api/v3/some/path", result.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package scaleset
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Header names for request IDs
|
||||||
|
const (
|
||||||
|
HeaderActionsActivityID = "ActivityId"
|
||||||
|
HeaderGitHubRequestID = "X-GitHub-Request-Id"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GitHubAPIError struct {
|
||||||
|
StatusCode int
|
||||||
|
RequestID string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GitHubAPIError) Error() string {
|
||||||
|
return fmt.Sprintf("github api error: StatusCode %d, RequestID %q: %v", e.StatusCode, e.RequestID, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GitHubAPIError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionsError struct {
|
||||||
|
ActivityID string
|
||||||
|
StatusCode int
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ActionsError) Error() string {
|
||||||
|
return fmt.Sprintf("actions error: StatusCode %d, AcivityId %q: %v", e.StatusCode, e.ActivityID, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ActionsError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ActionsError) IsException(target string) bool {
|
||||||
|
if ex, ok := e.Err.(*ActionsExceptionError); ok {
|
||||||
|
return strings.Contains(ex.ExceptionName, target)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionsExceptionError struct {
|
||||||
|
ExceptionName string `json:"typeName,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ActionsExceptionError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: %s", e.ExceptionName, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseActionsErrorFromResponse(response *http.Response) error {
|
||||||
|
if response.ContentLength == 0 {
|
||||||
|
return &ActionsError{
|
||||||
|
ActivityID: response.Header.Get(HeaderActionsActivityID),
|
||||||
|
StatusCode: response.StatusCode,
|
||||||
|
Err: errors.New("unknown exception"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defer response.Body.Close()
|
||||||
|
body, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return &ActionsError{
|
||||||
|
ActivityID: response.Header.Get(HeaderActionsActivityID),
|
||||||
|
StatusCode: response.StatusCode,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body = trimByteOrderMark(body)
|
||||||
|
contentType, ok := response.Header["Content-Type"]
|
||||||
|
if ok && len(contentType) > 0 && strings.Contains(contentType[0], "text/plain") {
|
||||||
|
message := string(body)
|
||||||
|
return &ActionsError{
|
||||||
|
ActivityID: response.Header.Get(HeaderActionsActivityID),
|
||||||
|
StatusCode: response.StatusCode,
|
||||||
|
Err: errors.New(message),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var exception ActionsExceptionError
|
||||||
|
if err := json.Unmarshal(body, &exception); err != nil {
|
||||||
|
return &ActionsError{
|
||||||
|
ActivityID: response.Header.Get(HeaderActionsActivityID),
|
||||||
|
StatusCode: response.StatusCode,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ActionsError{
|
||||||
|
ActivityID: response.Header.Get(HeaderActionsActivityID),
|
||||||
|
StatusCode: response.StatusCode,
|
||||||
|
Err: &exception,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageQueueTokenExpiredError struct {
|
||||||
|
activityID string
|
||||||
|
statusCode int
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MessageQueueTokenExpiredError) Error() string {
|
||||||
|
return fmt.Sprintf("MessageQueueTokenExpiredError: AcivityId %q, StatusCode %d: %s", e.activityID, e.statusCode, e.msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type HttpClientSideError struct {
|
||||||
|
msg string
|
||||||
|
Code int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HttpClientSideError) Error() string {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
package scaleset_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/actions/scaleset"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestActionsError(t *testing.T) {
|
||||||
|
t.Run("contains the status code, activity ID, and error", func(t *testing.T) {
|
||||||
|
err := &scaleset.ActionsError{
|
||||||
|
ActivityID: "activity-id",
|
||||||
|
StatusCode: 404,
|
||||||
|
Err: errors.New("example error description"),
|
||||||
|
}
|
||||||
|
|
||||||
|
s := err.Error()
|
||||||
|
assert.Contains(t, s, "StatusCode 404")
|
||||||
|
assert.Contains(t, s, "AcivityId \"activity-id\"")
|
||||||
|
assert.Contains(t, s, "example error description")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unwraps the error", func(t *testing.T) {
|
||||||
|
err := &scaleset.ActionsError{
|
||||||
|
ActivityID: "activity-id",
|
||||||
|
StatusCode: 404,
|
||||||
|
Err: &scaleset.ActionsExceptionError{
|
||||||
|
ExceptionName: "exception-name",
|
||||||
|
Message: "example error message",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, err.Unwrap(), err.Err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("is exception is ok", func(t *testing.T) {
|
||||||
|
err := &scaleset.ActionsError{
|
||||||
|
ActivityID: "activity-id",
|
||||||
|
StatusCode: 404,
|
||||||
|
Err: &scaleset.ActionsExceptionError{
|
||||||
|
ExceptionName: "exception-name",
|
||||||
|
Message: "example error message",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var exception *scaleset.ActionsExceptionError
|
||||||
|
assert.True(t, errors.As(err, &exception))
|
||||||
|
|
||||||
|
assert.True(t, err.IsException("exception-name"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("is exception is not ok", func(t *testing.T) {
|
||||||
|
tt := map[string]*scaleset.ActionsError{
|
||||||
|
"not an exception": {
|
||||||
|
ActivityID: "activity-id",
|
||||||
|
StatusCode: 404,
|
||||||
|
Err: errors.New("example error description"),
|
||||||
|
},
|
||||||
|
"not target exception": {
|
||||||
|
ActivityID: "activity-id",
|
||||||
|
StatusCode: 404,
|
||||||
|
Err: &scaleset.ActionsExceptionError{
|
||||||
|
ExceptionName: "exception-name",
|
||||||
|
Message: "example error message",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
targetException := "target-exception"
|
||||||
|
for name, err := range tt {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert.False(t, err.IsException(targetException))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionsExceptionError(t *testing.T) {
|
||||||
|
t.Run("contains the exception name and message", func(t *testing.T) {
|
||||||
|
err := &scaleset.ActionsExceptionError{
|
||||||
|
ExceptionName: "exception-name",
|
||||||
|
Message: "example error message",
|
||||||
|
}
|
||||||
|
|
||||||
|
s := err.Error()
|
||||||
|
assert.Contains(t, s, "exception-name")
|
||||||
|
assert.Contains(t, s, "example error message")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGitHubAPIError(t *testing.T) {
|
||||||
|
t.Run("contains the status code, request ID, and error", func(t *testing.T) {
|
||||||
|
err := &scaleset.GitHubAPIError{
|
||||||
|
StatusCode: 404,
|
||||||
|
RequestID: "request-id",
|
||||||
|
Err: errors.New("example error description"),
|
||||||
|
}
|
||||||
|
|
||||||
|
s := err.Error()
|
||||||
|
assert.Contains(t, s, "StatusCode 404")
|
||||||
|
assert.Contains(t, s, "RequestID \"request-id\"")
|
||||||
|
assert.Contains(t, s, "example error description")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unwraps the error", func(t *testing.T) {
|
||||||
|
err := &scaleset.GitHubAPIError{
|
||||||
|
StatusCode: 404,
|
||||||
|
RequestID: "request-id",
|
||||||
|
Err: errors.New("example error description"),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, err.Unwrap(), err.Err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseActionsErrorFromResponse(t *testing.T) {
|
||||||
|
t.Run("empty content length", func(t *testing.T) {
|
||||||
|
response := &http.Response{
|
||||||
|
ContentLength: 0,
|
||||||
|
Header: http.Header{
|
||||||
|
scaleset.HeaderActionsActivityID: []string{"activity-id"},
|
||||||
|
},
|
||||||
|
StatusCode: 404,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := scaleset.ParseActionsErrorFromResponse(response)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, err.(*scaleset.ActionsError).ActivityID, "activity-id")
|
||||||
|
assert.Equal(t, err.(*scaleset.ActionsError).StatusCode, 404)
|
||||||
|
assert.Equal(t, err.(*scaleset.ActionsError).Err.Error(), "unknown exception")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("contains text plain error", func(t *testing.T) {
|
||||||
|
errorMessage := "example error message"
|
||||||
|
response := &http.Response{
|
||||||
|
ContentLength: int64(len(errorMessage)),
|
||||||
|
Header: http.Header{
|
||||||
|
scaleset.HeaderActionsActivityID: []string{"activity-id"},
|
||||||
|
"Content-Type": []string{"text/plain"},
|
||||||
|
},
|
||||||
|
StatusCode: 404,
|
||||||
|
Body: io.NopCloser(strings.NewReader(errorMessage)),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := scaleset.ParseActionsErrorFromResponse(response)
|
||||||
|
require.Error(t, err)
|
||||||
|
var actionsError *scaleset.ActionsError
|
||||||
|
assert.ErrorAs(t, err, &actionsError)
|
||||||
|
assert.Equal(t, actionsError.ActivityID, "activity-id")
|
||||||
|
assert.Equal(t, actionsError.StatusCode, 404)
|
||||||
|
assert.Equal(t, actionsError.Err.Error(), errorMessage)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("contains json error", func(t *testing.T) {
|
||||||
|
errorMessage := `{"typeName":"exception-name","message":"example error message"}`
|
||||||
|
response := &http.Response{
|
||||||
|
ContentLength: int64(len(errorMessage)),
|
||||||
|
Header: http.Header{
|
||||||
|
scaleset.HeaderActionsActivityID: []string{"activity-id"},
|
||||||
|
"Content-Type": []string{"application/json"},
|
||||||
|
},
|
||||||
|
StatusCode: 404,
|
||||||
|
Body: io.NopCloser(strings.NewReader(errorMessage)),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := scaleset.ParseActionsErrorFromResponse(response)
|
||||||
|
require.Error(t, err)
|
||||||
|
var actionsError *scaleset.ActionsError
|
||||||
|
assert.ErrorAs(t, err, &actionsError)
|
||||||
|
assert.Equal(t, actionsError.ActivityID, "activity-id")
|
||||||
|
assert.Equal(t, actionsError.StatusCode, 404)
|
||||||
|
|
||||||
|
inner, ok := actionsError.Err.(*scaleset.ActionsExceptionError)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, inner.ExceptionName, "exception-name")
|
||||||
|
assert.Equal(t, inner.Message, "example error message")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("wrapped exception error", func(t *testing.T) {
|
||||||
|
errorMessage := `{"typeName":"exception-name","message":"example error message"}`
|
||||||
|
response := &http.Response{
|
||||||
|
ContentLength: int64(len(errorMessage)),
|
||||||
|
Header: http.Header{
|
||||||
|
scaleset.HeaderActionsActivityID: []string{"activity-id"},
|
||||||
|
"Content-Type": []string{"application/json"},
|
||||||
|
},
|
||||||
|
StatusCode: 404,
|
||||||
|
Body: io.NopCloser(strings.NewReader(errorMessage)),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := scaleset.ParseActionsErrorFromResponse(response)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
var actionsExceptionError *scaleset.ActionsExceptionError
|
||||||
|
assert.ErrorAs(t, err, &actionsExceptionError)
|
||||||
|
|
||||||
|
assert.Equal(t, actionsExceptionError.ExceptionName, "exception-name")
|
||||||
|
assert.Equal(t, actionsExceptionError.Message, "example error message")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package scaleset_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions/scaleset"
|
||||||
|
"github.com/actions/scaleset/testserver"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testUserAgent = scaleset.UserAgentInfo{
|
||||||
|
Version: "test",
|
||||||
|
CommitSHA: "test",
|
||||||
|
ScaleSetID: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewGitHubAPIRequest(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("uses the right host/path prefix", func(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
configURL string
|
||||||
|
path string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
configURL: "https://github.com/org/repo",
|
||||||
|
path: "/app/installations/123/access_tokens",
|
||||||
|
expected: "https://api.github.com/app/installations/123/access_tokens",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configURL: "https://www.github.com/org/repo",
|
||||||
|
path: "/app/installations/123/access_tokens",
|
||||||
|
expected: "https://api.github.com/app/installations/123/access_tokens",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configURL: "http://github.localhost/org/repo",
|
||||||
|
path: "/app/installations/123/access_tokens",
|
||||||
|
expected: "http://api.github.localhost/app/installations/123/access_tokens",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configURL: "https://my-instance.com/org/repo",
|
||||||
|
path: "/app/installations/123/access_tokens",
|
||||||
|
expected: "https://my-instance.com/api/v3/app/installations/123/access_tokens",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configURL: "http://localhost/org/repo",
|
||||||
|
path: "/app/installations/123/access_tokens",
|
||||||
|
expected: "http://localhost/api/v3/app/installations/123/access_tokens",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
client, err := scaleset.NewClient(scenario.configURL, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req, err := client.NewGitHubAPIRequest(ctx, http.MethodGet, scenario.path, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, scenario.expected, req.URL.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sets user agent header if present", func(t *testing.T) {
|
||||||
|
client, err := scaleset.NewClient("http://localhost/my-org", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client.SetUserAgent(testUserAgent)
|
||||||
|
|
||||||
|
req, err := client.NewGitHubAPIRequest(ctx, http.MethodGet, "/app/installations/123/access_tokens", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, testUserAgent.String(), req.Header.Get("User-Agent"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sets the body we pass", func(t *testing.T) {
|
||||||
|
client, err := scaleset.NewClient("http://localhost/my-org", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req, err := client.NewGitHubAPIRequest(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
"/app/installations/123/access_tokens",
|
||||||
|
strings.NewReader("the-body"),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
b, err := io.ReadAll(req.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "the-body", string(b))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewActionsServiceRequest(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
defaultCreds := &scaleset.ActionsAuth{Token: "token"}
|
||||||
|
|
||||||
|
t.Run("manages authentication", func(t *testing.T) {
|
||||||
|
t.Run("client is brand new", func(t *testing.T) {
|
||||||
|
token := defaultActionsToken(t)
|
||||||
|
server := testserver.New(t, nil, testserver.WithActionsToken(token))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.ConfigURLForOrg("my-org"), defaultCreds)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req, err := client.NewActionsServiceRequest(ctx, http.MethodGet, "my-path", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "Bearer "+token, req.Header.Get("Authorization"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("admin token is about to expire", func(t *testing.T) {
|
||||||
|
newToken := defaultActionsToken(t)
|
||||||
|
server := testserver.New(t, nil, testserver.WithActionsToken(newToken))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.ConfigURLForOrg("my-org"), defaultCreds)
|
||||||
|
require.NoError(t, err)
|
||||||
|
client.ActionsServiceAdminToken = "expiring-token"
|
||||||
|
client.ActionsServiceAdminTokenExpiresAt = time.Now().Add(59 * time.Second)
|
||||||
|
|
||||||
|
req, err := client.NewActionsServiceRequest(ctx, http.MethodGet, "my-path", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "Bearer "+newToken, req.Header.Get("Authorization"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("admin token refresh failure", func(t *testing.T) {
|
||||||
|
newToken := defaultActionsToken(t)
|
||||||
|
errMessage := `{"message":"test"}`
|
||||||
|
unauthorizedHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
w.Write([]byte(errMessage))
|
||||||
|
}
|
||||||
|
server := testserver.New(
|
||||||
|
t,
|
||||||
|
nil,
|
||||||
|
testserver.WithActionsToken("random-token"),
|
||||||
|
testserver.WithActionsToken(newToken),
|
||||||
|
testserver.WithActionsRegistrationTokenHandler(unauthorizedHandler),
|
||||||
|
)
|
||||||
|
client, err := scaleset.NewClient(server.ConfigURLForOrg("my-org"), defaultCreds)
|
||||||
|
require.NoError(t, err)
|
||||||
|
expiringToken := "expiring-token"
|
||||||
|
expiresAt := time.Now().Add(59 * time.Second)
|
||||||
|
client.ActionsServiceAdminToken = expiringToken
|
||||||
|
client.ActionsServiceAdminTokenExpiresAt = expiresAt
|
||||||
|
_, err = client.NewActionsServiceRequest(ctx, http.MethodGet, "my-path", nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), errMessage)
|
||||||
|
assert.Equal(t, client.ActionsServiceAdminToken, expiringToken)
|
||||||
|
assert.Equal(t, client.ActionsServiceAdminTokenExpiresAt, expiresAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("admin token refresh retry", func(t *testing.T) {
|
||||||
|
newToken := defaultActionsToken(t)
|
||||||
|
errMessage := `{"message":"test"}`
|
||||||
|
|
||||||
|
srv := "http://github.com/my-org"
|
||||||
|
resp := &scaleset.ActionsServiceAdminConnection{
|
||||||
|
AdminToken: &newToken,
|
||||||
|
ActionsServiceUrl: &srv,
|
||||||
|
}
|
||||||
|
failures := 0
|
||||||
|
unauthorizedHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if failures < 5 {
|
||||||
|
failures++
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
w.Write([]byte(errMessage))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_ = json.NewEncoder(w).Encode(resp)
|
||||||
|
}
|
||||||
|
server := testserver.New(t, nil, testserver.WithActionsToken("random-token"), testserver.WithActionsToken(newToken), testserver.WithActionsRegistrationTokenHandler(unauthorizedHandler))
|
||||||
|
client, err := scaleset.NewClient(server.ConfigURLForOrg("my-org"), defaultCreds)
|
||||||
|
require.NoError(t, err)
|
||||||
|
expiringToken := "expiring-token"
|
||||||
|
expiresAt := time.Now().Add(59 * time.Second)
|
||||||
|
client.ActionsServiceAdminToken = expiringToken
|
||||||
|
client.ActionsServiceAdminTokenExpiresAt = expiresAt
|
||||||
|
|
||||||
|
_, err = client.NewActionsServiceRequest(ctx, http.MethodGet, "my-path", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, client.ActionsServiceAdminToken, newToken)
|
||||||
|
assert.Equal(t, client.ActionsServiceURL, srv)
|
||||||
|
assert.NotEqual(t, client.ActionsServiceAdminTokenExpiresAt, expiresAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("token is currently valid", func(t *testing.T) {
|
||||||
|
tokenThatShouldNotBeFetched := defaultActionsToken(t)
|
||||||
|
server := testserver.New(t, nil, testserver.WithActionsToken(tokenThatShouldNotBeFetched))
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.ConfigURLForOrg("my-org"), defaultCreds)
|
||||||
|
require.NoError(t, err)
|
||||||
|
client.ActionsServiceAdminToken = "healthy-token"
|
||||||
|
client.ActionsServiceAdminTokenExpiresAt = time.Now().Add(1 * time.Hour)
|
||||||
|
|
||||||
|
req, err := client.NewActionsServiceRequest(ctx, http.MethodGet, "my-path", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "Bearer healthy-token", req.Header.Get("Authorization"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("builds the right URL including api version", func(t *testing.T) {
|
||||||
|
server := testserver.New(t, nil)
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.ConfigURLForOrg("my-org"), defaultCreds)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req, err := client.NewActionsServiceRequest(ctx, http.MethodGet, "/my/path?name=banana", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
serverURL, err := url.Parse(server.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
result := req.URL
|
||||||
|
assert.Equal(t, serverURL.Host, result.Host)
|
||||||
|
assert.Equal(t, "/tenant/123/my/path", result.Path)
|
||||||
|
assert.Equal(t, "banana", result.Query().Get("name"))
|
||||||
|
assert.Equal(t, "6.0-preview", result.Query().Get("api-version"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("populates header", func(t *testing.T) {
|
||||||
|
server := testserver.New(t, nil)
|
||||||
|
|
||||||
|
client, err := scaleset.NewClient(server.ConfigURLForOrg("my-org"), defaultCreds)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client.SetUserAgent(testUserAgent)
|
||||||
|
|
||||||
|
req, err := client.NewActionsServiceRequest(ctx, http.MethodGet, "/my/path", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, testUserAgent.String(), req.Header.Get("User-Agent"))
|
||||||
|
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
module github.com/actions/scaleset
|
||||||
|
|
||||||
|
go 1.25.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-logr/logr v1.4.3
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.8
|
||||||
|
github.com/onsi/ginkgo/v2 v2.27.2
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
|
golang.org/x/net v0.46.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
|
golang.org/x/mod v0.28.0 // indirect
|
||||||
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
|
golang.org/x/text v0.30.0 // indirect
|
||||||
|
golang.org/x/tools v0.37.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||||
|
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
|
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
|
||||||
|
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
|
||||||
|
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
|
||||||
|
github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
|
||||||
|
github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE=
|
||||||
|
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||||
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
|
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||||
|
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||||
|
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
|
||||||
|
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
|
||||||
|
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
|
||||||
|
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||||
|
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||||
|
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||||
|
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||||
|
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||||
|
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||||
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
|
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||||
|
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||||
|
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||||
|
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||||
|
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package scaleset_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/actions/scaleset"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClient_Identifier(t *testing.T) {
|
||||||
|
t.Run("configURL changes", func(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
url string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "url of a different repo",
|
||||||
|
url: "https://github.com/org/repo2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url of an org",
|
||||||
|
url: "https://github.com/org",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url of an enterprise",
|
||||||
|
url: "https://github.com/enterprises/my-enterprise",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url of a self-hosted github",
|
||||||
|
url: "https://selfhosted.com/org/repo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
configURL := "https://github.com/org/repo"
|
||||||
|
defaultCreds := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
oldClient, err := scaleset.NewClient(configURL, defaultCreds)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.name, func(t *testing.T) {
|
||||||
|
newClient, err := scaleset.NewClient(scenario.url, defaultCreds)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEqual(t, oldClient.Identifier(), newClient.Identifier())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("credentials change", func(t *testing.T) {
|
||||||
|
defaultTokenCreds := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
defaultAppCreds := &scaleset.ActionsAuth{
|
||||||
|
AppCreds: &scaleset.GitHubAppAuth{
|
||||||
|
AppID: "123",
|
||||||
|
AppInstallationID: 123,
|
||||||
|
AppPrivateKey: "private key",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
old *scaleset.ActionsAuth
|
||||||
|
new *scaleset.ActionsAuth
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "different token",
|
||||||
|
old: defaultTokenCreds,
|
||||||
|
new: &scaleset.ActionsAuth{
|
||||||
|
Token: "new token",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "changing from token to github app",
|
||||||
|
old: defaultTokenCreds,
|
||||||
|
new: defaultAppCreds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "changing from github app to token",
|
||||||
|
old: defaultAppCreds,
|
||||||
|
new: defaultTokenCreds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "different github app",
|
||||||
|
old: defaultAppCreds,
|
||||||
|
new: &scaleset.ActionsAuth{
|
||||||
|
AppCreds: &scaleset.GitHubAppAuth{
|
||||||
|
AppID: "456",
|
||||||
|
AppInstallationID: 456,
|
||||||
|
AppPrivateKey: "new private key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfigURL := "https://github.com/org/repo"
|
||||||
|
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.name, func(t *testing.T) {
|
||||||
|
oldClient, err := scaleset.NewClient(defaultConfigURL, scenario.old)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
newClient, err := scaleset.NewClient(defaultConfigURL, scenario.new)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEqual(t, oldClient.Identifier(), newClient.Identifier())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("changes in TLS config", func(t *testing.T) {
|
||||||
|
configURL := "https://github.com/org/repo"
|
||||||
|
defaultCreds := &scaleset.ActionsAuth{
|
||||||
|
Token: "token",
|
||||||
|
}
|
||||||
|
|
||||||
|
noTlS, err := scaleset.NewClient(configURL, defaultCreds)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
poolFromCert := func(t *testing.T, path string) *x509.CertPool {
|
||||||
|
t.Helper()
|
||||||
|
f, err := os.ReadFile(path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
require.True(t, pool.AppendCertsFromPEM(f))
|
||||||
|
return pool
|
||||||
|
}
|
||||||
|
|
||||||
|
root, err := scaleset.NewClient(
|
||||||
|
configURL,
|
||||||
|
defaultCreds,
|
||||||
|
scaleset.WithRootCAs(poolFromCert(t, filepath.Join("testdata", "rootCA.crt"))),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
chain, err := scaleset.NewClient(
|
||||||
|
configURL,
|
||||||
|
defaultCreds,
|
||||||
|
scaleset.WithRootCAs(poolFromCert(t, filepath.Join("testdata", "intermediate.crt"))),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
clients := []*scaleset.Client{
|
||||||
|
noTlS,
|
||||||
|
root,
|
||||||
|
chain,
|
||||||
|
}
|
||||||
|
identifiers := map[string]struct{}{}
|
||||||
|
for _, client := range clients {
|
||||||
|
identifiers[client.Identifier()] = struct{}{}
|
||||||
|
}
|
||||||
|
assert.Len(t, identifiers, len(clients), "all clients should have a unique identifier")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
// Code generated by mockery v2.36.1. DO NOT EDIT.
|
||||||
|
|
||||||
|
package scaleset
|
||||||
|
|
||||||
|
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
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int, string, []int64) ([]int64, error)); ok {
|
||||||
|
return rf(ctx, runnerScaleSetId, messageQueueAccessToken, requestIds)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int, string) (*RunnerScaleSetSession, error)); ok {
|
||||||
|
return rf(ctx, runnerScaleSetId, owner)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *RunnerScaleSet) (*RunnerScaleSet, error)); ok {
|
||||||
|
return rf(ctx, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRunnerScaleSet provides a mock function with given fields: ctx, runnerScaleSetId
|
||||||
|
func (_m *MockActionsService) DeleteRunnerScaleSet(ctx context.Context, runnerScaleSetId int) error {
|
||||||
|
ret := _m.Called(ctx, runnerScaleSetId)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int) error); ok {
|
||||||
|
r0 = rf(ctx, runnerScaleSetId)
|
||||||
|
} 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
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *RunnerScaleSetJitRunnerSetting, int) (*RunnerScaleSetJitRunnerConfig, error)); ok {
|
||||||
|
return rf(ctx, jitRunnerSetting, scaleSetId)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int) (*AcquirableJobList, error)); ok {
|
||||||
|
return rf(ctx, runnerScaleSetId)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, maxCapacity
|
||||||
|
func (_m *MockActionsService) GetMessage(ctx context.Context, messageQueueUrl string, messageQueueAccessToken string, lastMessageId int64, maxCapacity int) (*RunnerScaleSetMessage, error) {
|
||||||
|
ret := _m.Called(ctx, messageQueueUrl, messageQueueAccessToken, lastMessageId, maxCapacity)
|
||||||
|
|
||||||
|
var r0 *RunnerScaleSetMessage
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string, string, int64, int) (*RunnerScaleSetMessage, error)); ok {
|
||||||
|
return rf(ctx, messageQueueUrl, messageQueueAccessToken, lastMessageId, maxCapacity)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string, string, int64, int) *RunnerScaleSetMessage); ok {
|
||||||
|
r0 = rf(ctx, messageQueueUrl, messageQueueAccessToken, lastMessageId, maxCapacity)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*RunnerScaleSetMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, string, string, int64, int) error); ok {
|
||||||
|
r1 = rf(ctx, messageQueueUrl, messageQueueAccessToken, lastMessageId, maxCapacity)
|
||||||
|
} 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
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int64) (*RunnerReference, error)); ok {
|
||||||
|
return rf(ctx, runnerId)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string) (*RunnerReference, error)); ok {
|
||||||
|
return rf(ctx, runnerName)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string) (*RunnerGroup, error)); ok {
|
||||||
|
return rf(ctx, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, runnerGroupId, runnerScaleSetName
|
||||||
|
func (_m *MockActionsService) GetRunnerScaleSet(ctx context.Context, runnerGroupId int, runnerScaleSetName string) (*RunnerScaleSet, error) {
|
||||||
|
ret := _m.Called(ctx, runnerGroupId, runnerScaleSetName)
|
||||||
|
|
||||||
|
var r0 *RunnerScaleSet
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int, string) (*RunnerScaleSet, error)); ok {
|
||||||
|
return rf(ctx, runnerGroupId, runnerScaleSetName)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int, string) *RunnerScaleSet); ok {
|
||||||
|
r0 = rf(ctx, runnerGroupId, runnerScaleSetName)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*RunnerScaleSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, int, string) error); ok {
|
||||||
|
r1 = rf(ctx, runnerGroupId, 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
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int) (*RunnerScaleSet, error)); ok {
|
||||||
|
return rf(ctx, runnerScaleSetId)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int, *uuid.UUID) (*RunnerScaleSetSession, error)); ok {
|
||||||
|
return rf(ctx, runnerScaleSetId, sessionId)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserAgent provides a mock function with given fields: info
|
||||||
|
func (_m *MockActionsService) SetUserAgent(info UserAgentInfo) {
|
||||||
|
_m.Called(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRunnerScaleSet provides a mock function with given fields: ctx, runnerScaleSetId, runnerScaleSet
|
||||||
|
func (_m *MockActionsService) UpdateRunnerScaleSet(ctx context.Context, runnerScaleSetId int, runnerScaleSet *RunnerScaleSet) (*RunnerScaleSet, error) {
|
||||||
|
ret := _m.Called(ctx, runnerScaleSetId, runnerScaleSet)
|
||||||
|
|
||||||
|
var r0 *RunnerScaleSet
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int, *RunnerScaleSet) (*RunnerScaleSet, error)); ok {
|
||||||
|
return rf(ctx, runnerScaleSetId, runnerScaleSet)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int, *RunnerScaleSet) *RunnerScaleSet); ok {
|
||||||
|
r0 = rf(ctx, runnerScaleSetId, runnerScaleSet)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*RunnerScaleSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, int, *RunnerScaleSet) error); ok {
|
||||||
|
r1 = rf(ctx, runnerScaleSetId, runnerScaleSet)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// The first argument is typically a *testing.T value.
|
||||||
|
func NewMockActionsService(t interface {
|
||||||
|
mock.TestingT
|
||||||
|
Cleanup(func())
|
||||||
|
}) *MockActionsService {
|
||||||
|
mock := &MockActionsService{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// Code generated by mockery v2.36.1. DO NOT EDIT.
|
||||||
|
|
||||||
|
package scaleset
|
||||||
|
|
||||||
|
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
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, []int64) ([]int64, error)); ok {
|
||||||
|
return rf(ctx, requestIds)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, maxCapacity
|
||||||
|
func (_m *MockSessionService) GetMessage(ctx context.Context, lastMessageId int64, maxCapacity int) (*RunnerScaleSetMessage, error) {
|
||||||
|
ret := _m.Called(ctx, lastMessageId, maxCapacity)
|
||||||
|
|
||||||
|
var r0 *RunnerScaleSetMessage
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int64, int) (*RunnerScaleSetMessage, error)); ok {
|
||||||
|
return rf(ctx, lastMessageId, maxCapacity)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int64, int) *RunnerScaleSetMessage); ok {
|
||||||
|
r0 = rf(ctx, lastMessageId, maxCapacity)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*RunnerScaleSetMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, int64, int) error); ok {
|
||||||
|
r1 = rf(ctx, lastMessageId, maxCapacity)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// The first argument is typically a *testing.T value.
|
||||||
|
func NewMockSessionService(t interface {
|
||||||
|
mock.TestingT
|
||||||
|
Cleanup(func())
|
||||||
|
}) *MockSessionService {
|
||||||
|
mock := &MockSessionService{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package scaleset
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate mockery --inpackage --name=SessionService
|
||||||
|
type SessionService interface {
|
||||||
|
GetMessage(ctx context.Context, lastMessageId int64, maxCapacity int) (*RunnerScaleSetMessage, error)
|
||||||
|
DeleteMessage(ctx context.Context, messageId int64) error
|
||||||
|
AcquireJobs(ctx context.Context, requestIds []int64) ([]int64, error)
|
||||||
|
io.Closer
|
||||||
|
}
|
||||||
Vendored
+29
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Generate Root CA
|
||||||
|
openssl genrsa -out rootCA.key 2048
|
||||||
|
openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt -subj "/CN=Test Root CA" \
|
||||||
|
-addext "basicConstraints = critical, CA:TRUE" \
|
||||||
|
-addext "keyUsage = critical, keyCertSign, cRLSign"
|
||||||
|
|
||||||
|
# Generate Intermediate Certificate
|
||||||
|
openssl genrsa -out intermediate.key 2048
|
||||||
|
openssl req -new -key intermediate.key -out intermediate.csr -subj "/CN=Test Intermediate CA"
|
||||||
|
openssl x509 -req -in intermediate.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out intermediate.crt -days 1000 -sha256 \
|
||||||
|
-extfile <(echo -e "basicConstraints = critical, CA:TRUE, pathlen:0\nkeyUsage = critical, keyCertSign, cRLSign")
|
||||||
|
|
||||||
|
# Generate Leaf Certificate
|
||||||
|
openssl genrsa -out leaf.key 2048
|
||||||
|
openssl req -new -key leaf.key -out leaf.csr -subj "/CN=localhost" \
|
||||||
|
-addext "subjectAltName = IP:127.0.0.1"
|
||||||
|
openssl x509 -req -in leaf.csr -CA intermediate.crt -CAkey intermediate.key -CAcreateserial -out leaf.crt -days 500 -sha256 \
|
||||||
|
-extfile <(echo -e "authorityKeyIdentifier=keyid,issuer\nbasicConstraints=CA:FALSE\nkeyUsage = digitalSignature, keyEncipherment\nextendedKeyUsage=serverAuth\nsubjectAltName=IP:127.0.0.1")
|
||||||
|
|
||||||
|
# Generate Leaf Certificate
|
||||||
|
openssl genrsa -out server.key 2048
|
||||||
|
openssl req -new -key server.key -out server.csr -subj "/CN=localhost" \
|
||||||
|
-addext "subjectAltName = IP:127.0.0.1"
|
||||||
|
openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out server.crt -days 500 -sha256 \
|
||||||
|
-extfile <(echo -e "authorityKeyIdentifier=keyid,issuer\nbasicConstraints=CA:FALSE\nkeyUsage = digitalSignature, keyEncipherment\nextendedKeyUsage=serverAuth\nsubjectAltName=IP:127.0.0.1")
|
||||||
|
|
||||||
|
rm rootCA.key intermediate.key *.csr *.srl
|
||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDKjCCAhKgAwIBAgIUQr7R8yN5+2and6ucUOPF6oIbD44wDQYJKoZIhvcNAQEL
|
||||||
|
BQAwFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMB4XDTI1MDIyODEyMDEzMFoXDTI3
|
||||||
|
MTEyNTEyMDEzMFowHzEdMBsGA1UEAwwUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwggEi
|
||||||
|
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDOGvN95wCkYO35qyJnf/RwTiDb
|
||||||
|
oEVaefKnZZny1JrO34MFjlAz8C/P5WwxNUzzbQLTPh5iTqFRU+vis6HPvV0HJEoI
|
||||||
|
wTfgBCZxcdY8fEIY96FGHLju3PzfxeJaVHyi+2cTtzU+oNp4OFF8huApjYXjaV4y
|
||||||
|
pAirPbiiP/cgtcT4L5WErQi0aGZkq+1YqY2duNFNIGPTEcXV4iN4IhuD9dpqdKFg
|
||||||
|
H0wmZDgH+VE/5ACXovU8j5cxCKOJGxTVMKVZlvxPH3w69Z85x3o5AAnyxwo8E2zo
|
||||||
|
TC1FJ1eFLsmYLZki6cGBzSkIl5QlLGHakWYh+JLu/pkfTL8t+AkY3hZJM96ZAgMB
|
||||||
|
AAGjZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1Ud
|
||||||
|
DgQWBBSmAyntm95+KoyL3ffLBXZKSpk1VTAfBgNVHSMEGDAWgBR93+rQFjh+RUFX
|
||||||
|
df4dbNcfS2hbTzANBgkqhkiG9w0BAQsFAAOCAQEABopVFLGQf/LFH+OKVCOT8FCC
|
||||||
|
y/+o1B/U5jXVvbfwlSGScaiJGQ94FsuH59XJCGySQj77ZVTeElBtntoLXmOCFjyF
|
||||||
|
jKHCDfUpB4nzeqNMvTDzuoYyPS8DhoGfEnaCgJyKf6GU4p41502gH8mQRB7azzL7
|
||||||
|
5jW0aFatCA6G6T1oogHZpHf0ice80C2JkFbWHSE9JxqARbTc06wCDBiSBFTGZQDO
|
||||||
|
JaBIbn6FL3zSkKcpwgJEqDRavVuoDUlJPDqtTzjf/fMQGGR2LUFkceJpsQqf1jrF
|
||||||
|
1yTtEZ8gjR2g2Vj6IszUAgbc87xR0AgyGDVckiUdhlX2Y6KCqo2cl9LfSVpqtw==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
Vendored
+20
@@ -0,0 +1,20 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDQjCCAiqgAwIBAgIUHT3JtqsYKs7NHv1LNyS9RYC7vsAwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwHzEdMBsGA1UEAwwUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwHhcNMjUwMjI4MTIw
|
||||||
|
MTMwWhcNMjYwNzEzMTIwMTMwWjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0G
|
||||||
|
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHwI/xSLgCuQrx+WsvupA8w4eMdSef
|
||||||
|
WGw523OJYPJkDYJGgSGsjVb9htba6vXYbGNohuluEAZIyT7GvmPezTokeVMkuSYT
|
||||||
|
lSV8xplFEtDlQhTzaI/cofbi7qtT91/5zS/w0JSaNosThGtZg/M4ZOiMj04m0NGK
|
||||||
|
Zz56l9Lpe/yM7fPda++D9xYEGSSdwK9CqqwF+cXN09d6IK1VINIIjT3Sdb9Sssok
|
||||||
|
GWmD7UUPLvwZ5379+HRs1K8AFXqvbkeWVYtrJwJMxJGVnNSeiqKGSmMEpP7tVNHl
|
||||||
|
s4V7oyQXd8KX+HpziiayjGy9giVteJJi/bAmUp+0+hTHBes5fOWI5JyDAgMBAAGj
|
||||||
|
gYAwfjAfBgNVHSMEGDAWgBSmAyntm95+KoyL3ffLBXZKSpk1VTAJBgNVHRMEAjAA
|
||||||
|
MAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHREECDAGhwR/
|
||||||
|
AAABMB0GA1UdDgQWBBTVdJE2lkGsNIU3LlEf3rN5fyaRkjANBgkqhkiG9w0BAQsF
|
||||||
|
AAOCAQEAo1klH9WMsPWTN9qN3tdud07eatulEKo/0okaph6MJ59ozseOzxrfpwL0
|
||||||
|
67Nr8yl+VwZqrRTBurp0n6G+n0j8UHfWjSrAqN4yUHl+heT0HpnLR2FE9YgZEmxR
|
||||||
|
bPfVbPBef/eJeE7/U6imfBYzzMajua+hg05sVHUNNdPaFOP+Xj47x8uQmf9w5/kf
|
||||||
|
MrylRUSgH5RRge4+2T5hmNM9tHfF6OfDHitrXnl+X6h/x/tkBvDcUXtKa5xuEcSg
|
||||||
|
WpmJKl3pKfXvdmCIrj9Vca+UD2Bntkk2jgDTLEPJAxMgrsQRhnUJclaunnd1NQbc
|
||||||
|
FmjFW7iaNvDVKt+vYqH8ff8U9iCB2g==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
Vendored
+28
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDHwI/xSLgCuQrx
|
||||||
|
+WsvupA8w4eMdSefWGw523OJYPJkDYJGgSGsjVb9htba6vXYbGNohuluEAZIyT7G
|
||||||
|
vmPezTokeVMkuSYTlSV8xplFEtDlQhTzaI/cofbi7qtT91/5zS/w0JSaNosThGtZ
|
||||||
|
g/M4ZOiMj04m0NGKZz56l9Lpe/yM7fPda++D9xYEGSSdwK9CqqwF+cXN09d6IK1V
|
||||||
|
INIIjT3Sdb9SssokGWmD7UUPLvwZ5379+HRs1K8AFXqvbkeWVYtrJwJMxJGVnNSe
|
||||||
|
iqKGSmMEpP7tVNHls4V7oyQXd8KX+HpziiayjGy9giVteJJi/bAmUp+0+hTHBes5
|
||||||
|
fOWI5JyDAgMBAAECggEADanzbrrds3n68LByD5LAeRea9xWwfyrqRE7pqVUTX5q4
|
||||||
|
9Z+xsP7+G1uU6Oa3qHVJm4XXA+tesq3peGjfpgb92i7ebB2qKB7EsLNZGqt91KDf
|
||||||
|
lALsDFib7cwLtjOuwgSyKdPqxl3Cx7QAL+Bhy9LDQZIv82HHY6NKV9J43/XWQcGK
|
||||||
|
KNZyS1o0vBWvt135YeE3qfQA9Ww8GI3jWyk49QDOVtVNZ1HRQpXPkt5exAyU8JpW
|
||||||
|
Y3Y5VqyEcKPBRlw/scEc5CRuzIP3P06Y+NEuuvnlnDt/BR/wyyuPiZoIqGXhXUKe
|
||||||
|
oDzEmtVrvB5RmhRc1PoS1l1GBfBfVqwkLUmeSitn8QKBgQD1pC6Ukiqtm7vyhirU
|
||||||
|
ynE+Dik/gHBBW6fQalD7yZwStid1+HfIvoU+RB0wyPUVwbu7eOoMnjG2ChLYX7cV
|
||||||
|
UhAcu5ZWFhlc8OS5bGGMI99d4ueBqTYONqnCV3DhiWZhs8OesiF3hohE6jV3G5xC
|
||||||
|
ra8DzImMpujyMIWXvOwnPLRWCQKBgQDQLPwTvx472XbY1aWviQkzxMgxP8l9D+lO
|
||||||
|
nBaybarLxDa+89RMWidgJX2kGM7i6FgicGgSNpNDIYNLrVZYAz11PP6o2Oqa9ZaR
|
||||||
|
5IvnfhP1iOwSgIoC8weNSE+Y0Lw/w0IOW71+XsfgswhJG45eXV8hRqYpUEkjjl1x
|
||||||
|
nQM0hCshKwKBgCQDFfEiHK+nDT7Y/J6Fr2Rxnwp4QfzS+x9K9uRzAjacDdz1uFnt
|
||||||
|
1Ir0YXMtgwDVjjhF2cpPxunxQCIIpkax6TrNJZUpWD6P8nhcs1BgUfbptRcFP6+F
|
||||||
|
xA2B1EK8ag4Y1K0HYHCtgHzZ+Uyk95uu6uGbsu6z6aLYCj3crKJz+9xBAoGACOdT
|
||||||
|
pLiQ33hul9mTa42N4jPxaAHVaU7r6JvOcLU2D98FhGdDVjyo4HjaBdG1z4imdFqg
|
||||||
|
aN8Cr2VYiz0Pq1YAI+qG7cvRRO1qEjVXMoB29BJ2Hlh3Dqc8VHOaS+vpkUSVp62O
|
||||||
|
zj/ZhqfBm/bcwPZ3YiH2a1/usOGe54QSpgVdHt8CgYBr6xpKQSrQvgtiuv9kCzI+
|
||||||
|
WQYP2Xxj+zsQb29hagXY/JllKOl5aDGz2qMV2RgOWZabB/xxkAeTF4A26wWrmcq0
|
||||||
|
wsv96jEl1MtI3lB3bi/8Y/tctkNsp0drvDZdfnGMpzxDafECKdxdFvhO63p7yBOA
|
||||||
|
LG1OvFTywkxBuOUKsNsErQ==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDHzCCAgegAwIBAgIUUmc9nWf4fhGFNd0oCNE0CzOXMaEwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMB4XDTI1MDIyODEyMDEyOVoXDTI3
|
||||||
|
MTIxOTEyMDEyOVowFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMIIBIjANBgkqhkiG
|
||||||
|
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw5ZFoDGTL0YyrwEA7qTu32cur0sQXDV86Xwl
|
||||||
|
G0ilk2DXXH4F70ruTTW3NG0Rniw/rt2jzJADo1Tlosq9eJKQGQKAr21N5kjhlU3J
|
||||||
|
8nFBK+1WJyG27EvyeqZOCucXOJaAm0HSbhlT0MYpZ4kzuxmOUPmTsJmt2BtK+uRU
|
||||||
|
3LlXtzyZnJo53azQuLZz26tBGd9LXsBUMi+KJ0eX1HPluIT3o+nslnJZaqGySLKm
|
||||||
|
cJnLf9hio+rAwFBb8sgDdzeI7jqZ2bmAGPJBYpIT/dIxuZUkgTfX+OMp2g3RnQea
|
||||||
|
M0w0UjhbbQeAJONH9HGREDdp7tYtuyuBbE4miNTyjSsouqk6AwIDAQABo2MwYTAd
|
||||||
|
BgNVHQ4EFgQUfd/q0BY4fkVBV3X+HWzXH0toW08wHwYDVR0jBBgwFoAUfd/q0BY4
|
||||||
|
fkVBV3X+HWzXH0toW08wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw
|
||||||
|
DQYJKoZIhvcNAQELBQADggEBAGLUya6xwaCwUPWHNOjlvGBGzGwAmSstJzh5o77O
|
||||||
|
XTTvyCwb0p80AnS9XoX3An5e4ePzw69mEw6RzfVLIex7fCRDekqPFuSWjVagKGJc
|
||||||
|
G7nvCqdHoCh2z1Jkb9gFpYPd6p45dtLWBw9e9/t9cFHtDR6stC16/Hy8cLzEIr0c
|
||||||
|
EWxCNdJdZW+soJivaZQeVWtlMXxVpGIs8i33CAFYufZCTKMgyYRegZuMQ676OcDE
|
||||||
|
9VSi2vJnnhdn7OBip82xX3NDQrwVt60fvFMr25cPOlzhXRY4mQLslGOleqT3sSPV
|
||||||
|
DVJnOBBmdjgFQQ8BO7rFUNGGOaUcEZp0HLRwxPZyc6OBCIg=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
Vendored
+20
@@ -0,0 +1,20 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDOjCCAiKgAwIBAgIUQr7R8yN5+2and6ucUOPF6oIbD48wDQYJKoZIhvcNAQEL
|
||||||
|
BQAwFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMB4XDTI1MDIyODEyMDEzMFoXDTI2
|
||||||
|
MDcxMzEyMDEzMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B
|
||||||
|
AQEFAAOCAQ8AMIIBCgKCAQEA4oL2hAPQlDVaNJru5fIstkpoVSuam0vpswC7ciRc
|
||||||
|
XQRjF3q8kjtIA7+jdySsKJqOLGnybDX3awvRyKMEjq11IfnZLjZc+FzTlA+x4z0h
|
||||||
|
MHb0GiBFXKNzrExGI9F0KEPtFxcMIqZ119LY2ReexxWkZBQYlgTepaevp71za4c2
|
||||||
|
n4Zy1+0iS5+uklZ4ANKMTBGlN76Qgt530VnpNiIeUbiUzY58Vx4q7kFcUv/oSz8p
|
||||||
|
rbXr+/GGpAjrOc6/JsezRE8YK2po60dvV80TJ2Jt6pduvF7OSQnq/v4mJl1xuXKl
|
||||||
|
Byo9HLbeu3BuVRWQs2/EwEzx5kX3Ugysl9Bm44K2yKe9/QIDAQABo4GAMH4wHwYD
|
||||||
|
VR0jBBgwFoAUfd/q0BY4fkVBV3X+HWzXH0toW08wCQYDVR0TBAIwADALBgNVHQ8E
|
||||||
|
BAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0RBAgwBocEfwAAATAdBgNV
|
||||||
|
HQ4EFgQUe0rTTfWjho3hgeLTnajTCpddo2MwDQYJKoZIhvcNAQELBQADggEBAIR2
|
||||||
|
5zkA7rPnddxCunsz8Jjq3wyhR/KiAFz+RGeFeiXDkF2fWr7QIQ9KbFbv8tpfXR7P
|
||||||
|
B75bY0sXwutHMB2sZDi92cH5sthNBfp19fI35cxcU4oTPxp4UZJKEiA3Qx8y73CX
|
||||||
|
NJu1009nPdOJNlIboDGAFdZ5SH6RCh+YcQZ68kjHPWBIpXxLbs9FN3QmpbAvtLh1
|
||||||
|
PoPaSy7IjKmxm1u+Lf6tyIn2IiB3MiynaB3OKvbkLCseM/5SZKMk6WKSDWopOCJr
|
||||||
|
xciPOc+yeLz5I2Omn0uViOIIciqjlgxncWAyNtDgvJcecwqB2cPiIhk6GY0QZ1uM
|
||||||
|
e7KoqGzWXvWLqJ13a9U=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
Vendored
+28
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDigvaEA9CUNVo0
|
||||||
|
mu7l8iy2SmhVK5qbS+mzALtyJFxdBGMXerySO0gDv6N3JKwomo4safJsNfdrC9HI
|
||||||
|
owSOrXUh+dkuNlz4XNOUD7HjPSEwdvQaIEVco3OsTEYj0XQoQ+0XFwwipnXX0tjZ
|
||||||
|
F57HFaRkFBiWBN6lp6+nvXNrhzafhnLX7SJLn66SVngA0oxMEaU3vpCC3nfRWek2
|
||||||
|
Ih5RuJTNjnxXHiruQVxS/+hLPymttev78YakCOs5zr8mx7NETxgramjrR29XzRMn
|
||||||
|
Ym3ql268Xs5JCer+/iYmXXG5cqUHKj0ctt67cG5VFZCzb8TATPHmRfdSDKyX0Gbj
|
||||||
|
grbIp739AgMBAAECggEADgUIbbAFbJbyHV1q5Jqc/9oSeRW40lyG0Mh+fEMZ4Gam
|
||||||
|
x3ZA+QAS+1W/hV6ktTf+YsCv+4NKQWWQN3iM41PYcyDmu1XWt/Hu5TQk0NQgxhd8
|
||||||
|
EP3nAnkvbf5OkmWiveHuaRvJFCqfZ/Cp8U3lSvHg+edwhMs1CKXHWSeAXwBrIMEb
|
||||||
|
ajpxuD3B/NT/CGmKnj3cgAuIbvNHVIcwu8ACbpczDL++vi7KrWmOJn1QzSlUlNFi
|
||||||
|
fsgnF0heO5Uff4vkjXU84INQxOP3tbvXcDNiwDewZy75h2d3Pv+ku8GoZYWFUXSJ
|
||||||
|
yKtafJMJUD0kJMuKhkzrwYcQGY6ioSYisPK+JoungQKBgQD8fWmuHwCXbM4Ckyns
|
||||||
|
Wg4f+kG8d+wypgIs6ENmgr9UnNB0N6n7nO7v/4l/l7IN9CQQmdtSvek2ytk5rGBM
|
||||||
|
XUAWxZaokE3MecxR0EUJx42k/k3dN4XgU/YNk4D6/wpEsyUATE4nIFDVjxE+Jc07
|
||||||
|
CZ2CUWKyxTPGz2kfHnEQ0vFiYQKBgQDlqRiGlJ6c99zTas2wrvr+50aQhn6BryDK
|
||||||
|
kjGM6woPnnwMq+Jy6vum3o0cU+iNNeFAijShXo2XR3iZJcoJ2sPhy3dRWdBNdFyy
|
||||||
|
hwxgD0cXzEjQL0M03DPDykTnM7ZvE6KUZjnxJZkytJHLKapoGzxBH9656zx5qnuH
|
||||||
|
MPYwTWg5HQKBgDKBD4OBtgeT/v0q3KbnOI4S69U8E6Xp6ON8rgayPn05RMUKYVjw
|
||||||
|
AidFcQZxnG8IF7KuY92AGUcZeiv8G+MKgAhOC526B6XP8xumUjjrjpyjNYX7Vi8R
|
||||||
|
/FSo3ZLXMwGc59jQao2O/DxLesJ4oz2c5cGsb9acdYfd8wQDfdBEsX3hAoGAc4Pu
|
||||||
|
NiMi9MknZZ/e/fPFg9lIgQFlOE2iLMID8mF2mgyZULZUHIFdOr3ONGVwHzbuqcva
|
||||||
|
VSB+D41/d2iuiu5igHwa8+w8/fh9d7691sNYevvh0/Ux1LC9yMlAhxpXtN8nc4VH
|
||||||
|
t6e1uu9gNdQrRloMoKUrHlDYBkpd/838xqbouXECgYEArwn+eXKD5zgNN4jEbNBp
|
||||||
|
ygIp+Oh2abt+CNQjfLUa+qon5ziH53mHixJ2hpaOa6Rxxu9R2ZgNLtbodm+ccD8z
|
||||||
|
ZNA7Z0rApAwfuhD8zIzkZ4HuARN8eopYmTubpzDkAcfRWhw1EBDQc0V6trl+EJsK
|
||||||
|
xfbmGepRVWXw2dLmxhA9/zM=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package testserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
"github.com/onsi/ginkgo/v2"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns a new httptest.Server that handles the
|
||||||
|
// authentication requests neeeded to create a new client. Any requests not
|
||||||
|
// made to the /actions/runners/registration-token or
|
||||||
|
// /actions/runner-registration endpoints will be handled by the provided
|
||||||
|
// handler. The returned server is started and will be automatically closed
|
||||||
|
// when the test ends.
|
||||||
|
//
|
||||||
|
// TODO: this uses ginkgo interface _only_ to support our current controller tests
|
||||||
|
func New(t ginkgo.GinkgoTInterface, handler http.Handler, options ...actionsServerOption) *actionsServer {
|
||||||
|
s := NewUnstarted(t, handler, options...)
|
||||||
|
s.Start()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this uses ginkgo interface _only_ to support our current controller tests
|
||||||
|
func NewUnstarted(t ginkgo.GinkgoTInterface, handler http.Handler, options ...actionsServerOption) *actionsServer {
|
||||||
|
s := httptest.NewUnstartedServer(handler)
|
||||||
|
server := &actionsServer{
|
||||||
|
Server: s,
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
server.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
server.setDefaults(t)
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
option(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// handle getRunnerRegistrationToken
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/runners/registration-token") {
|
||||||
|
server.runnerRegistrationTokenHandler(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle getActionsServiceAdminConnection
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/actions/runner-registration") {
|
||||||
|
server.actionRegistrationTokenHandler(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
server.Config.Handler = h
|
||||||
|
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionsServerOption func(*actionsServer)
|
||||||
|
|
||||||
|
func WithActionsToken(token string) actionsServerOption {
|
||||||
|
return func(s *actionsServer) {
|
||||||
|
s.token = token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithRunnerRegistrationTokenHandler(h http.HandlerFunc) actionsServerOption {
|
||||||
|
return func(s *actionsServer) {
|
||||||
|
s.runnerRegistrationTokenHandler = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithActionsRegistrationTokenHandler(h http.HandlerFunc) actionsServerOption {
|
||||||
|
return func(s *actionsServer) {
|
||||||
|
s.actionRegistrationTokenHandler = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionsServer struct {
|
||||||
|
*httptest.Server
|
||||||
|
|
||||||
|
token string
|
||||||
|
runnerRegistrationTokenHandler http.HandlerFunc
|
||||||
|
actionRegistrationTokenHandler http.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionsServer) setDefaults(t ginkgo.GinkgoTInterface) {
|
||||||
|
if s.runnerRegistrationTokenHandler == nil {
|
||||||
|
s.runnerRegistrationTokenHandler = func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
w.Write([]byte(`{"token":"token"}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.actionRegistrationTokenHandler == nil {
|
||||||
|
s.actionRegistrationTokenHandler = func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if s.token == "" {
|
||||||
|
s.token = DefaultActionsToken(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
w.Write([]byte(`{"url":"` + s.URL + `/tenant/123/","token":"` + s.token + `"}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionsServer) ConfigURLForOrg(org string) string {
|
||||||
|
return s.URL + "/" + org
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultActionsToken(t ginkgo.GinkgoTInterface) string {
|
||||||
|
claims := &jwt.RegisteredClaims{
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute)),
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)),
|
||||||
|
Issuer: "123",
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||||
|
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(samplePrivateKey))
|
||||||
|
require.NoError(t, err)
|
||||||
|
tokenString, err := token.SignedString(privateKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return tokenString
|
||||||
|
}
|
||||||
|
|
||||||
|
const samplePrivateKey = `-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQC7tgquvNIp+Ik3
|
||||||
|
rRVZ9r0zJLsSzTHqr2dA6EUUmpRiQ25MzjMqKqu0OBwvh/pZyfjSIkKrhIridNK4
|
||||||
|
DWnPfPWHE2K3Muh0X2sClxtqiiFmXsvbiTzhUm5a+zCcv0pJCWYnKi0HmyXpAXjJ
|
||||||
|
iN8mWliZN896verVYXWrod7EaAnuST4TiJeqZYW4bBBG81fPNc/UP4j6CKAW8nx9
|
||||||
|
HtcX6ApvlHeCLZUTW/qhGLO0nLKoEOr3tXCPW5VjKzlm134Dl+8PN6f1wv6wMAoA
|
||||||
|
lo7Ha5+c74jhPL6gHXg7cRaHQmuJCJrtl8qbLkFAulfkBixBw/6i11xoM/MOC64l
|
||||||
|
TWmXqrxTAgMBAAECgf9zYlxfL+rdHRXCoOm7pUeSPL0dWaPFP12d/Z9LSlDAt/h6
|
||||||
|
Pd+eqYEwhf795SAbJuzNp51Ls6LUGnzmLOdojKwfqJ51ahT1qbcBcMZNOcvtGqZ9
|
||||||
|
xwLG993oyR49C361Lf2r8mKrdrR5/fW0B1+1s6A+eRFivqFOtsOc4V4iMeHYsCVJ
|
||||||
|
hM7yMu0UfpolDJA/CzopsoGq3UuQlibUEUxKULza06aDjg/gBH3PnP+fQ1m0ovDY
|
||||||
|
h0pX6SCq5fXVJFS+Pbpu7j2ePNm3mr0qQhrUONZq0qhGN/piCbBZe1CqWApyO7nA
|
||||||
|
B95VChhL1eYs1BKvQePh12ap83woIUcW2mJF2F0CgYEA+aERTuKWEm+zVNKS9t3V
|
||||||
|
qNhecCOpayKM9OlALIK/9W6KBS+pDsjQQteQAUAItjvLiDjd5KsrtSgjbSgr66IP
|
||||||
|
b615Pakywe5sdnVGzSv+07KMzuFob9Hj6Xv9als9Y2geVhUZB2Frqve/UCjmC56i
|
||||||
|
zuQTSele5QKCSSTFBV3423cCgYEAwIBv9ChsI+mse6vPaqSPpZ2n237anThMcP33
|
||||||
|
aS0luYXqMWXZ0TQ/uSmCElY4G3xqNo8szzfy6u0HpldeUsEUsIcBNUV5kIIb8wKu
|
||||||
|
Zmgcc8gBIjJkyUJI4wuz9G/fegEUj3u6Cttmmj4iWLzCRscRJdfGpqwRIhOGyXb9
|
||||||
|
2Rur5QUCgYAGWIPaH4R1H4XNiDTYNbdyvV1ZOG7cHFq89xj8iK5cjNzRWO7RQ2WX
|
||||||
|
7WbpwTj3ePmpktiBMaDA0C5mXfkP2mTOD/jfCmgR6f+z2zNbj9zAgO93at9+yDUl
|
||||||
|
AFPm2j7rQgBTa+HhACb+h6HDZebDMNsuqzmaTWZuJ+wr89VWV5c17QKBgH3jwNNQ
|
||||||
|
mCAIUidynaulQNfTOZIe7IMC7WK7g9CBmPkx7Y0uiXr6C25hCdJKFllLTP6vNWOy
|
||||||
|
uCcQqf8LhgDiilBDifO3op9xpyuOJlWMYocJVkxx3l2L/rSU07PYcbKNAFAxXuJ4
|
||||||
|
xym51qZnkznMN5ei/CPFxVKeqHgaXDpekVStAoGAV3pSWAKDXY/42XEHixrCTqLW
|
||||||
|
kBxfaf3g7iFnl3u8+7Z/7Cb4ZqFcw0bRJseKuR9mFvBhcZxSErbMDEYrevefU9aM
|
||||||
|
APeCxEyw6hJXgbWKoG7Fw2g2HP3ytCJ4YzH0zNitHjk/1h4BG7z8cEQILCSv5mN2
|
||||||
|
etFcaQuTHEZyRhhJ4BU=
|
||||||
|
-----END PRIVATE KEY-----`
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package scaleset
|
||||||
|
|
||||||
|
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"`
|
||||||
|
JobID string `json:"jobId"`
|
||||||
|
JobWorkflowRef string `json:"jobWorkflowRef"`
|
||||||
|
JobDisplayName string `json:"jobDisplayName"`
|
||||||
|
WorkflowRunID int64 `json:"workflowRunId"`
|
||||||
|
EventName string `json:"eventName"`
|
||||||
|
RequestLabels []string `json:"requestLabels"`
|
||||||
|
QueueTime time.Time `json:"queueTime"`
|
||||||
|
ScaleSetAssignTime time.Time `json:"scaleSetAssignTime"`
|
||||||
|
RunnerAssignTime time.Time `json:"runnerAssignTime"`
|
||||||
|
FinishTime time.Time `json:"finishTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package scaleset_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/actions/scaleset"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserAgentInfoString(t *testing.T) {
|
||||||
|
userAgentInfo := scaleset.UserAgentInfo{
|
||||||
|
Version: "0.1.0",
|
||||||
|
CommitSHA: "1234567890abcdef",
|
||||||
|
ScaleSetID: 10,
|
||||||
|
HasProxy: true,
|
||||||
|
Subsystem: "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
userAgent := userAgentInfo.String()
|
||||||
|
expectedProduct := "actions-runner-controller/0.1.0 (1234567890abcdef; test)"
|
||||||
|
assert.Contains(t, userAgent, expectedProduct)
|
||||||
|
expectedScaleSet := "ScaleSetID/10 (Proxy/enabled)"
|
||||||
|
assert.Contains(t, userAgent, expectedScaleSet)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user