Add Identifier to actions.Client (#2237)

This commit is contained in:
Francesco Renzi 2023-02-01 13:47:54 +00:00 committed by GitHub
parent 34efb9d585
commit 7414dc6568
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 197 additions and 171 deletions

View File

@ -165,6 +165,29 @@ func NewClient(githubConfigURL string, creds *ActionsAuth, options ...ClientOpti
return ac, nil
}
// Identifier returns a string to help identify a client uniquely.
// This is used for caching client instances and understanding when a config
// change warrants creating a new client. Any changes to Client that would
// require a new client should be reflected here.
func (c *Client) Identifier() string {
identifier := fmt.Sprintf("configURL:%q,", c.config.ConfigURL.String())
if c.creds.Token != "" {
identifier += fmt.Sprintf("token:%q", c.creds.Token)
}
if c.creds.AppCreds != nil {
identifier += fmt.Sprintf(
"appID:%q,installationID:%q,key:%q",
c.creds.AppCreds.AppID,
c.creds.AppCreds.AppInstallationID,
c.creds.AppCreds.AppPrivateKey,
)
}
return uuid.NewMD5(uuid.NameSpaceOID, []byte(identifier)).String()
}
func (c *Client) Do(req *http.Request) (*http.Response, error) {
resp, err := c.Client.Do(req)
if err != nil {

View File

@ -91,18 +91,19 @@ func TestGitHubConfig(t *testing.T) {
}
})
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",
}
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 := actions.ParseGitHubConfigFromURL(u)
require.Error(t, err)
assert.True(t, errors.Is(err, actions.ErrInvalidGitHubConfigURL))
}
for _, u := range invalidURLs {
_, err := actions.ParseGitHubConfigFromURL(u)
require.Error(t, err)
assert.True(t, errors.Is(err, actions.ErrInvalidGitHubConfigURL))
}
})
}
func TestGitHubConfig_GitHubAPIURL(t *testing.T) {

View File

@ -0,0 +1,111 @@
package actions_test
import (
"testing"
"github.com/actions/actions-runner-controller/github/actions"
"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 := &actions.ActionsAuth{
Token: "token",
}
oldClient, err := actions.NewClient(configURL, defaultCreds)
require.NoError(t, err)
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
newClient, err := actions.NewClient(scenario.url, defaultCreds)
require.NoError(t, err)
assert.NotEqual(t, oldClient.Identifier(), newClient.Identifier())
})
}
})
t.Run("credentials change", func(t *testing.T) {
defaultTokenCreds := &actions.ActionsAuth{
Token: "token",
}
defaultAppCreds := &actions.ActionsAuth{
AppCreds: &actions.GitHubAppAuth{
AppID: 123,
AppInstallationID: 123,
AppPrivateKey: "private key",
},
}
scenarios := []struct {
name string
old *actions.ActionsAuth
new *actions.ActionsAuth
}{
{
name: "different token",
old: defaultTokenCreds,
new: &actions.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: &actions.ActionsAuth{
AppCreds: &actions.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 := actions.NewClient(defaultConfigURL, scenario.old)
require.NoError(t, err)
newClient, err := actions.NewClient(defaultConfigURL, scenario.new)
require.NoError(t, err)
assert.NotEqual(t, oldClient.Identifier(), newClient.Identifier())
})
}
})
}

View File

@ -4,7 +4,6 @@ import (
"context"
"crypto/x509"
"fmt"
"net/url"
"strconv"
"sync"
@ -19,7 +18,7 @@ type MultiClient interface {
type multiClient struct {
// To lock adding and removing of individual clients.
mu sync.Mutex
clients map[ActionsClientKey]*actionsClientWrapper
clients map[ActionsClientKey]*Client
logger logr.Logger
userAgent string
@ -40,22 +39,14 @@ type ActionsAuth struct {
}
type ActionsClientKey struct {
ActionsURL string
Auth ActionsAuth
Identifier string
Namespace string
}
type actionsClientWrapper struct {
// To lock client usage when tokens are being refreshed.
mu sync.Mutex
client ActionsService
}
func NewMultiClient(userAgent string, logger logr.Logger) MultiClient {
return &multiClient{
mu: sync.Mutex{},
clients: make(map[ActionsClientKey]*actionsClientWrapper),
clients: make(map[ActionsClientKey]*Client),
logger: logger,
userAgent: userAgent,
}
@ -64,11 +55,6 @@ func NewMultiClient(userAgent string, logger logr.Logger) MultiClient {
func (m *multiClient) GetClientFor(ctx context.Context, githubConfigURL string, creds ActionsAuth, namespace string) (ActionsService, error) {
m.logger.Info("retrieve actions client", "githubConfigURL", githubConfigURL, "namespace", namespace)
parsedGitHubURL, err := url.Parse(githubConfigURL)
if err != nil {
return nil, err
}
if creds.Token == "" && creds.AppCreds == nil {
return nil, fmt.Errorf("no credentials provided. either a PAT or GitHub App credentials should be provided")
}
@ -77,34 +63,6 @@ func (m *multiClient) GetClientFor(ctx context.Context, githubConfigURL string,
return nil, fmt.Errorf("both PAT and GitHub App credentials provided. should only provide one")
}
key := ActionsClientKey{
ActionsURL: parsedGitHubURL.String(),
Namespace: namespace,
}
if creds.AppCreds != nil {
key.Auth = ActionsAuth{
AppCreds: creds.AppCreds,
}
}
if creds.Token != "" {
key.Auth = ActionsAuth{
Token: creds.Token,
}
}
m.mu.Lock()
defer m.mu.Unlock()
clientWrapper, has := m.clients[key]
if has {
m.logger.Info("using cache client", "githubConfigURL", githubConfigURL, "namespace", namespace)
return clientWrapper.client, nil
}
m.logger.Info("creating new client", "githubConfigURL", githubConfigURL, "namespace", namespace)
client, err := NewClient(
githubConfigURL,
&creds,
@ -115,11 +73,24 @@ func (m *multiClient) GetClientFor(ctx context.Context, githubConfigURL string,
return nil, err
}
m.clients[key] = &actionsClientWrapper{
mu: sync.Mutex{},
client: client,
m.mu.Lock()
defer m.mu.Unlock()
key := ActionsClientKey{
Identifier: client.Identifier(),
Namespace: namespace,
}
cachedClient, has := m.clients[key]
if has {
m.logger.Info("using cache client", "githubConfigURL", githubConfigURL, "namespace", namespace)
return cachedClient, nil
}
m.logger.Info("creating new client", "githubConfigURL", githubConfigURL, "namespace", namespace)
m.clients[key] = client
m.logger.Info("successfully created new client", "githubConfigURL", githubConfigURL, "namespace", namespace)
return client, nil

View File

@ -2,131 +2,51 @@ package actions
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/go-logr/logr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAddClient(t *testing.T) {
func TestMultiClientCaching(t *testing.T) {
logger := logr.Discard()
ctx := context.Background()
multiClient := NewMultiClient("test-user-agent", logger).(*multiClient)
ctx := context.Background()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "actions/runners/registration-token") {
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
token := "abc-123"
rt := &registrationToken{Token: &token}
if err := json.NewEncoder(w).Encode(rt); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
if strings.HasSuffix(r.URL.Path, "actions/runner-registration") {
w.Header().Set("Content-Type", "application/json")
url := "actions.github.com/abc"
jwt := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE"
adminConnInfo := &ActionsServiceAdminConnection{ActionsServiceUrl: &url, AdminToken: &jwt}
if err := json.NewEncoder(w).Encode(adminConnInfo); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
if strings.HasSuffix(r.URL.Path, "/access_tokens") {
w.Header().Set("Content-Type", "application/vnd.github+json")
t, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z07:00")
accessToken := &accessToken{
Token: "abc-123",
ExpiresAt: t,
}
if err := json.NewEncoder(w).Encode(accessToken); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}))
defer srv.Close()
want := 1
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{Token: "PAT"}, "namespace"); err != nil {
t.Fatal(err)
defaultNamespace := "default"
defaultConfigURL := "https://github.com/org/repo"
defaultCreds := &ActionsAuth{
Token: "token",
}
client, err := NewClient(defaultConfigURL, defaultCreds)
require.NoError(t, err)
want++ // New repo
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/actions", srv.URL), ActionsAuth{Token: "PAT"}, "namespace"); err != nil {
t.Fatal(err)
}
multiClient.clients[ActionsClientKey{client.Identifier(), defaultNamespace}] = client
// Repeat
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{Token: "PAT"}, "namespace"); err != nil {
t.Fatal(err)
}
// Verify that the client is cached
cachedClient, err := multiClient.GetClientFor(
ctx,
defaultConfigURL,
*defaultCreds,
defaultNamespace,
)
require.NoError(t, err)
assert.Equal(t, client, cachedClient)
assert.Len(t, multiClient.clients, 1)
want++ // New namespace
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{Token: "PAT"}, "other"); err != nil {
t.Fatal(err)
}
want++ // New pat
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{Token: "other"}, "other"); err != nil {
t.Fatal(err)
}
want++ // New org
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github", srv.URL), ActionsAuth{Token: "PAT"}, "other"); err != nil {
t.Fatal(err)
}
// No org, repo, enterprise
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v", srv.URL), ActionsAuth{Token: "PAT"}, "other"); err == nil {
t.Fatal(err)
}
want++ // Test keying on GitHub App
appAuth := &GitHubAppAuth{
AppID: 1,
AppPrivateKey: `-----BEGIN RSA PRIVATE KEY-----
MIICWgIBAAKBgHXfRT9cv9UY9fAAD4+1RshpfSSZe277urfEmPfX3/Og9zJYRk//
CZrJVD1CaBZDiIyQsNEzjta7r4UsqWdFOggiNN2E7ZTFQjMSaFkVgrzHqWuiaCBf
/BjbKPn4SMDmTzHvIe7Nel76hBdCaVgu6mYCW5jmuSH5qz/yR1U1J/WJAgMBAAEC
gYARWGWsSU3BYgbu5lNj5l0gKMXNmPhdAJYdbMTF0/KUu18k/XB7XSBgsre+vALt
I8r4RGKApoGif8P4aPYUyE8dqA1bh0X3Fj1TCz28qoUL5//dA+pigCRS20H7HM3C
ojoqF7+F+4F2sXmzFNd1NgY5RxFPYosTT7OnUiFuu2IisQJBALnMLe09LBnjuHXR
xxR65DDNxWPQLBjW3dL+ubLcwr7922l6ZIQsVjdeE0ItEUVRjjJ9/B/Jq9VJ/Lw4
g9LCkkMCQQCiaM2f7nYmGivPo9hlAbq5lcGJ5CCYFfeeYzTxMqum7Mbqe4kk5lgb
X6gWd0Izg2nGdAEe/97DClO6VpKcPbpDAkBTR/JOJN1fvXMxXJaf13XxakrQMr+R
Yr6LlSInykyAz8lJvlLP7A+5QbHgN9NF/wh+GXqpxPwA3ukqdSqhjhWBAkBn6mDv
HPgR5xrzL6XM8y9TgaOlJAdK6HtYp6d/UOmN0+Butf6JUq07TphRT5tXNJVgemch
O5x/9UKfbrc+KyzbAkAo97TfFC+mZhU1N5fFelaRu4ikPxlp642KRUSkOh8GEkNf
jQ97eJWiWtDcsMUhcZgoB5ydHcFlrBIn6oBcpge5
-----END RSA PRIVATE KEY-----`,
}
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{AppCreds: appAuth}, "other"); err != nil {
t.Fatal(err)
}
// Repeat last to verify GitHub App keys are mapped together
if _, err := multiClient.GetClientFor(ctx, fmt.Sprintf("%v/github/github", srv.URL), ActionsAuth{AppCreds: appAuth}, "other"); err != nil {
t.Fatal(err)
}
if len(multiClient.clients) != want {
t.Fatalf("GetClientFor: unexpected number of clients: got=%v want=%v", len(multiClient.clients), want)
}
// Asking for a different client results in creating and caching a new client
otherNamespace := "other"
newClient, err := multiClient.GetClientFor(
ctx,
defaultConfigURL,
*defaultCreds,
otherNamespace,
)
require.NoError(t, err)
assert.NotEqual(t, client, newClient)
assert.Len(t, multiClient.clients, 2)
}
func TestCreateJWT(t *testing.T) {