Ctrl K
ComunidadGuías…

iclient/credentials.go

Rama develop · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}