From f3ab19d2b8f5ceb2c0d9c3a9f8309eee77798543 Mon Sep 17 00:00:00 2001 From: Nikola Jokic Date: Wed, 17 Dec 2025 16:29:45 +0100 Subject: [PATCH] Use system info and build info as user agent, and expose DebugInfo (#41) * Refactor setting and unsetting system info * detect version on init * Update client.go * Update client_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update client.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client.go | 161 ++++++++---- client_test.go | 397 ++++++++++++++++++++++++------ examples/dockerscaleset/config.go | 27 +- examples/dockerscaleset/main.go | 8 +- listener/listener.go | 5 +- 5 files changed, 467 insertions(+), 131 deletions(-) diff --git a/client.go b/client.go index ff9be7c..6c85ea9 100644 --- a/client.go +++ b/client.go @@ -60,6 +60,17 @@ type Client struct { config *gitHubConfig logger *slog.Logger + buildInfo clientBuildInfo + // systemInfoMu guards setting system info. + systemInfoMu sync.Mutex + systemInfo SystemInfo + + // userAgent is computed based on buildInfo and systemInfo. + // userAgent should be re-computed every time client.SetSystemInfo + // is called. + // + // On every call, load the userAgent first locally so we can + // avoid lock-unlock on every call. userAgent atomic.Pointer[string] rootCAs *x509.CertPool @@ -68,6 +79,31 @@ type Client struct { proxyFunc ProxyFunc } +type clientBuildInfo struct { + version string + commitSHA string +} + +type debugInfo struct { + HasProxy bool `json:"has_proxy"` + HasRootCA bool `json:"has_root_ca"` + SystemInfo string `json:"system_info"` +} + +// DebugInfo returns a JSON string containing debug information about the client, +// including whether a proxy or custom root CA is configured, and the current system info. +// This method is intended for diagnostic and troubleshooting purposes. +func (c *Client) DebugInfo() string { + info := debugInfo{ + HasProxy: c.proxyFunc != nil, + HasRootCA: c.rootCAs != nil, + SystemInfo: *c.userAgent.Load(), + } + + b, _ := json.Marshal(info) + return string(b) +} + // GitHubAppAuth contains the GitHub App authentication credentials. All fields are required. type GitHubAppAuth struct { // ClientID is the Client ID of the application (app id also works) @@ -105,37 +141,27 @@ type ProxyFunc func(req *http.Request) (*url.URL, error) // Option defines a functional option for configuring the Client. type Option func(*Client) -// UserAgentInfo contains information for constructing the user agent string. -type UserAgentInfo struct { +// SystemInfo contains information about the system that uses the +// scaleset client. +// +// For example, when Actions Runner Controller uses the scaleset API, +// it will set the following: +// - System: "actions-runner-controller" +// - Version: "release-version" +// - CommitSHA: "sha-of-the-release-commit" +// - Subsystem: "listener" or "controller" +type SystemInfo struct { // System is the name of the scale set implementation - System string + System string `json:"system"` // Version is the version of the controller - Version string + Version string `json:"version"` // CommitSHA is the git commit SHA of the controller - CommitSHA string + CommitSHA string `json:"commit_sha"` // ScaleSetID is the ID of the scale set - ScaleSetID int + ScaleSetID int `json:"scale_set_id"` // Subsystem is the subsystem such as listener, controller, etc. // Each system may pick its own subsystem name. - Subsystem string -} - -func (u UserAgentInfo) String() string { - scaleSetID := "NA" - if u.ScaleSetID > 0 { - scaleSetID = strconv.Itoa(u.ScaleSetID) - } - - return fmt.Sprintf( - "%s/%s (%s; %s) ScaleSetID/%s; client (%s; %s)", - u.System, - u.Version, - u.CommitSHA, - u.Subsystem, - scaleSetID, - packageVersion, - commitSHA, - ) + Subsystem string `json:"subsystem"` } // WithLogger sets a custom logger for the Client. @@ -180,23 +206,45 @@ func WithProxy(proxyFunc ProxyFunc) Option { } } +type ClientWithGitHubAppConfig struct { + GitHubConfigURL string + GitHubAppAuth GitHubAppAuth + SystemInfo SystemInfo +} + // NewClientWithGitHubApp creates a new Client using GitHub App credentials. -func NewClientWithGitHubApp(githubConfigURL string, appCreds *GitHubAppAuth, options ...Option) (*Client, error) { +func NewClientWithGitHubApp(config ClientWithGitHubAppConfig, options ...Option) (*Client, error) { creds := &actionsAuth{ - app: appCreds, + app: &config.GitHubAppAuth, } - return newClient(githubConfigURL, creds, options...) + return newClient( + config.SystemInfo, + config.GitHubConfigURL, + creds, + options..., + ) +} + +type NewClientWithPersonalAccessTokenConfig struct { + GitHubConfigURL string + PersonalAccessToken string + SystemInfo SystemInfo } // NewClientWithPersonalAccessToken creates a new Client using a personal access token. -func NewClientWithPersonalAccessToken(githubConfigURL, personalAccessToken string, options ...Option) (*Client, error) { +func NewClientWithPersonalAccessToken(config NewClientWithPersonalAccessTokenConfig, options ...Option) (*Client, error) { creds := &actionsAuth{ - token: personalAccessToken, + token: config.PersonalAccessToken, } - return newClient(githubConfigURL, creds, options...) + return newClient( + config.SystemInfo, + config.GitHubConfigURL, + creds, + options..., + ) } -func newClient(githubConfigURL string, creds *actionsAuth, options ...Option) (*Client, error) { +func newClient(systemInfo SystemInfo, githubConfigURL string, creds *actionsAuth, options ...Option) (*Client, error) { config, err := parseGitHubConfigFromURL(githubConfigURL) if err != nil { return nil, fmt.Errorf("failed to parse githubConfigURL: %w", err) @@ -210,17 +258,14 @@ func newClient(githubConfigURL string, creds *actionsAuth, options ...Option) (* // retryablehttp defaults retryMax: 4, retryWaitMax: 30 * time.Second, + + buildInfo: clientBuildInfo{ + version: packageVersion, + commitSHA: commitSHA, + }, } - version, sha := detectModuleVersionAndCommit() - userAgent := UserAgentInfo{ - System: "scaleset-client", - Version: version, - CommitSHA: sha, - Subsystem: "NA", - ScaleSetID: 0, - }.String() - ac.userAgent.Store(&userAgent) + ac.SetSystemInfo(systemInfo) for _, option := range options { option(ac) @@ -267,10 +312,36 @@ func (c *Client) newRetryableHTTPClient() (*retryablehttp.Client, error) { return retryClient, nil } -// SetUserAgent updates the user agent -func (c *Client) SetUserAgent(info UserAgentInfo) { - v := info.String() - c.userAgent.Store(&v) +// SetSystemInfo updates the information about the system. +func (c *Client) SetSystemInfo(info SystemInfo) { + c.systemInfoMu.Lock() + defer c.systemInfoMu.Unlock() + c.systemInfo = info + c.setUserAgent() +} + +// SystemInfo returns the current system info that the client +// has configured. +func (c *Client) SystemInfo() SystemInfo { + c.systemInfoMu.Lock() + defer c.systemInfoMu.Unlock() + return c.systemInfo +} + +type userAgent struct { + SystemInfo + BuildVersion string `json:"build_version"` + BuildCommitSHA string `json:"build_commit_sha"` +} + +func (c *Client) setUserAgent() { + b, _ := json.Marshal(userAgent{ + SystemInfo: c.systemInfo, + BuildVersion: c.buildInfo.version, + BuildCommitSHA: c.buildInfo.commitSHA, + }) + userAgent := string(b) + c.userAgent.Store(&userAgent) } func (c *Client) do(req *http.Request) (*http.Response, error) { diff --git a/client_test.go b/client_test.go index 2815870..cdb1e3b 100644 --- a/client_test.go +++ b/client_test.go @@ -29,9 +29,11 @@ import ( const exampleRequestID = "5ddf2050-dae0-013c-9159-04421ad31b68" -var testUserAgent = UserAgentInfo{ - Version: "test", - CommitSHA: "test", +var testSystemInfo = SystemInfo{ + System: "test", + Subsystem: "subtest", + Version: "test-version", + CommitSHA: "test-sha", ScaleSetID: 1, } @@ -72,7 +74,11 @@ func TestNewGitHubAPIRequest(t *testing.T) { } for _, scenario := range scenarios { - client, err := newClient(scenario.configURL, nil) + client, err := newClient( + testSystemInfo, + scenario.configURL, + nil, + ) require.NoError(t, err) req, err := client.newGitHubAPIRequest(ctx, http.MethodGet, scenario.path, nil) @@ -81,20 +87,12 @@ func TestNewGitHubAPIRequest(t *testing.T) { } }) - t.Run("sets user agent header if present", func(t *testing.T) { - client, err := 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 := newClient("http://localhost/my-org", nil) + client, err := newClient( + testSystemInfo, + "http://localhost/my-org", + nil, + ) require.NoError(t, err) req, err := client.newGitHubAPIRequest( @@ -120,7 +118,11 @@ func TestNewActionsServiceRequest(t *testing.T) { token := defaultActionsToken(t) server := testserver.New(t, nil, testserver.WithActionsToken(token)) - client, err := newClient(server.ConfigURLForOrg("my-org"), defaultCreds) + client, err := newClient( + testSystemInfo, + server.ConfigURLForOrg("my-org"), + defaultCreds, + ) require.NoError(t, err) req, err := client.newActionsServiceRequest(ctx, http.MethodGet, "my-path", nil) @@ -133,7 +135,11 @@ func TestNewActionsServiceRequest(t *testing.T) { newToken := defaultActionsToken(t) server := testserver.New(t, nil, testserver.WithActionsToken(newToken)) - client, err := newClient(server.ConfigURLForOrg("my-org"), defaultCreds) + client, err := newClient( + testSystemInfo, + server.ConfigURLForOrg("my-org"), + defaultCreds, + ) require.NoError(t, err) client.actionsServiceAdminToken = "expiring-token" client.actionsServiceAdminTokenExpiresAt = time.Now().Add(59 * time.Second) @@ -160,6 +166,7 @@ func TestNewActionsServiceRequest(t *testing.T) { testserver.WithActionsRegistrationTokenHandler(unauthorizedHandler), ) client, err := newClient( + testSystemInfo, server.ConfigURLForOrg("my-org"), defaultCreds, WithRetryWaitMax(1*time.Millisecond), @@ -206,6 +213,7 @@ func TestNewActionsServiceRequest(t *testing.T) { testserver.WithActionsRegistrationTokenHandler(unauthorizedHandler), ) client, err := newClient( + testSystemInfo, server.ConfigURLForOrg("my-org"), defaultCreds, WithRetryWaitMax(1*time.Millisecond), @@ -227,7 +235,11 @@ func TestNewActionsServiceRequest(t *testing.T) { tokenThatShouldNotBeFetched := defaultActionsToken(t) server := testserver.New(t, nil, testserver.WithActionsToken(tokenThatShouldNotBeFetched)) - client, err := newClient(server.ConfigURLForOrg("my-org"), defaultCreds) + client, err := newClient( + testSystemInfo, + server.ConfigURLForOrg("my-org"), + defaultCreds, + ) require.NoError(t, err) client.actionsServiceAdminToken = "healthy-token" client.actionsServiceAdminTokenExpiresAt = time.Now().Add(1 * time.Hour) @@ -242,7 +254,11 @@ func TestNewActionsServiceRequest(t *testing.T) { t.Run("builds the right URL including api version", func(t *testing.T) { server := testserver.New(t, nil) - client, err := newClient(server.ConfigURLForOrg("my-org"), defaultCreds) + client, err := newClient( + testSystemInfo, + server.ConfigURLForOrg("my-org"), + defaultCreds, + ) require.NoError(t, err) req, err := client.newActionsServiceRequest(ctx, http.MethodGet, "/my/path?name=banana", nil) @@ -261,15 +277,19 @@ func TestNewActionsServiceRequest(t *testing.T) { t.Run("populates header", func(t *testing.T) { server := testserver.New(t, nil) - client, err := newClient(server.ConfigURLForOrg("my-org"), defaultCreds) + client, err := newClient( + testSystemInfo, + server.ConfigURLForOrg("my-org"), + defaultCreds, + ) require.NoError(t, err) - client.SetUserAgent(testUserAgent) + client.SetSystemInfo(testSystemInfo) 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, *client.userAgent.Load(), req.Header.Get("User-Agent")) assert.Equal(t, "application/json", req.Header.Get("Content-Type")) }) } @@ -292,7 +312,11 @@ func TestGetRunner(t *testing.T) { w.Write(response) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) got, err := client.GetRunner(ctx, runnerID) @@ -313,7 +337,13 @@ func TestGetRunner(t *testing.T) { actualRetry++ })) - client, err := newClient(server.configURLForOrg("my-org"), auth, WithRetryMax(retryMax), WithRetryWaitMax(retryWaitMax)) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + WithRetryMax(retryMax), + WithRetryWaitMax(retryWaitMax), + ) require.NoError(t, err) _, err = client.GetRunner(ctx, runnerID) @@ -341,7 +371,11 @@ func TestGetRunnerByName(t *testing.T) { w.Write(response) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) got, err := client.GetRunnerByName(ctx, runnerName) @@ -357,7 +391,11 @@ func TestGetRunnerByName(t *testing.T) { w.Write(response) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) got, err := client.GetRunnerByName(ctx, runnerName) @@ -379,7 +417,13 @@ func TestGetRunnerByName(t *testing.T) { actualRetry++ })) - client, err := newClient(server.configURLForOrg("my-org"), auth, WithRetryMax(retryMax), WithRetryWaitMax(retryWaitMax)) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + WithRetryMax(retryMax), + WithRetryWaitMax(retryWaitMax), + ) require.NoError(t, err) _, err = client.GetRunnerByName(ctx, runnerName) @@ -401,7 +445,11 @@ func TestDeleteRunner(t *testing.T) { w.WriteHeader(http.StatusNoContent) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) err = client.RemoveRunner(ctx, runnerID) @@ -423,6 +471,7 @@ func TestDeleteRunner(t *testing.T) { })) client, err := newClient( + testSystemInfo, server.configURLForOrg("my-org"), auth, WithRetryMax(retryMax), @@ -455,7 +504,11 @@ func TestGetRunnerGroupByName(t *testing.T) { w.Write(response) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) got, err := client.GetRunnerGroupByName(ctx, runnerGroupName) @@ -471,7 +524,11 @@ func TestGetRunnerGroupByName(t *testing.T) { w.Write(response) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) got, err := client.GetRunnerGroupByName(ctx, runnerGroupName) @@ -496,7 +553,11 @@ func TestGetRunnerScaleSet(t *testing.T) { w.Write(runnerScaleSetsResp) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) got, err := client.GetRunnerScaleSet(ctx, 1, scaleSetName) @@ -512,7 +573,11 @@ func TestGetRunnerScaleSet(t *testing.T) { url = *r.URL })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) _, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName) @@ -529,7 +594,11 @@ func TestGetRunnerScaleSet(t *testing.T) { w.WriteHeader(http.StatusNotFound) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) _, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName) @@ -542,7 +611,11 @@ func TestGetRunnerScaleSet(t *testing.T) { w.Header().Set("Content-Type", "text/plain") })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) _, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName) @@ -560,6 +633,7 @@ func TestGetRunnerScaleSet(t *testing.T) { retryWaitMax := 1 * time.Microsecond client, err := newClient( + testSystemInfo, server.configURLForOrg("my-org"), auth, WithRetryMax(retryMax), @@ -580,7 +654,11 @@ func TestGetRunnerScaleSet(t *testing.T) { w.Write(runnerScaleSetsResp) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) got, err := client.GetRunnerScaleSet(ctx, 1, scaleSetName) @@ -601,7 +679,11 @@ func TestGetRunnerScaleSet(t *testing.T) { w.Write(runnerScaleSetsResp) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) _, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName) @@ -627,7 +709,11 @@ func TestGetRunnerScaleSetByID(t *testing.T) { w.Write(rsl) })) - client, err := newClient(sservere.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + sservere.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) got, err := client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) @@ -645,7 +731,11 @@ func TestGetRunnerScaleSetByID(t *testing.T) { url = *r.URL })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) _, err = client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) @@ -661,7 +751,11 @@ func TestGetRunnerScaleSetByID(t *testing.T) { w.WriteHeader(http.StatusNotFound) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) _, err = client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) @@ -674,7 +768,11 @@ func TestGetRunnerScaleSetByID(t *testing.T) { w.Header().Set("Content-Type", "text/plain") })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) _, err = client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) @@ -691,6 +789,7 @@ func TestGetRunnerScaleSetByID(t *testing.T) { retryMax := 1 retryWaitMax := 1 * time.Microsecond client, err := newClient( + testSystemInfo, server.configURLForOrg("my-org"), auth, WithRetryMax(retryMax), @@ -712,7 +811,11 @@ func TestGetRunnerScaleSetByID(t *testing.T) { w.Write(rsl) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) got, err := client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) @@ -738,7 +841,11 @@ func TestCreateRunnerScaleSet(t *testing.T) { w.Write(rsl) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) got, err := client.CreateRunnerScaleSet(ctx, &runnerScaleSet) @@ -755,7 +862,11 @@ func TestCreateRunnerScaleSet(t *testing.T) { url = *r.URL })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) _, err = client.CreateRunnerScaleSet(ctx, &runnerScaleSet) @@ -772,7 +883,11 @@ func TestCreateRunnerScaleSet(t *testing.T) { w.Header().Set("Content-Type", "text/plain") })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) _, err = client.CreateRunnerScaleSet(ctx, &runnerScaleSet) @@ -792,6 +907,7 @@ func TestCreateRunnerScaleSet(t *testing.T) { retryWaitMax := 1 * time.Microsecond client, err := newClient( + testSystemInfo, server.configURLForOrg("my-org"), auth, WithRetryMax(retryMax), @@ -823,7 +939,11 @@ func TestUpdateRunnerScaleSet(t *testing.T) { w.Write(rsl) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) got, err := client.UpdateRunnerScaleSet(ctx, 1, &RunnerScaleSet{RunnerGroupID: 1}) @@ -843,7 +963,11 @@ func TestUpdateRunnerScaleSet(t *testing.T) { w.Write(rsl) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) _, err = client.UpdateRunnerScaleSet(ctx, 1, &runnerScaleSet) @@ -864,7 +988,11 @@ func TestDeleteRunnerScaleSet(t *testing.T) { w.WriteHeader(http.StatusNoContent) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) err = client.DeleteRunnerScaleSet(ctx, 10) @@ -879,7 +1007,11 @@ func TestDeleteRunnerScaleSet(t *testing.T) { w.Write([]byte(`{"message": "test error"}`)) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) err = client.DeleteRunnerScaleSet(ctx, 10) @@ -925,7 +1057,11 @@ func TestCreateMessageSession(t *testing.T) { w.Write(resp) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) got, err := client.CreateMessageSession(ctx, runnerScaleSet.ID, owner) @@ -959,7 +1095,11 @@ func TestCreateMessageSession(t *testing.T) { w.Write(resp) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) _, err = client.CreateMessageSession(ctx, runnerScaleSet.ID, owner) @@ -997,6 +1137,7 @@ func TestCreateMessageSession(t *testing.T) { wantRetries := retryMax + 1 client, err := newClient( + testSystemInfo, server.configURLForOrg("my-org"), auth, WithRetryMax(retryMax), @@ -1036,6 +1177,7 @@ func TestDeleteMessageSession(t *testing.T) { wantRetries := retryMax + 1 client, err := newClient( + testSystemInfo, server.configURLForOrg("my-org"), auth, WithRetryMax(retryMax), @@ -1076,6 +1218,7 @@ func TestRefreshMessageSession(t *testing.T) { wantRetries := retryMax + 1 client, err := newClient( + testSystemInfo, server.configURLForOrg("my-org"), auth, WithRetryMax(retryMax), @@ -1109,7 +1252,11 @@ func TestGetMessage(t *testing.T) { w.Write(response) })) - client, err := newClient(s.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + s.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) got, err := client.GetMessage(ctx, s.URL, token, 0, 10) @@ -1126,7 +1273,11 @@ func TestGetMessage(t *testing.T) { w.Write(response) })) - client, err := newClient(s.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + s.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) got, err := client.GetMessage(ctx, s.URL, token, 1, 10) @@ -1146,6 +1297,7 @@ func TestGetMessage(t *testing.T) { })) client, err := newClient( + testSystemInfo, server.configURLForOrg("my-org"), auth, WithRetryMax(retryMax), @@ -1163,7 +1315,11 @@ func TestGetMessage(t *testing.T) { w.WriteHeader(http.StatusUnauthorized) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) _, err = client.GetMessage(ctx, server.URL, token, 0, 10) @@ -1184,7 +1340,11 @@ func TestGetMessage(t *testing.T) { w.WriteHeader(http.StatusNotFound) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) _, err = client.GetMessage(ctx, server.URL, token, 0, 10) @@ -1198,7 +1358,11 @@ func TestGetMessage(t *testing.T) { w.Header().Set("Content-Type", "text/plain") })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) _, err = client.GetMessage(ctx, server.URL, token, 0, 10) @@ -1216,7 +1380,11 @@ func TestGetMessage(t *testing.T) { w.Header().Set("Content-Type", "text/plain") })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) _, err = client.GetMessage(ctx, server.URL, token, 0, 0) @@ -1243,7 +1411,11 @@ func TestDeleteMessage(t *testing.T) { w.WriteHeader(http.StatusNoContent) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) err = client.DeleteMessage(ctx, server.URL, token, runnerScaleSetMessage.MessageID) @@ -1255,7 +1427,11 @@ func TestDeleteMessage(t *testing.T) { w.WriteHeader(http.StatusUnauthorized) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) err = client.DeleteMessage(ctx, server.URL, token, 0) @@ -1271,7 +1447,11 @@ func TestDeleteMessage(t *testing.T) { w.Header().Set("Content-Type", "text/plain") })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) err = client.DeleteMessage(ctx, server.URL, token, runnerScaleSetMessage.MessageID) @@ -1290,6 +1470,7 @@ func TestDeleteMessage(t *testing.T) { retryMax := 1 client, err := newClient( + testSystemInfo, server.configURLForOrg("my-org"), auth, WithRetryMax(retryMax), @@ -1311,7 +1492,11 @@ func TestDeleteMessage(t *testing.T) { w.Write(rsl) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) err = client.DeleteMessage(ctx, server.URL, token, runnerScaleSetMessage.MessageID+1) @@ -1334,7 +1519,12 @@ func TestClientProxy(t *testing.T) { return proxyConfig.ProxyFunc()(req.URL) } - c, err := newClient("http://github.com/org/repo", nil, WithProxy(proxyFunc)) + c, err := newClient( + testSystemInfo, + "http://github.com/org/repo", + nil, + WithProxy(proxyFunc), + ) require.NoError(t, err) req, err := http.NewRequest(http.MethodGet, "http://example.com", nil) @@ -1361,7 +1551,11 @@ func TestGenerateJitRunnerConfig(t *testing.T) { server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Write(response) })) - client, err := newClient(server.configURLForOrg("my-org"), auth) + client, err := newClient( + testSystemInfo, + server.configURLForOrg("my-org"), + auth, + ) require.NoError(t, err) got, err := client.GenerateJitRunnerConfig(ctx, runnerSettings, 1) @@ -1382,6 +1576,7 @@ func TestGenerateJitRunnerConfig(t *testing.T) { })) client, err := newClient( + testSystemInfo, server.configURLForOrg("my-org"), auth, WithRetryMax(1), @@ -1402,7 +1597,11 @@ func TestClient_Do(t *testing.T) { })) defer server.Close() - client, err := newClient("https://localhost/org/repo", &actionsAuth{token: "token"}) + client, err := newClient( + testSystemInfo, + "https://localhost/org/repo", + &actionsAuth{token: "token"}, + ) require.NoError(t, err) req, err := http.NewRequest("GET", server.URL, nil) @@ -1428,7 +1627,11 @@ func TestClient_Do(t *testing.T) { })) defer server.Close() - client, err := newClient("https://localhost/org/repo", &actionsAuth{token: "token"}) + client, err := newClient( + testSystemInfo, + "https://localhost/org/repo", + &actionsAuth{token: "token"}, + ) require.NoError(t, err) req, err := http.NewRequest("GET", server.URL, nil) @@ -1561,7 +1764,11 @@ func TestServerWithSelfSignedCertificates(t *testing.T) { auth := &actionsAuth{ token: "token", } - client, err := newClient(configURL, auth) + client, err := newClient( + testSystemInfo, + configURL, + auth, + ) require.NoError(t, err) require.NotNil(t, client) @@ -1600,6 +1807,7 @@ func TestServerWithSelfSignedCertificates(t *testing.T) { require.True(t, pool.AppendCertsFromPEM(cert)) client, err := newClient( + testSystemInfo, configURL, auth, WithRootCAs(pool), @@ -1632,6 +1840,7 @@ func TestServerWithSelfSignedCertificates(t *testing.T) { require.True(t, pool.AppendCertsFromPEM(cert)) client, err := newClient( + testSystemInfo, configURL, auth, WithRootCAs(pool), @@ -1652,7 +1861,12 @@ func TestServerWithSelfSignedCertificates(t *testing.T) { token: "token", } - client, err := newClient(configURL, auth, WithoutTLSVerify()) + client, err := newClient( + testSystemInfo, + configURL, + auth, + WithoutTLSVerify(), + ) require.NoError(t, err) assert.NotNil(t, client) }) @@ -1673,8 +1887,9 @@ func startNewTLSTestServer(t *testing.T, certPath, keyPath string, handler http. return server } -func TestUserAgentInfoString(t *testing.T) { - userAgentInfo := UserAgentInfo{ +func TestUserAgent(t *testing.T) { + version, sha := detectModuleVersionAndCommit() + userAgentInfo := SystemInfo{ System: "actions-runner-controller", Version: "0.1.0", CommitSHA: "1234567890abcdef", @@ -1682,13 +1897,43 @@ func TestUserAgentInfoString(t *testing.T) { Subsystem: "test", } - userAgent := userAgentInfo.String() - expectedProduct := fmt.Sprintf( - "actions-runner-controller/0.1.0 (1234567890abcdef; test) ScaleSetID/10; client (%s; %s)", - packageVersion, - commitSHA, + client, err := newClient( + testSystemInfo, + "https://github.com/org/repo", + &actionsAuth{token: "token"}, ) - assert.Contains(t, userAgent, expectedProduct) + require.NoError(t, err, "failed to instantiate the client") + got := *client.userAgent.Load() + wantInfo := userAgent{ + SystemInfo: testSystemInfo, + BuildCommitSHA: sha, + BuildVersion: version, + } + b, err := json.Marshal(wantInfo) + require.NoError(t, err, "failed to marshal expected user agent") + want := string(b) + + assert.Equal(t, want, got) + + client.SetSystemInfo(SystemInfo{ + System: "actions-runner-controller", + Version: "0.1.0", + CommitSHA: "1234567890abcdef", + ScaleSetID: 10, + Subsystem: "test", + }) + + got = *client.userAgent.Load() + wantInfo = userAgent{ + SystemInfo: userAgentInfo, + BuildCommitSHA: sha, + BuildVersion: version, + } + b, err = json.Marshal(wantInfo) + require.NoError(t, err, "failed to marshal expected user agent after SetSystemInfo") + want = string(b) + + assert.Equal(t, want, got) } const samplePrivateKey = `-----BEGIN PRIVATE KEY----- diff --git a/examples/dockerscaleset/config.go b/examples/dockerscaleset/config.go index 3add468..829c8bc 100644 --- a/examples/dockerscaleset/config.go +++ b/examples/dockerscaleset/config.go @@ -59,12 +59,35 @@ func (c *Config) Validate() error { return nil } +// systemInfo serves as a base system info +func systemInfo(scaleSetID int) scaleset.SystemInfo { + return scaleset.SystemInfo{ + System: "dockerscaleset", + Subsystem: "dockerscaleset", + CommitSHA: "NA", // You can leverage build flags to set commit SHA + Version: "0.1.0", // You can leverage build flags to set version + ScaleSetID: scaleSetID, + } +} + func (c *Config) ScalesetClient() (*scaleset.Client, error) { if err := c.GitHubApp.Validate(); err == nil { - return scaleset.NewClientWithGitHubApp(c.RegistrationURL, &c.GitHubApp) + return scaleset.NewClientWithGitHubApp( + scaleset.ClientWithGitHubAppConfig{ + GitHubConfigURL: c.RegistrationURL, + GitHubAppAuth: c.GitHubApp, + SystemInfo: systemInfo(0), + }, + ) } - return scaleset.NewClientWithPersonalAccessToken(c.RegistrationURL, c.Token) + return scaleset.NewClientWithPersonalAccessToken( + scaleset.NewClientWithPersonalAccessTokenConfig{ + GitHubConfigURL: c.RegistrationURL, + PersonalAccessToken: c.Token, + SystemInfo: systemInfo(0), + }, + ) } func (c *Config) Logger() *slog.Logger { diff --git a/examples/dockerscaleset/main.go b/examples/dockerscaleset/main.go index c0c3664..8218bd2 100644 --- a/examples/dockerscaleset/main.go +++ b/examples/dockerscaleset/main.go @@ -92,13 +92,7 @@ func run(ctx context.Context, c Config) error { } // Set the user agent for the scaleset client now that we have the scale set ID - scalesetClient.SetUserAgent(scaleset.UserAgentInfo{ - System: "dockerscaleset", - Version: "0.1.0", - CommitSHA: "unknown", - ScaleSetID: scaleSet.ID, - Subsystem: scaleSet.Name, - }) + scalesetClient.SetSystemInfo(systemInfo(scaleSet.ID)) defer func() { logger.Info( diff --git a/listener/listener.go b/listener/listener.go index 7ab92dc..97c8653 100644 --- a/listener/listener.go +++ b/listener/listener.go @@ -133,7 +133,10 @@ func (l *Listener) Run(ctx context.Context, scaler Scaler) error { defer func() { l.logger.Debug("Deleting message session") if err := l.deleteMessageSession(); err != nil { - l.logger.Error("failed to delete message session", "error", err.Error()) + l.logger.Error( + "failed to delete message session", + slog.String("error", err.Error()), + ) } }()