mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-04 06:25:32 +00:00
1. Restructures the README into installation, quickstart, deployment, configuration, jobs and operation, and shortens it to user-facing prose. Option details are left to `config.example.yaml` and `docs/`, existing heading anchors are kept where possible. 2. FreeBSD builds used the Docker stub, so `docker://` labels, services and Docker actions failed there. The Docker code works on FreeBSD against a remote daemon, so it is enabled, and host jobs report `runner.os` as `FreeBSD`. 3. `make build` on Windows now writes `gitea-runner.exe`, taking the suffix from `go env GOEXE`. Reviewed-on: https://gitea.com/gitea/runner/pulls/1367 Reviewed-by: Lunny Xiao <[email protected]> Co-authored-by: silverwind <[email protected]>
303 lines
9.1 KiB
Go
303 lines
9.1 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// Copyright 2021 The nektos/act Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd || freebsd))
|
|
|
|
package container
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"maps"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"gitea.com/gitea/runner/act/common"
|
|
|
|
"github.com/distribution/reference"
|
|
"github.com/moby/moby/api/types/registry"
|
|
)
|
|
|
|
func LoadDockerAuthConfig(ctx context.Context, image string) (registry.AuthConfig, error) {
|
|
logger := common.Logger(ctx)
|
|
cfg, err := loadDockerConfig(ctx)
|
|
if err != nil {
|
|
logger.Warnf("Could not load docker config: %v", err)
|
|
return registry.AuthConfig{}, err
|
|
}
|
|
registryKey := registryAuthConfigKey("docker.io")
|
|
if image != "" {
|
|
if registryRef, refErr := reference.ParseNormalizedNamed(image); refErr != nil {
|
|
logger.Warnf("Could not normalize image reference: %v", refErr)
|
|
} else {
|
|
registryKey = registryAuthConfigKey(reference.Domain(registryRef))
|
|
}
|
|
}
|
|
|
|
authConfig, err := cfg.authConfig(ctx, registryKey)
|
|
if err != nil {
|
|
logger.Warnf("Could not get auth config from docker config: %v", err)
|
|
return registry.AuthConfig{}, err
|
|
}
|
|
|
|
return authConfig, nil
|
|
}
|
|
|
|
func LoadDockerAuthConfigs(ctx context.Context) map[string]registry.AuthConfig {
|
|
logger := common.Logger(ctx)
|
|
cfg, err := loadDockerConfig(ctx)
|
|
if err != nil {
|
|
logger.Warnf("Could not load docker config: %v", err)
|
|
return nil
|
|
}
|
|
authConfigs, err := cfg.allAuthConfigs(ctx)
|
|
if err != nil {
|
|
logger.Warnf("Could not get docker auth configs: %v", err)
|
|
return nil
|
|
}
|
|
return authConfigs
|
|
}
|
|
|
|
func registryAuthConfigKey(domainName string) string {
|
|
if domainName == "docker.io" || domainName == "index.docker.io" {
|
|
return "https://index.docker.io/v1/"
|
|
}
|
|
return domainName
|
|
}
|
|
|
|
type dockerConfig struct {
|
|
Auths map[string]registry.AuthConfig `json:"auths"`
|
|
CredsStore string `json:"credsStore"`
|
|
CredHelpers map[string]string `json:"credHelpers"`
|
|
env map[string]registry.AuthConfig // from DOCKER_AUTH_CONFIG, nil when unset or unusable
|
|
}
|
|
|
|
func loadDockerConfig(ctx context.Context) (*dockerConfig, error) {
|
|
dir := os.Getenv("DOCKER_CONFIG")
|
|
if dir == "" {
|
|
home, _ := os.UserHomeDir()
|
|
if home == "" && runtime.GOOS != "windows" {
|
|
if current, err := user.Current(); err == nil {
|
|
home = current.HomeDir
|
|
}
|
|
}
|
|
dir = filepath.Join(home, ".docker")
|
|
}
|
|
cfg := &dockerConfig{}
|
|
filename := filepath.Join(dir, "config.json")
|
|
file, err := os.Open(filename)
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return nil, fmt.Errorf("loading config file: %w", err)
|
|
}
|
|
if err == nil {
|
|
defer file.Close()
|
|
if err := json.NewDecoder(file).Decode(cfg); err != nil && !errors.Is(err, io.EOF) {
|
|
return nil, fmt.Errorf("parsing config file (%s): %w", filename, err)
|
|
}
|
|
}
|
|
for address, auth := range cfg.Auths {
|
|
if auth.Auth != "" {
|
|
if auth.Username, auth.Password, err = decodeAuth(auth.Auth); err != nil {
|
|
return nil, fmt.Errorf("parsing config file (%s): %w", filename, err)
|
|
}
|
|
}
|
|
auth.Auth, auth.ServerAddress = "", address
|
|
cfg.Auths[address] = auth
|
|
}
|
|
if value := os.Getenv("DOCKER_AUTH_CONFIG"); value != "" {
|
|
if cfg.env, err = parseDockerAuthConfigEnv(value); err != nil {
|
|
common.Logger(ctx).Warnf("Failed to create credential store from DOCKER_AUTH_CONFIG: %v", err)
|
|
}
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func decodeAuth(auth string) (string, string, error) {
|
|
decoded, err := base64.StdEncoding.DecodeString(auth)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
username, password, ok := strings.Cut(string(decoded), ":")
|
|
if !ok || username == "" {
|
|
return "", "", errors.New("invalid auth configuration file")
|
|
}
|
|
return username, strings.Trim(password, "\x00"), nil
|
|
}
|
|
|
|
func parseDockerAuthConfigEnv(value string) (map[string]registry.AuthConfig, error) {
|
|
var env struct {
|
|
Auths map[string]struct {
|
|
Auth string `json:"auth"`
|
|
} `json:"auths"`
|
|
}
|
|
decoder := json.NewDecoder(strings.NewReader(value))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&env); err != nil && !errors.Is(err, io.EOF) {
|
|
return nil, err
|
|
}
|
|
if decoder.More() {
|
|
return nil, errors.New("DOCKER_AUTH_CONFIG does not support more than one JSON object")
|
|
}
|
|
auths := make(map[string]registry.AuthConfig, len(env.Auths))
|
|
for address, entry := range env.Auths {
|
|
if entry.Auth == "" {
|
|
return nil, fmt.Errorf("DOCKER_AUTH_CONFIG environment variable is missing key `auth` for %s", address)
|
|
}
|
|
username, password, err := decodeAuth(entry.Auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
auths[address] = registry.AuthConfig{Username: username, Password: password, ServerAddress: address}
|
|
}
|
|
return auths, nil
|
|
}
|
|
|
|
func (c *dockerConfig) authConfig(ctx context.Context, key string) (registry.AuthConfig, error) {
|
|
if auth, ok := c.env[key]; ok {
|
|
return auth, nil
|
|
}
|
|
helper := c.CredsStore
|
|
if named, ok := c.CredHelpers[key]; ok {
|
|
helper = named
|
|
}
|
|
if helper == "" {
|
|
return c.fileAuth(key), nil
|
|
}
|
|
auth := c.fileAuth(key)
|
|
stored, err := helperCredentials(ctx, helper, key)
|
|
if err != nil {
|
|
return auth, err
|
|
}
|
|
auth.Username, auth.Password, auth.IdentityToken, auth.ServerAddress = stored.Username, stored.Password, stored.IdentityToken, stored.ServerAddress
|
|
return auth, nil
|
|
}
|
|
|
|
func (c *dockerConfig) fileAuth(key string) registry.AuthConfig {
|
|
if auth, ok := c.Auths[key]; ok {
|
|
return auth
|
|
}
|
|
for address, auth := range c.Auths {
|
|
if registryHostname(address) == key {
|
|
return auth
|
|
}
|
|
}
|
|
return registry.AuthConfig{}
|
|
}
|
|
|
|
func (c *dockerConfig) allAuthConfigs(ctx context.Context) (map[string]registry.AuthConfig, error) {
|
|
var auths map[string]registry.AuthConfig
|
|
if c.CredsStore == "" {
|
|
auths = maps.Clone(c.Auths)
|
|
} else {
|
|
var err error
|
|
if auths, err = c.helperAuthConfigs(ctx); err != nil {
|
|
if c.env == nil {
|
|
return nil, err
|
|
}
|
|
common.Logger(ctx).Warnf("Could not list the credentials of docker-credential-%s: %v", c.CredsStore, err)
|
|
}
|
|
}
|
|
if auths == nil {
|
|
auths = map[string]registry.AuthConfig{}
|
|
}
|
|
maps.Copy(auths, c.env)
|
|
if c.CredsStore == "" {
|
|
c.Auths = maps.Clone(auths) // docker's file store holds the DOCKER_AUTH_CONFIG entries after listing, and credHelpers set to "" read it
|
|
}
|
|
for address := range c.CredHelpers {
|
|
auth, err := c.authConfig(ctx, registryAuthConfigKey(address))
|
|
if err != nil {
|
|
common.Logger(ctx).Warnf("Failed to get credentials for registry: %s: %v", address, err)
|
|
continue
|
|
}
|
|
auths[address] = auth
|
|
}
|
|
return auths, nil
|
|
}
|
|
|
|
func (c *dockerConfig) helperAuthConfigs(ctx context.Context) (map[string]registry.AuthConfig, error) {
|
|
out, err := runCredentialHelper(ctx, c.CredsStore, "list", "unused")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error listing credentials - err: %w, out: `%s`", err, strings.TrimSpace(string(out)))
|
|
}
|
|
var addresses map[string]string
|
|
if err := json.NewDecoder(bytes.NewReader(out)).Decode(&addresses); err != nil {
|
|
return nil, err
|
|
}
|
|
auths := make(map[string]registry.AuthConfig, len(addresses))
|
|
for address := range addresses {
|
|
stored, err := helperCredentials(ctx, c.CredsStore, address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
auth := c.Auths[address]
|
|
auth.Username, auth.Password, auth.IdentityToken = stored.Username, stored.Password, stored.IdentityToken
|
|
if auth.ServerAddress == "" {
|
|
auth.ServerAddress = stored.ServerAddress
|
|
}
|
|
auths[address] = auth
|
|
}
|
|
return auths, nil
|
|
}
|
|
|
|
func helperCredentials(ctx context.Context, helper, address string) (registry.AuthConfig, error) {
|
|
out, err := runCredentialHelper(ctx, helper, "get", address)
|
|
if err != nil {
|
|
if strings.TrimSpace(string(out)) == "credentials not found in native keychain" {
|
|
return registry.AuthConfig{}, nil
|
|
}
|
|
return registry.AuthConfig{}, fmt.Errorf("error getting credentials - err: %w, out: `%s`", err, strings.TrimSpace(string(out)))
|
|
}
|
|
var stored struct{ Username, Secret string }
|
|
if err := json.NewDecoder(bytes.NewReader(out)).Decode(&stored); err != nil {
|
|
return registry.AuthConfig{}, err
|
|
}
|
|
if stored.Username == "<token>" {
|
|
return registry.AuthConfig{IdentityToken: stored.Secret, ServerAddress: address}, nil
|
|
}
|
|
return registry.AuthConfig{Username: stored.Username, Password: stored.Secret, ServerAddress: address}, nil
|
|
}
|
|
|
|
func runCredentialHelper(ctx context.Context, helper, action, input string) ([]byte, error) {
|
|
cmd := exec.CommandContext(ctx, "docker-credential-"+helper, action)
|
|
cmd.Stdin = strings.NewReader(input)
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Output()
|
|
}
|
|
|
|
func registryHostname(address string) string {
|
|
if _, rest, ok := strings.Cut(address, "://"); ok {
|
|
if u, err := url.Parse(address); err == nil && u.Hostname() != "" {
|
|
if u.Port() == "" {
|
|
return u.Hostname()
|
|
}
|
|
return net.JoinHostPort(u.Hostname(), u.Port())
|
|
}
|
|
if host, _, _ := strings.Cut(rest, "/"); host != "" {
|
|
if colon := strings.LastIndex(host, ":"); strings.Count(host, ":") > 1 && !strings.HasPrefix(host, "[") && isDigits(host[colon+1:]) {
|
|
return net.JoinHostPort(host[:colon], host[colon+1:])
|
|
}
|
|
return host
|
|
}
|
|
}
|
|
host, _, _ := strings.Cut(address, "/")
|
|
return host
|
|
}
|
|
|
|
func isDigits(s string) bool {
|
|
return s != "" && strings.Trim(s, "0123456789") == ""
|
|
}
|