Skip to content

feat: download binaries for cred helpers #795

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 15, 2024
Merged
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
2 changes: 1 addition & 1 deletion pkg/cli/credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (c *Credential) Run(cmd *cobra.Command, _ []string) error {
opts.Runner.RuntimeManager = runtimes.Default(opts.Cache.CacheDir)
}

if err = opts.Runner.RuntimeManager.SetUpCredentialHelpers(cmd.Context(), cfg, opts.Env); err != nil {
if err = opts.Runner.RuntimeManager.SetUpCredentialHelpers(cmd.Context(), cfg); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/credential_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (c *Delete) Run(cmd *cobra.Command, args []string) error {
opts.Runner.RuntimeManager = runtimes.Default(opts.Cache.CacheDir)
}

if err = opts.Runner.RuntimeManager.SetUpCredentialHelpers(cmd.Context(), cfg, opts.Env); err != nil {
if err = opts.Runner.RuntimeManager.SetUpCredentialHelpers(cmd.Context(), cfg); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/credential_show.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (c *Show) Run(cmd *cobra.Command, args []string) error {
opts.Runner.RuntimeManager = runtimes.Default(opts.Cache.CacheDir)
}

if err = opts.Runner.RuntimeManager.SetUpCredentialHelpers(cmd.Context(), cfg, opts.Env); err != nil {
if err = opts.Runner.RuntimeManager.SetUpCredentialHelpers(cmd.Context(), cfg); err != nil {
return err
}

Expand Down
4 changes: 1 addition & 3 deletions pkg/credentials/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ import (
)

type CredentialHelperDirs struct {
RevisionFile, LastCheckedFile, BinDir, RepoDir, HelperDir string
RevisionFile, LastCheckedFile, BinDir string
}

func GetCredentialHelperDirs(cacheDir string) CredentialHelperDirs {
return CredentialHelperDirs{
RevisionFile: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "revision"),
LastCheckedFile: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "last-checked"),
BinDir: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "bin"),
RepoDir: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "repo"),
HelperDir: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers"),
}
}
2 changes: 1 addition & 1 deletion pkg/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type Model interface {
type RuntimeManager interface {
GetContext(ctx context.Context, tool types.Tool, cmd, env []string) (string, []string, error)
EnsureCredentialHelpers(ctx context.Context) error
SetUpCredentialHelpers(ctx context.Context, cliCfg *config.CLIConfig, env []string) error
SetUpCredentialHelpers(ctx context.Context, cliCfg *config.CLIConfig) error
}

type Engine struct {
Expand Down
2 changes: 1 addition & 1 deletion pkg/gptscript/gptscript.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func New(ctx context.Context, o ...Options) (*GPTScript, error) {
opts.Runner.RuntimeManager = runtimes.Default(cacheClient.CacheDir())
}

if err := opts.Runner.RuntimeManager.SetUpCredentialHelpers(context.Background(), cliCfg, opts.Env); err != nil {
if err := opts.Runner.RuntimeManager.SetUpCredentialHelpers(context.Background(), cliCfg); err != nil {
return nil, err
}

Expand Down
74 changes: 37 additions & 37 deletions pkg/repos/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,21 @@ import (
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"

"github.com/BurntSushi/locker"
"github.com/gptscript-ai/gptscript/pkg/config"
"github.com/gptscript-ai/gptscript/pkg/credentials"
runtimeEnv "github.com/gptscript-ai/gptscript/pkg/env"
"github.com/gptscript-ai/gptscript/pkg/hash"
"github.com/gptscript-ai/gptscript/pkg/loader/github"
"github.com/gptscript-ai/gptscript/pkg/repos/git"
"github.com/gptscript-ai/gptscript/pkg/repos/runtimes/golang"
"github.com/gptscript-ai/gptscript/pkg/types"
)

const credentialHelpersRepo = "github.com/gptscript-ai/gptscript-credential-helpers"

type Runtime interface {
ID() string
Supports(tool types.Tool, cmd []string) bool
Expand Down Expand Up @@ -68,7 +67,6 @@ type credHelperConfig struct {
lock sync.Mutex
initialized bool
cliCfg *config.CLIConfig
env []string
}

func New(cacheDir string, runtimes ...Runtime) *Manager {
Expand All @@ -90,7 +88,7 @@ func (m *Manager) EnsureCredentialHelpers(ctx context.Context) error {
defer m.credHelperConfig.lock.Unlock()

if !m.credHelperConfig.initialized {
if err := m.deferredSetUpCredentialHelpers(ctx, m.credHelperConfig.cliCfg, m.credHelperConfig.env); err != nil {
if err := m.deferredSetUpCredentialHelpers(ctx, m.credHelperConfig.cliCfg); err != nil {
return err
}
m.credHelperConfig.initialized = true
Expand All @@ -99,27 +97,28 @@ func (m *Manager) EnsureCredentialHelpers(ctx context.Context) error {
return nil
}

func (m *Manager) SetUpCredentialHelpers(_ context.Context, cliCfg *config.CLIConfig, env []string) error {
func (m *Manager) SetUpCredentialHelpers(_ context.Context, cliCfg *config.CLIConfig) error {
m.credHelperConfig = &credHelperConfig{
cliCfg: cliCfg,
env: env,
}
return nil
}

func (m *Manager) deferredSetUpCredentialHelpers(ctx context.Context, cliCfg *config.CLIConfig, env []string) error {
func (m *Manager) deferredSetUpCredentialHelpers(ctx context.Context, cliCfg *config.CLIConfig) error {
var (
helperName = cliCfg.CredentialsStore
suffix string
helperName = cliCfg.CredentialsStore
distInfo, suffix string
)
if helperName == "wincred" {
suffix = ".exe"
}

// The file helper is built-in and does not need to be compiled.
// The file helper is built-in and does not need to be downloaded.
if helperName == "file" {
return nil
}
switch helperName {
case "wincred":
suffix = ".exe"
default:
distInfo = fmt.Sprintf("-%s-%s", runtime.GOOS, runtime.GOARCH)
}

locker.Lock("gptscript-credential-helpers")
defer locker.Unlock("gptscript-credential-helpers")
Expand All @@ -137,13 +136,7 @@ func (m *Manager) deferredSetUpCredentialHelpers(ctx context.Context, cliCfg *co
}
}

// Load the credential helpers repo information.
_, _, repo, _, err := github.Load(ctx, nil, credentialHelpersRepo)
if err != nil {
return err
}

if err := os.MkdirAll(m.credHelperDirs.HelperDir, 0755); err != nil {
if err := os.MkdirAll(filepath.Dir(m.credHelperDirs.LastCheckedFile), 0755); err != nil {
return err
}

Expand All @@ -152,37 +145,44 @@ func (m *Manager) deferredSetUpCredentialHelpers(ctx context.Context, cliCfg *co
return err
}

var needsBuild bool
tool := types.Tool{
Source: types.ToolSource{
Repo: &types.Repo{
Root: runtimeEnv.VarOrDefault("GPTSCRIPT_CRED_HELPERS_ROOT", "https://github.com/gptscript-ai/gptscript-credential-helpers.git"),
},
},
}
tag, err := golang.GetLatestTag(tool)
if err != nil {
return err
}

var needsDownloaded bool
// Check the last revision shasum and see if it is different from the current one.
lastRevision, err := os.ReadFile(m.credHelperDirs.RevisionFile)
if (err == nil && strings.TrimSpace(string(lastRevision)) != repo.Revision) || errors.Is(err, fs.ErrNotExist) {
if (err == nil && strings.TrimSpace(string(lastRevision)) != tool.Source.Repo.Root+tag) || errors.Is(err, fs.ErrNotExist) {
// Need to pull the latest version.
needsBuild = true
if err := git.Checkout(ctx, m.gitDir, repo.Root, repo.Revision, filepath.Join(m.credHelperDirs.RepoDir, repo.Revision)); err != nil {
return err
}
needsDownloaded = true
// Update the revision file to the new revision.
if err := os.WriteFile(m.credHelperDirs.RevisionFile, []byte(repo.Revision), 0644); err != nil {
if err = os.WriteFile(m.credHelperDirs.RevisionFile, []byte(tool.Source.Repo.Root+tag), 0644); err != nil {
return err
}
} else if err != nil {
return err
}

if !needsBuild {
// Check for the existence of the gptscript-credential-osxkeychain binary.
// If it's there, we have no need to build it and can just return.
if _, err := os.Stat(filepath.Join(m.credHelperDirs.BinDir, "gptscript-credential-"+helperName+suffix)); err == nil {
if !needsDownloaded {
// Check for the existence of the credential helper binary.
// If it's there, we have no need to download it and can just return.
if _, err = os.Stat(filepath.Join(m.credHelperDirs.BinDir, "gptscript-credential-"+helperName+suffix)); err == nil {
return nil
}
}

// Find the Go runtime and use it to build the credential helper.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
// Find the Go runtime and use it to build the credential helper.
// Find the Go runtime and use it to download the credential helper.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

for _, runtime := range m.runtimes {
if strings.HasPrefix(runtime.ID(), "go") {
goRuntime := runtime.(*golang.Runtime)
return goRuntime.BuildCredentialHelper(ctx, helperName, m.credHelperDirs, m.runtimeDir, repo.Revision, env)
for _, rt := range m.runtimes {
if strings.HasPrefix(rt.ID(), "go") {
return rt.(*golang.Runtime).DownloadCredentialHelper(ctx, tool, helperName, distInfo, suffix, m.credHelperDirs.BinDir)
}
}

Expand Down
55 changes: 33 additions & 22 deletions pkg/repos/runtimes/golang/golang.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"runtime"
"strings"

"github.com/gptscript-ai/gptscript/pkg/credentials"
"github.com/gptscript-ai/gptscript/pkg/debugcmd"
runtimeEnv "github.com/gptscript-ai/gptscript/pkg/env"
"github.com/gptscript-ai/gptscript/pkg/hash"
Expand Down Expand Up @@ -97,6 +96,14 @@ type tag struct {
} `json:"commit"`
}

func GetLatestTag(tool types.Tool) (string, error) {
r, ok := getLatestRelease(tool)
if !ok {
return "", fmt.Errorf("failed to get latest release for %s", tool.Name)
}
return r.label, nil
}

func getLatestRelease(tool types.Tool) (*release, bool) {
if tool.Source.Repo == nil || !strings.HasPrefix(tool.Source.Repo.Root, "https://github.com/") {
return nil, false
Expand All @@ -116,11 +123,14 @@ func getLatestRelease(tool types.Tool) (*release, bool) {
account, repo := parts[1], parts[2]

resp, err := client.Get(fmt.Sprintf("https://api.github.com/repos/%s/%s/tags", account, repo))
if err != nil || resp.StatusCode != http.StatusOK {
if err != nil {
// ignore error
return nil, false
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, false
}

var tags []tag
if err := json.NewDecoder(resp.Body).Decode(&tags); err != nil {
Expand All @@ -137,11 +147,14 @@ func getLatestRelease(tool types.Tool) (*release, bool) {
}

resp, err = client.Get(fmt.Sprintf("https://github.com/%s/%s/releases/latest", account, repo))
if err != nil || resp.StatusCode != http.StatusFound {
if err != nil {
// ignore error
return nil, false
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusFound {
return nil, false
}

target := resp.Header.Get("Location")
if target == "" {
Expand Down Expand Up @@ -212,7 +225,7 @@ func downloadBin(ctx context.Context, checksum, src, url, bin string) error {
return nil
}

func getChecksum(ctx context.Context, rel *release) string {
func getChecksum(ctx context.Context, rel *release, artifactName string) string {
resp, err := get(ctx, rel.checksumTxt())
if err != nil {
// ignore error
Expand All @@ -223,7 +236,7 @@ func getChecksum(ctx context.Context, rel *release) string {
scan := bufio.NewScanner(resp.Body)
for scan.Scan() {
fields := strings.Fields(scan.Text())
if len(fields) == 2 && (fields[1] == rel.srcBinName() || fields[1] == "*"+rel.srcBinName()) {
if len(fields) == 2 && (fields[1] == artifactName || fields[1] == "*"+artifactName) {
return fields[0]
}
}
Expand All @@ -241,7 +254,7 @@ func (r *Runtime) Binary(ctx context.Context, tool types.Tool, _, toolSource str
return false, nil, nil
}

checksum := getChecksum(ctx, rel)
checksum := getChecksum(ctx, rel, rel.srcBinName())
if checksum == "" {
return false, nil, nil
}
Expand All @@ -268,30 +281,28 @@ func (r *Runtime) Setup(ctx context.Context, _ types.Tool, dataRoot, toolSource
return newEnv, nil
}

func (r *Runtime) BuildCredentialHelper(ctx context.Context, helperName string, credHelperDirs credentials.CredentialHelperDirs, dataRoot, revision string, env []string) error {
func (r *Runtime) DownloadCredentialHelper(ctx context.Context, tool types.Tool, helperName, distInfo, suffix string, binDir string) error {
if helperName == "file" {
return nil
}

var suffix string
if helperName == "wincred" {
suffix = ".exe"
rel, ok := getLatestRelease(tool)
if !ok {
return fmt.Errorf("failed to find %s release", r.ID())
}
binaryName := "gptscript-credential-" + helperName
checksum := getChecksum(ctx, rel, binaryName+distInfo+suffix)
if checksum == "" {
return fmt.Errorf("failed to find %s release checksum for os=%s arch=%s", r.ID(), runtime.GOOS, runtime.GOARCH)
}

binPath, err := r.getRuntime(ctx, dataRoot)
if err != nil {
return err
url, _ := strings.CutSuffix(rel.binURL(), rel.srcBinName())
url += binaryName + distInfo + suffix
if err := downloadBin(ctx, checksum, strings.TrimSuffix(binDir, "bin"), url, binaryName+suffix); err != nil {
return fmt.Errorf("failed to download %s release for os=%s arch=%s: %w", r.ID(), runtime.GOOS, runtime.GOARCH, err)
}
newEnv := runtimeEnv.AppendPath(env, binPath)

log.InfofCtx(ctx, "Building credential helper %s", helperName)
cmd := debugcmd.New(ctx, filepath.Join(binPath, "go"),
"build", "-buildvcs=false", "-o",
filepath.Join(credHelperDirs.BinDir, "gptscript-credential-"+helperName+suffix),
fmt.Sprintf("./%s/cmd/", helperName))
cmd.Env = stripGo(append(env, newEnv...))
cmd.Dir = filepath.Join(credHelperDirs.RepoDir, revision)
return cmd.Run()
return nil
}

func (r *Runtime) getReleaseAndDigest() (string, string, error) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/runner/runtimemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ func (r runtimeManagerLogger) EnsureCredentialHelpers(ctx context.Context) error
return r.rm.EnsureCredentialHelpers(mvl.WithInfo(ctx, r))
}

func (r runtimeManagerLogger) SetUpCredentialHelpers(_ context.Context, _ *config.CLIConfig, _ []string) error {
func (r runtimeManagerLogger) SetUpCredentialHelpers(_ context.Context, _ *config.CLIConfig) error {
panic("not implemented")
}