Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions pkg/cli/admin/mustgather/mustgather.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ const (
defaultMustGatherCommand = "/usr/bin/gather"
defaultVolumePercentage = 70
defaultSourceDir = "/must-gather/"

// defaultKeepAliveInterval is the default interval for client-side
// authenticated API probes that reset the OAuth access-token inactivity
// timer. Clusters with accessTokenInactivityTimeout (common in regulated
// environments) will revoke idle tokens; periodic probes prevent that
// during long-running log-follow connections that the API server does
// not count as activity.
defaultKeepAliveInterval = 30 * time.Second
)

var (
Expand Down Expand Up @@ -445,8 +453,9 @@ type MustGatherOptions struct {
Since time.Duration
SinceTime string

RsyncRshCmd string
clock clock.PassiveClock
RsyncRshCmd string
keepAliveInterval time.Duration
clock clock.PassiveClock

PrinterCreated printers.ResourcePrinter
PrinterDeleted printers.ResourcePrinter
Expand Down Expand Up @@ -805,6 +814,36 @@ func (o *MustGatherOptions) Run() error {
return kutilerrors.NewAggregate(errs)
}

// startClientKeepAlive spawns a background goroutine that periodically makes
// an authenticated API call to prevent the user's OAuth access token from being
// revoked due to accessTokenInactivityTimeout. The log-follow connection held
// by getGatherContainerLogs is a single long-lived HTTP stream that the API
// server does not count as discrete activity, so without these probes the token
// can expire on clusters with short inactivity windows (e.g. 5-10 minutes in
// banking/government/PCI-DSS environments).
//
// The goroutine runs until ctx is cancelled.
func (o *MustGatherOptions) startClientKeepAlive(ctx context.Context) {
interval := o.keepAliveInterval
if interval == 0 {
interval = defaultKeepAliveInterval
}
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if _, err := o.Client.Discovery().ServerVersion(); err != nil && !errors.Is(err, context.Canceled) {
klog.V(2).Infof("keep-alive probe failed (non-fatal): %v", err)
}
}
}
}()
}

// processNextWorkItem creates & processes the must-gather pod and returns error if any
func (o *MustGatherOptions) processNextWorkItem(ctx context.Context, ns string, pod *corev1.Pod) error {
var err error
Expand All @@ -831,6 +870,13 @@ func (o *MustGatherOptions) processNextWorkItem(ctx context.Context, ns string,

log := o.newPodOutLogger(o.Out, pod.Name)

// Prevent the user's OAuth token from expiring due to
// accessTokenInactivityTimeout while we hold long-lived log-follow
// connections. The probes run until the gather output has been copied.
keepAliveCtx, stopKeepAlive := context.WithCancel(ctx)
defer stopKeepAlive()
o.startClientKeepAlive(keepAliveCtx)

// wait for gather container to be running (gather is running)
if err := o.waitForGatherContainerRunning(ctx, pod); err != nil {
log("gather did not start: %s", err)
Expand Down
52 changes: 52 additions & 0 deletions pkg/cli/admin/mustgather/mustgather_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -699,3 +699,55 @@ func TestNewPod(t *testing.T) {
})
}
}

func TestStartClientKeepAlive(t *testing.T) {
t.Run("makes periodic authenticated API calls", func(t *testing.T) {
fakeClient := fake.NewSimpleClientset()
o := &MustGatherOptions{
IOStreams: genericiooptions.NewTestIOStreamsDiscard(),
Client: fakeClient,
keepAliveInterval: 50 * time.Millisecond,
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

o.startClientKeepAlive(ctx)
time.Sleep(200 * time.Millisecond)
cancel()

actions := fakeClient.Actions()
var probeCalls int
for _, a := range actions {
if a.GetVerb() == "get" {
probeCalls++
}
}
if probeCalls < 2 {
t.Errorf("expected at least 2 keep-alive probes, got %d actions: %v", probeCalls, actions)
}
})

t.Run("stops when context is cancelled", func(t *testing.T) {
fakeClient := fake.NewSimpleClientset()
o := &MustGatherOptions{
IOStreams: genericiooptions.NewTestIOStreamsDiscard(),
Client: fakeClient,
keepAliveInterval: 50 * time.Millisecond,
}

ctx, cancel := context.WithCancel(context.Background())
o.startClientKeepAlive(ctx)
time.Sleep(150 * time.Millisecond)
cancel()

time.Sleep(50 * time.Millisecond)
before := len(fakeClient.Actions())
time.Sleep(200 * time.Millisecond)
after := len(fakeClient.Actions())

if after != before {
t.Errorf("keep-alive goroutine continued after cancel: actions before=%d, after=%d", before, after)
}
})
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.