100 lines
2.8 KiB
Go
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
|
|
}
|