Skip to content

Commit c82f3a8

Browse files
Merge pull request #450 from bitrise-io/ACI-5269-bazel-credential-helper-token-refresh
fix: ACI-5269 refresh the auth token in the Bazel credential helper
2 parents 98937cc + 07cff5b commit c82f3a8

30 files changed

Lines changed: 1698 additions & 334 deletions

cmd/auth/auth.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ func probeKeychain() credAudit {
472472
}
473473

474474
audit := credAudit{state: sourcePopulated, workspaceID: creds.WorkspaceID, authToken: creds.AuthToken, username: creds.Username}
475-
if desc := configcommon.DescribeKeychainCredentials(creds); desc.IsOAuthLogin {
475+
if desc := configcommon.DescribeStoredCredentials(configcommon.AuthSourceKeychain, creds); desc.IsOAuthLogin {
476476
audit.note = desc.Detail()
477477
}
478478

@@ -489,7 +489,12 @@ func probeFileStore() credAudit {
489489
return credAudit{state: sourceAbsent, note: "credentials block present but empty"}
490490
}
491491

492-
return credAudit{state: sourcePopulated, workspaceID: creds.WorkspaceID, authToken: creds.AuthToken, username: creds.Username}
492+
audit := credAudit{state: sourcePopulated, workspaceID: creds.WorkspaceID, authToken: creds.AuthToken, username: creds.Username}
493+
if desc := configcommon.DescribeStoredCredentials(configcommon.AuthSourceFile, creds); desc.IsOAuthLogin {
494+
audit.note = desc.Detail()
495+
}
496+
497+
return audit
493498
}
494499

495500
func probeMultiplatform() credAudit {

cmd/common/auth_hydrate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func hydrateStoredAuth(ctx context.Context) {
1717
return
1818
}
1919
_, source, _ := configcommon.ResolveAuthConfig(envs)
20-
if source != configcommon.AuthSourceKeychain && source != configcommon.AuthSourceFile {
20+
if !source.StoreManaged() {
2121
return
2222
}
2323
logger := log.NewLogger(log.WithDebugLog(IsDebugLogMode))

cmd/get/get.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package get
22

33
import (
4+
"context"
45
"fmt"
56

67
"github.com/spf13/cobra"
@@ -22,13 +23,17 @@ var getCmd = &cobra.Command{
2223
SilenceErrors: true,
2324
// Bazel spawns the helper N times in parallel with a tight per-invocation
2425
// timeout — override the root PersistentPreRun (version check, stored-auth
25-
// hydration) with a no-op so the helper fast-paths straight to Run. The
26-
// helper resolves credentials via configcommon.ResolveAuthConfig(envs),
27-
// which walks env → keychain → file without needing hydration to have run.
26+
// hydration) with a no-op. The helper does its own expiry-aware refresh,
27+
// bounded by Budget, and the root's logging would corrupt stdout anyway.
2828
PersistentPreRun: func(*cobra.Command, []string) {},
2929
RunE: func(cmd *cobra.Command, _ []string) error {
30-
if err := bazelcredhelper.Run(cmd.InOrStdin(), cmd.OutOrStdout(), utils.AllEnvs()); err != nil {
31-
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), err.Error())
30+
ctx, cancel := context.WithTimeout(cmd.Context(), bazelcredhelper.Budget)
31+
defer cancel()
32+
33+
stderr := cmd.ErrOrStderr()
34+
resolve := bazelcredhelper.NewResolver(utils.AllEnvs(), stderr)
35+
if err := bazelcredhelper.Run(ctx, cmd.InOrStdin(), cmd.OutOrStdout(), resolve); err != nil {
36+
_, _ = fmt.Fprintln(stderr, err.Error())
3237

3338
return fmt.Errorf("run bazel credential helper: %w", err)
3439
}

cmd/get/get_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//go:build unit
2+
3+
package get
4+
5+
import (
6+
"bytes"
7+
"encoding/json"
8+
"strings"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
keyring "github.com/zalando/go-keyring"
14+
15+
configcommon "github.com/bitrise-io/bitrise-build-cache-cli/v3/internal/config/common"
16+
_ "github.com/bitrise-io/bitrise-build-cache-cli/v3/internal/config/multiplatform" // registers the config-file credential readers
17+
)
18+
19+
// A single stray log line on stdout breaks every build using the helper.
20+
func TestGetCmd_StdoutIsOnlyTheJSONResponse(t *testing.T) {
21+
keyring.MockInit()
22+
t.Setenv("HOME", t.TempDir())
23+
t.Setenv(configcommon.EnvAuthToken, "test-token")
24+
t.Setenv(configcommon.EnvWorkspaceID, "ws-1")
25+
26+
var stdout, stderr bytes.Buffer
27+
getCmd.SetContext(t.Context())
28+
getCmd.SetIn(strings.NewReader(`{"uri":"https://bitrise-accelerate.services.bitrise.io"}`))
29+
getCmd.SetOut(&stdout)
30+
getCmd.SetErr(&stderr)
31+
32+
require.NoError(t, getCmd.RunE(getCmd, nil))
33+
34+
lines := strings.Split(strings.TrimRight(stdout.String(), "\n"), "\n")
35+
require.Len(t, lines, 1, "stdout must carry the JSON response and nothing else, got:\n%s", stdout.String())
36+
37+
var resp map[string]any
38+
require.NoError(t, json.Unmarshal([]byte(lines[0]), &resp))
39+
assert.Contains(t, resp, "headers")
40+
assert.Empty(t, stderr.String())
41+
}

cmd/xcode/proxy_lock.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package xcode
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strconv"
9+
"strings"
10+
11+
"github.com/bitrise-io/go-utils/v2/log"
12+
"github.com/gofrs/flock"
13+
14+
"github.com/bitrise-io/bitrise-build-cache-cli/v3/internal/config/xcelerate"
15+
"github.com/bitrise-io/bitrise-build-cache-cli/v3/internal/paths"
16+
"github.com/bitrise-io/bitrise-build-cache-cli/v3/internal/utils"
17+
)
18+
19+
// ErrProxyAlreadyRunning means another process holds the proxy lock.
20+
var ErrProxyAlreadyRunning = errors.New("xcelerate proxy already running")
21+
22+
// The file is both the lock and the pid advertisement: exclusion comes from the
23+
// kernel lock, while stop-proxy and the xcodebuild wrapper read the pid out of it.
24+
// It is never removed — unlinking a flock file lets two processes each believe they
25+
// hold it.
26+
func proxyPidFile(osProxy utils.OsProxy) string {
27+
return xcelerate.PathFor(osProxy, paths.ProxyPidFileName)
28+
}
29+
30+
// proxyOwner reports whether a proxy is serving, and the pid it advertised.
31+
//
32+
// The lock is the authority; the pid is only for the message. Deciding on the pid
33+
// first would report "not running" whenever the advertisement happens to be
34+
// mid-write — WriteFile truncates before it fills — and that answer starts a
35+
// second proxy.
36+
func proxyOwner(osProxy utils.OsProxy) (int, bool) {
37+
path := proxyPidFile(osProxy)
38+
39+
// Probing would create the file, so an absent one is answered without one.
40+
content, exists, err := osProxy.ReadFileIfExists(path)
41+
if err != nil || !exists {
42+
return 0, false
43+
}
44+
45+
probe := flock.New(path)
46+
free, err := probe.TryLock()
47+
if err != nil {
48+
return 0, false
49+
}
50+
if free {
51+
_ = probe.Unlock()
52+
53+
return 0, false
54+
}
55+
56+
// Held, so a pid we cannot parse means "running, identity unknown".
57+
pid, err := strconv.Atoi(strings.TrimSpace(content))
58+
if err != nil || pid <= 0 {
59+
return 0, true
60+
}
61+
62+
return pid, true
63+
}
64+
65+
// withProxySingleton runs serve as the only proxy on this machine. Contention is
66+
// not a failure: another proxy is already serving, so this one has nothing to do
67+
// and says so rather than erroring.
68+
//
69+
// The only way to take the lock, so the policy cannot be bypassed by a future
70+
// caller claiming it and deciding for itself.
71+
func withProxySingleton(osProxy utils.OsProxy, logger log.Logger, serve func() error) error {
72+
path := proxyPidFile(osProxy)
73+
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
74+
return fmt.Errorf("create proxy pid dir: %w", err)
75+
}
76+
77+
lock := flock.New(path)
78+
locked, err := lock.TryLock()
79+
if err != nil {
80+
return fmt.Errorf("lock %s: %w", path, err)
81+
}
82+
if !locked {
83+
pid, _ := proxyOwner(osProxy)
84+
logger.Infof("Skipping proxy startup: %s (pid: %d)", ErrProxyAlreadyRunning, pid)
85+
86+
return nil
87+
}
88+
defer func() {
89+
if err := lock.Unlock(); err != nil {
90+
logger.Warnf("Failed to release proxy lock: %s", err)
91+
}
92+
}()
93+
94+
// Advertised after the lock is held, so a reader never sees a pid that does not
95+
// own the proxy.
96+
if err := osProxy.WriteFile(path, []byte(strconv.Itoa(os.Getpid())), 0o644); err != nil {
97+
return fmt.Errorf("advertise proxy pid: %w", err)
98+
}
99+
100+
return serve()
101+
}

0 commit comments

Comments
 (0)