Files
kandev/apps/backend/internal/agent/runtime/lifecycle/credential_uploader.go
T
2026-07-02 21:57:25 +01:00

100 lines
2.8 KiB
Go

package lifecycle
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"go.uber.org/zap"
"github.com/kandev/kandev/internal/agent/remoteauth"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/common/subproc"
)
// FileUploader abstracts writing files to a remote environment. Used by
// UploadCredentialFiles to seed agent auth files into the kandev-managed
// per-container session dir (local) or sprite (remote).
type FileUploader interface {
WriteFile(ctx context.Context, path string, data []byte, mode os.FileMode) error
}
const credentialFileMode os.FileMode = 0o600
// UploadCredentialFiles reads local credential files and uploads them to the remote environment.
func UploadCredentialFiles(
ctx context.Context,
uploader FileUploader,
methods []remoteauth.Method,
targetHomeDir string,
log *logger.Logger,
) error {
if len(methods) == 0 {
return nil
}
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
for _, method := range methods {
if method.Type != "files" {
continue
}
for _, relPath := range method.SourceFiles {
srcPath := filepath.Join(home, relPath)
data, readErr := os.ReadFile(srcPath)
if readErr != nil {
log.Warn("credential source file not found, skipping",
zap.String("method_id", method.MethodID),
zap.String("path", srcPath))
continue
}
targetPath := filepath.Join(targetHomeDir, method.TargetRelDir, filepath.Base(relPath))
if err := uploader.WriteFile(ctx, targetPath, data, credentialFileMode); err != nil {
return fmt.Errorf("failed to upload %s: %w", targetPath, err)
}
log.Debug("uploaded credential file",
zap.String("method_id", method.MethodID),
zap.String("target", targetPath))
}
}
return nil
}
// DetectGHToken runs `gh auth token` locally and returns the GitHub OAuth token.
//
// Splits the throttle wait (30s) from the exec budget (5s) so a busy gh pool
// can't silently disable token injection. Critically the exec context is
// built AFTER Acquire returns — otherwise its 5s deadline would already be
// ticking down while we waited in the throttle queue.
func DetectGHToken() (string, error) {
acquireCtx, cancelAcquire := context.WithTimeout(context.Background(), 30*time.Second)
defer cancelAcquire()
release, err := subproc.GH().Acquire(acquireCtx)
if err != nil {
return "", fmt.Errorf("gh throttle acquire: %w", err)
}
defer release()
execCtx, cancelExec := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelExec()
cmd := exec.CommandContext(execCtx, "gh", "auth", "token")
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("gh auth token failed: %w", err)
}
token := strings.TrimSpace(string(out))
if token == "" {
return "", fmt.Errorf("gh auth token returned empty")
}
return token, nil
}