iclient/credentials.go
Rama develop · réplica de lectura del repositorio original.
package iclient
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/url"
"os/exec"
"strings"
"sync"
"time"
)
// A helper reads a keyring, it does not wait on a human.
const credHelperTimeout = 30 * time.Second
// credEntry is one remote's resolved login, or why there is none. The helper
// runs once per entry, whatever it answers.
type credEntry struct {
once sync.Once
username string
password string
err error
}
// applyCredentials moves a registry's login out of its addresses and into info.
// Nothing that logs or formats an address can leak it afterwards, and the
// address is a URL again only where incusd's API demands one.
//
// A credentials helper wins over an address that carries its own, matching what
// the incus CLI does with the same remote.
func (c *Config) applyCredentials(remote string, r ConfigRemote, info *ConfigRemoteInfo) error {
// rollingAddrs built this slice, so rewriting it does not touch the Config.
for i, addr := range info.Addrs {
parsed, err := url.Parse(addr)
if err != nil {
return fmt.Errorf("%q: parsing the registry address: %w", remote, err)
}
if parsed.User == nil {
continue
}
if info.Username == "" {
info.Username = parsed.User.Username()
info.Password, _ = parsed.User.Password()
}
parsed.User = nil
info.Addrs[i] = parsed.String()
}
if r.CredHelper == "" {
return nil
}
entry := c.credentials(remote, r.CredHelper, info.Addrs[0])
if entry.err != nil {
return entry.err
}
info.Username, info.Password = entry.username, entry.password
return nil
}
// credentials runs remote's helper, at most once per Config. ReadConfig gave
// every remote an entry of its own, so nothing here writes the map and two
// registries resolve at the same time. A Config built by hand has no entry and
// resolves without memoising.
func (c *Config) credentials(remote string, helper string, addr string) *credEntry {
entry, ok := c.creds[remote]
if !ok {
entry = &credEntry{}
}
entry.once.Do(func() { entry.fill(helper, addr) })
return entry
}
// fill makes the call the incus CLI makes: the registry host on stdin, a
// docker credentials helper's JSON back.
func (e *credEntry) fill(helper string, addr string) {
parsed, err := url.Parse(addr)
if err != nil {
e.err = fmt.Errorf("%w %q: parsing the registry address: %w", ErrCredHelper, helper, err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), credHelperTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, helper, "get") //nolint:gosec // the helper is named by the user's own configuration
cmd.Stdin = strings.NewReader(parsed.Host)
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
cmd.Stdout = stdout
cmd.Stderr = stderr
err = cmd.Run()
if err != nil {
detail := strings.TrimSpace(stderr.String())
if detail != "" {
e.err = fmt.Errorf("%w %q: %w: %s", ErrCredHelper, helper, err, detail)
return
}
e.err = fmt.Errorf("%w %q: %w", ErrCredHelper, helper, err)
return
}
// The wire names are docker's, not ours.
var res struct {
Username string
Secret string
}
err = json.Unmarshal(stdout.Bytes(), &res)
if err != nil {
e.err = fmt.Errorf("%w %q: decoding its response: %w", ErrCredHelper, helper, err)
return
}
e.username, e.password = res.Username, res.Secret
}