657 lines
24 KiB
Go
657 lines
24 KiB
Go
// Package dockerclient implements the Docker container/build defaults Target.
|
|
// It only edits the owning user's Docker client config.json and never contacts
|
|
// or changes a Docker daemon.
|
|
package dockerclient
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"proxyctl/internal/pathutil"
|
|
"proxyctl/internal/profile"
|
|
"proxyctl/internal/securefs"
|
|
"proxyctl/internal/target"
|
|
)
|
|
|
|
var managedKeys = []string{"httpProxy", "httpsProxy", "allProxy", "noProxy"}
|
|
|
|
type Configuration struct {
|
|
ConfigFile string
|
|
RecoveryFile string
|
|
OwnFiles bool
|
|
UID int
|
|
GID int
|
|
TrustedRoots []string
|
|
}
|
|
|
|
// ResolveConfiguration keeps Docker client effects with the invoking user when
|
|
// proxyctl itself was explicitly started through sudo.
|
|
func ResolveConfiguration(environment profile.IdentityEnvironment) (Configuration, error) {
|
|
profileConfiguration, err := profile.ResolveConfiguration(environment)
|
|
if err != nil {
|
|
return Configuration{}, err
|
|
}
|
|
owner := profileConfiguration.Owner
|
|
current, err := environment.CurrentUser()
|
|
if err != nil {
|
|
return Configuration{}, fmt.Errorf("identify effective user: %w", err)
|
|
}
|
|
invokingUser := current.Username != owner.Username
|
|
|
|
dockerDirectory := ""
|
|
if value, ok := environment.LookupEnv("DOCKER_CONFIG"); ok && filepath.IsAbs(value) {
|
|
if !invokingUser || pathWithin(value, owner.HomeDir) {
|
|
dockerDirectory = value
|
|
}
|
|
}
|
|
if dockerDirectory == "" {
|
|
dockerDirectory = filepath.Join(owner.HomeDir, ".docker")
|
|
}
|
|
stateDirectory := ""
|
|
if value, ok := environment.LookupEnv("XDG_STATE_HOME"); ok && filepath.IsAbs(value) {
|
|
if !invokingUser || pathWithin(value, owner.HomeDir) {
|
|
stateDirectory = value
|
|
}
|
|
}
|
|
if stateDirectory == "" {
|
|
stateDirectory = filepath.Join(owner.HomeDir, ".local", "state")
|
|
}
|
|
|
|
roots := []string{owner.HomeDir}
|
|
if !pathWithin(dockerDirectory, owner.HomeDir) {
|
|
roots = append(roots, filepath.Dir(dockerDirectory))
|
|
}
|
|
if !pathWithin(stateDirectory, owner.HomeDir) {
|
|
roots = append(roots, filepath.Dir(stateDirectory))
|
|
}
|
|
return Configuration{
|
|
ConfigFile: filepath.Join(dockerDirectory, "config.json"),
|
|
RecoveryFile: filepath.Join(stateDirectory, "proxyctl", "docker-containers.json"),
|
|
OwnFiles: true, UID: owner.UID, GID: owner.GID, TrustedRoots: roots,
|
|
}, nil
|
|
}
|
|
|
|
func pathWithin(path, parent string) bool { return pathutil.Within(path, parent) }
|
|
|
|
type Ownership struct{ UID, GID int }
|
|
|
|
type FileSystem interface {
|
|
ReadFile(string) ([]byte, error)
|
|
AtomicWrite(string, []byte, os.FileMode, *Ownership) error
|
|
Remove(string) error
|
|
WithLock(string, *Ownership, func() error) error
|
|
}
|
|
|
|
type OSFileSystem struct{ TrustedRoots []string }
|
|
|
|
func NewOSFileSystem(configuration Configuration) OSFileSystem {
|
|
return OSFileSystem{TrustedRoots: append([]string(nil), configuration.TrustedRoots...)}
|
|
}
|
|
func (f OSFileSystem) store(path string, owner *Ownership) securefs.Store {
|
|
root := ""
|
|
if len(f.TrustedRoots) == 0 {
|
|
root = string(filepath.Separator)
|
|
}
|
|
for _, candidate := range f.TrustedRoots {
|
|
if pathWithin(path, candidate) && (root == "" || len(filepath.Clean(candidate)) > len(filepath.Clean(root))) {
|
|
root = candidate
|
|
}
|
|
}
|
|
var secureOwner *securefs.Owner
|
|
if owner != nil {
|
|
secureOwner = &securefs.Owner{UID: owner.UID, GID: owner.GID}
|
|
}
|
|
return securefs.Store{TrustedRoot: root, Owner: secureOwner}
|
|
}
|
|
func (f OSFileSystem) ReadFile(path string) ([]byte, error) { return f.store(path, nil).ReadFile(path) }
|
|
func (f OSFileSystem) Remove(path string) error { return f.store(path, nil).Remove(path) }
|
|
func (f OSFileSystem) AtomicWrite(path string, data []byte, mode os.FileMode, owner *Ownership) error {
|
|
return f.store(path, owner).AtomicWrite(path, data, mode)
|
|
}
|
|
func (f OSFileSystem) WithLock(path string, owner *Ownership, action func() error) error {
|
|
return f.store(path, owner).WithLock(path, action)
|
|
}
|
|
|
|
type priorValue struct {
|
|
Present bool `json:"present"`
|
|
Value json.RawMessage `json:"value,omitempty"`
|
|
}
|
|
type recovery struct {
|
|
ConfigPresent bool `json:"configPresent"`
|
|
ProxiesPresent bool `json:"proxiesPresent"`
|
|
DefaultPresent bool `json:"defaultPresent"`
|
|
Original map[string]priorValue `json:"original"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
}
|
|
|
|
type Adapter struct {
|
|
configuration Configuration
|
|
files FileSystem
|
|
}
|
|
|
|
var (
|
|
_ target.Target = (*Adapter)(nil)
|
|
_ target.Diagnosable = (*Adapter)(nil)
|
|
)
|
|
|
|
func New(configuration Configuration, files FileSystem) *Adapter {
|
|
if files == nil {
|
|
configured := NewOSFileSystem(configuration)
|
|
files = configured
|
|
}
|
|
return &Adapter{configuration: configuration, files: files}
|
|
}
|
|
|
|
func (a *Adapter) ID() string { return "docker-containers" }
|
|
func (a *Adapter) Description() target.Description {
|
|
return target.Description{
|
|
Name: "Docker containers/builds",
|
|
Summary: "Sets proxy defaults injected into future Docker containers and builds.",
|
|
Scope: "future containers and builds; not existing containers, networking, or daemon registry access",
|
|
Permissions: "write access to the owning user's Docker client configuration",
|
|
Ownership: "managed proxy keys and recovery metadata; other config.json content and prior External values are preserved",
|
|
RestartRisk: "none; the Docker daemon is not contacted",
|
|
Details: "Affects new containers and builds through the owning user's Docker client configuration; it does not affect existing containers or container networking and is not Docker daemon registry access. proxyctl owns selected proxy keys and recovery metadata while preserving other config.json content and restoring prior External values on disable. User-file write permission is required; it does not contact or restart the daemon.",
|
|
}
|
|
}
|
|
func (a *Adapter) Confirmation(bool) string { return "" }
|
|
|
|
// DiagnosticChecks validates the readable JSON surfaces and observes write
|
|
// authority without taking the adapter lock or creating any files.
|
|
func (a *Adapter) DiagnosticChecks(_ context.Context, _ *profile.Profile) []target.Check {
|
|
commandCheck := target.Check{Name: "required commands", OK: true, Evidence: "Docker client defaults require no external commands"}
|
|
schemaCheck := target.Check{Name: "configuration schema", OK: true, Evidence: "Docker client configuration and recovery metadata are absent or valid JSON objects"}
|
|
if config, _, err := a.readConfig(); err != nil {
|
|
schemaCheck.OK = false
|
|
schemaCheck.Evidence = profile.RedactText("Docker client config.json is invalid or unreadable: " + err.Error())
|
|
} else if proxies, _, err := childObject(config, "proxies"); err != nil {
|
|
schemaCheck.OK = false
|
|
schemaCheck.Evidence = profile.RedactText(err.Error())
|
|
} else if defaults, _, err := childObject(proxies, "default"); err != nil {
|
|
schemaCheck.OK = false
|
|
schemaCheck.Evidence = profile.RedactText(err.Error())
|
|
} else if err := validateManagedProxyStrings(defaults); err != nil {
|
|
schemaCheck.OK = false
|
|
schemaCheck.Evidence = err.Error()
|
|
} else if _, _, err := a.readRecovery(); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
schemaCheck.OK = false
|
|
schemaCheck.Evidence = profile.RedactText("Docker client recovery metadata is invalid or unreadable: " + err.Error())
|
|
}
|
|
return []target.Check{
|
|
commandCheck,
|
|
schemaCheck,
|
|
target.ReadOnlyWriteAuthority(a.configuration.ConfigFile, a.configuration.RecoveryFile),
|
|
}
|
|
}
|
|
|
|
func (a *Adapter) Availability(context.Context) target.Availability {
|
|
if !a.usablePath(a.configuration.ConfigFile) || !a.usablePath(a.configuration.RecoveryFile) {
|
|
return target.Availability{Evidence: "standard Docker client configuration paths are not usable for the owning user"}
|
|
}
|
|
return target.Availability{Available: true, Evidence: "standard Docker client configuration is available for the owning user"}
|
|
}
|
|
|
|
func (a *Adapter) usablePath(path string) bool {
|
|
if !filepath.IsAbs(path) {
|
|
return false
|
|
}
|
|
if len(a.configuration.TrustedRoots) == 0 {
|
|
return true
|
|
}
|
|
for _, root := range a.configuration.TrustedRoots {
|
|
if filepath.IsAbs(root) && pathWithin(path, root) && filepath.Clean(path) != filepath.Clean(root) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
func (a *Adapter) ConfigPath() string { return a.configuration.ConfigFile }
|
|
func (a *Adapter) RecoveryPath() string { return a.configuration.RecoveryFile }
|
|
func (a *Adapter) LockPath() string { return a.configuration.RecoveryFile + ".lock" }
|
|
|
|
func (a *Adapter) Detect(ctx context.Context, active *profile.Profile) target.Detection {
|
|
inspection, err := a.inspect(active)
|
|
return target.Detection{Availability: a.Availability(ctx), Inspection: inspection, Err: redactError(err, active)}
|
|
}
|
|
|
|
// Inspect preserves the maintenance state projection while Detect also
|
|
// reports malformed or unreadable state as a failed check.
|
|
func (a *Adapter) Inspect(_ context.Context, active *profile.Profile) target.Inspection {
|
|
inspection, _ := a.inspect(active)
|
|
return inspection
|
|
}
|
|
|
|
func (a *Adapter) inspect(active *profile.Profile) (target.Inspection, error) {
|
|
metadata, managed, metadataErr := a.readRecovery()
|
|
config, _, configErr := a.readConfig()
|
|
if managed || (metadataErr != nil && !errors.Is(metadataErr, fs.ErrNotExist)) {
|
|
if metadataErr != nil {
|
|
return target.Inspection{State: target.Drifted, Evidence: "managed Docker client recovery metadata cannot be read"}, fmt.Errorf("inspect Docker client recovery metadata: %w", metadataErr)
|
|
}
|
|
if configErr != nil {
|
|
return target.Inspection{State: target.Drifted, Evidence: "managed Docker client configuration is missing, unreadable, or not safely mergeable"}, fmt.Errorf("inspect managed Docker client configuration: %w", configErr)
|
|
}
|
|
desired, desiredErr := desiredDefaults(active)
|
|
if desiredErr == nil && fingerprintCurrent(config) == metadata.Fingerprint && fingerprintDefaults(desired) == metadata.Fingerprint {
|
|
return target.Inspection{State: target.Enabled, Evidence: "managed Docker client proxy defaults match the Active Profile"}, nil
|
|
}
|
|
return target.Inspection{State: target.Drifted, Evidence: "managed Docker client proxy defaults do not match the Active Profile"}, nil
|
|
}
|
|
if configErr != nil {
|
|
return target.Inspection{State: target.Disabled, Evidence: "Docker client configuration could not be inspected safely"}, fmt.Errorf("inspect Docker client configuration: %w", configErr)
|
|
}
|
|
proxies, _, err := childObject(config, "proxies")
|
|
if err != nil {
|
|
return target.Inspection{State: target.Disabled, Evidence: "Docker client configuration could not be inspected safely"}, err
|
|
}
|
|
defaults, _, err := childObject(proxies, "default")
|
|
if err != nil {
|
|
return target.Inspection{State: target.Disabled, Evidence: "Docker client configuration could not be inspected safely"}, err
|
|
}
|
|
if err := validateManagedProxyStrings(defaults); err != nil {
|
|
return target.Inspection{State: target.Disabled, Evidence: "Docker client configuration could not be inspected safely"}, err
|
|
}
|
|
for _, key := range append(append([]string(nil), managedKeys...), "ftpProxy") {
|
|
var value string
|
|
if raw, ok := defaults[key]; ok && json.Unmarshal(raw, &value) == nil && value != "" {
|
|
return target.Inspection{State: target.External, Evidence: "recognized proxy defaults exist in Docker client config.json"}, nil
|
|
}
|
|
}
|
|
return target.Inspection{State: target.Disabled, Evidence: "no recognized Docker client proxy defaults detected"}, nil
|
|
}
|
|
|
|
func (a *Adapter) SetDesired(ctx context.Context, enabled bool, active *profile.Profile) target.OperationResult {
|
|
if !a.Availability(ctx).Available {
|
|
return target.OperationResult{Inspection: a.Inspect(ctx, active), Err: errors.New("Docker containers/builds Target is unavailable: standard Docker client configuration cannot be used")}
|
|
}
|
|
if enabled && active == nil {
|
|
return target.OperationResult{Inspection: a.Inspect(ctx, active), Err: errors.New("no Active Profile; open Profile management to create and activate one")}
|
|
}
|
|
|
|
var inspection target.Inspection
|
|
err := a.files.WithLock(a.LockPath(), a.ownership(), func() error {
|
|
if enabled {
|
|
inspection = a.Inspect(ctx, active)
|
|
if inspection.State == target.Enabled {
|
|
return nil
|
|
}
|
|
if err := a.enable(active); err != nil {
|
|
inspection = a.Inspect(ctx, active)
|
|
return err
|
|
}
|
|
} else if err := a.disable(); err != nil {
|
|
inspection = a.Inspect(ctx, active)
|
|
return err
|
|
}
|
|
inspection = a.Inspect(ctx, active)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
if inspection.Evidence == "" {
|
|
inspection = a.Inspect(ctx, active)
|
|
}
|
|
err = redactError(err, active)
|
|
}
|
|
return target.OperationResult{Inspection: inspection, Err: err}
|
|
}
|
|
|
|
func (a *Adapter) enable(active *profile.Profile) error {
|
|
desired, err := desiredDefaults(active)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
config, configPresent, err := a.readConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("read Docker client configuration for safe JSON merge: %w", err)
|
|
}
|
|
observedConfig := fingerprintDocument(config, configPresent)
|
|
metadata, managed, recoveryErr := a.readRecovery()
|
|
if recoveryErr != nil && !errors.Is(recoveryErr, fs.ErrNotExist) {
|
|
return fmt.Errorf("read Docker client recovery metadata: %w", recoveryErr)
|
|
}
|
|
proxies, proxiesPresent, err := childObject(config, "proxies")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defaults, defaultPresent, err := childObject(proxies, "default")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := validateManagedProxyStrings(defaults); err != nil {
|
|
return err
|
|
}
|
|
if !managed {
|
|
metadata = recovery{ConfigPresent: configPresent, ProxiesPresent: proxiesPresent, DefaultPresent: defaultPresent, Original: make(map[string]priorValue)}
|
|
for _, key := range managedKeys {
|
|
value, present := defaults[key]
|
|
metadata.Original[key] = priorValue{Present: present, Value: cloneRaw(value)}
|
|
}
|
|
}
|
|
metadata.Fingerprint = fingerprintDefaults(desired)
|
|
metadataData, err := json.MarshalIndent(metadata, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := a.writeOwned(a.configuration.RecoveryFile, append(metadataData, '\n')); err != nil {
|
|
return fmt.Errorf("persist Docker client recovery metadata before changing proxy defaults: %w", err)
|
|
}
|
|
applyDesired(defaults, desired)
|
|
setChildObject(proxies, "default", defaults)
|
|
setChildObject(config, "proxies", proxies)
|
|
if err := a.requireConfigUnchanged(observedConfig); err != nil {
|
|
return err
|
|
}
|
|
if err := a.writeConfig(config); err != nil {
|
|
return fmt.Errorf("write Docker client proxy defaults after recovery metadata persisted: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *Adapter) disable() error {
|
|
metadata, managed, err := a.readRecovery()
|
|
if errors.Is(err, fs.ErrNotExist) || !managed {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("read Docker client recovery metadata: %w", err)
|
|
}
|
|
config, configPresent, err := a.readConfig()
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("read Docker client configuration for restoration: %w", err)
|
|
}
|
|
observedConfig := fingerprintDocument(config, configPresent)
|
|
if !configPresent {
|
|
config = make(map[string]json.RawMessage)
|
|
}
|
|
proxies, _, err := childObject(config, "proxies")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defaults, _, err := childObject(proxies, "default")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := validateManagedProxyStrings(defaults); err != nil {
|
|
return err
|
|
}
|
|
currentFingerprint := fingerprintCurrent(config)
|
|
if currentFingerprint == fingerprintOriginal(metadata) {
|
|
if err := a.files.Remove(a.configuration.RecoveryFile); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("clean Docker client recovery metadata for already-restored proxy defaults: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
if currentFingerprint != metadata.Fingerprint {
|
|
return errors.New("Docker client managed proxy defaults changed after proxyctl applied them; refusing to overwrite current configuration or discard recovery metadata")
|
|
}
|
|
for _, key := range managedKeys {
|
|
prior := metadata.Original[key]
|
|
if prior.Present {
|
|
defaults[key] = cloneRaw(prior.Value)
|
|
} else {
|
|
delete(defaults, key)
|
|
}
|
|
}
|
|
if len(defaults) == 0 && !metadata.DefaultPresent {
|
|
delete(proxies, "default")
|
|
} else {
|
|
setChildObject(proxies, "default", defaults)
|
|
}
|
|
if len(proxies) == 0 && !metadata.ProxiesPresent {
|
|
delete(config, "proxies")
|
|
} else {
|
|
setChildObject(config, "proxies", proxies)
|
|
}
|
|
if err := a.requireConfigUnchanged(observedConfig); err != nil {
|
|
return err
|
|
}
|
|
if len(config) == 0 && !metadata.ConfigPresent {
|
|
if err := a.files.Remove(a.configuration.ConfigFile); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("remove newly created empty Docker client configuration: %w", err)
|
|
}
|
|
} else if err := a.writeConfig(config); err != nil {
|
|
return fmt.Errorf("restore prior Docker client proxy defaults: %w", err)
|
|
}
|
|
if err := a.files.Remove(a.configuration.RecoveryFile); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("remove Docker client recovery metadata after restoration: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func desiredDefaults(active *profile.Profile) (map[string]string, error) {
|
|
if active == nil {
|
|
return nil, errors.New("Active Profile is required")
|
|
}
|
|
normalized, err := profile.ValidateAndNormalize(*active)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for field, raw := range map[string]string{"HTTP proxy": normalized.HTTPURL, "HTTPS proxy": normalized.HTTPSURL} {
|
|
if raw == "" {
|
|
continue
|
|
}
|
|
parsed, _ := url.Parse(raw)
|
|
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
|
return nil, fmt.Errorf("%s scheme %s cannot be represented by Docker's documented %sProxy default", field, parsed.Scheme, strings.ToLower(strings.Fields(field)[0]))
|
|
}
|
|
}
|
|
return map[string]string{
|
|
"httpProxy": normalized.HTTPURL,
|
|
"httpsProxy": normalized.HTTPSURL,
|
|
"allProxy": normalized.AllURL,
|
|
"noProxy": strings.Join(normalized.Bypass, ","),
|
|
}, nil
|
|
}
|
|
|
|
func (a *Adapter) readRecovery() (recovery, bool, error) {
|
|
data, err := a.files.ReadFile(a.configuration.RecoveryFile)
|
|
if err != nil {
|
|
return recovery{}, false, err
|
|
}
|
|
var value recovery
|
|
if err := json.Unmarshal(data, &value); err != nil {
|
|
return recovery{}, true, err
|
|
}
|
|
if value.Original == nil {
|
|
return recovery{}, true, errors.New("Docker client recovery metadata is incomplete")
|
|
}
|
|
fingerprintBytes, fingerprintErr := hex.DecodeString(value.Fingerprint)
|
|
if fingerprintErr != nil || len(fingerprintBytes) != sha256.Size {
|
|
return recovery{}, true, errors.New("Docker client recovery metadata has an invalid fingerprint")
|
|
}
|
|
for _, key := range managedKeys {
|
|
prior, present := value.Original[key]
|
|
if !present || (prior.Present && !json.Valid(prior.Value)) {
|
|
return recovery{}, true, errors.New("Docker client recovery metadata is incomplete")
|
|
}
|
|
}
|
|
return value, true, nil
|
|
}
|
|
|
|
func (a *Adapter) readConfig() (map[string]json.RawMessage, bool, error) {
|
|
data, err := a.files.ReadFile(a.configuration.ConfigFile)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return make(map[string]json.RawMessage), false, nil
|
|
}
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
object, err := decodeObject(data, "Docker client configuration")
|
|
return object, true, err
|
|
}
|
|
|
|
func decodeObject(data []byte, description string) (map[string]json.RawMessage, error) {
|
|
var object map[string]json.RawMessage
|
|
if err := json.Unmarshal(data, &object); err != nil {
|
|
return nil, fmt.Errorf("parse %s: %w", description, err)
|
|
}
|
|
if object == nil {
|
|
return nil, fmt.Errorf("%s must be a JSON object", description)
|
|
}
|
|
return object, nil
|
|
}
|
|
func childObject(parent map[string]json.RawMessage, key string) (map[string]json.RawMessage, bool, error) {
|
|
raw, present := parent[key]
|
|
if !present {
|
|
return make(map[string]json.RawMessage), false, nil
|
|
}
|
|
object, err := decodeObject(raw, "Docker client "+key+" setting")
|
|
return object, true, err
|
|
}
|
|
func validateManagedProxyStrings(defaults map[string]json.RawMessage) error {
|
|
for _, key := range managedKeys {
|
|
raw, present := defaults[key]
|
|
if !present {
|
|
continue
|
|
}
|
|
var value string
|
|
if err := json.Unmarshal(raw, &value); err != nil {
|
|
return fmt.Errorf("Docker client proxies.default.%s must be a JSON string", key)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setChildObject(parent map[string]json.RawMessage, key string, child map[string]json.RawMessage) {
|
|
data, _ := json.Marshal(child)
|
|
parent[key] = data
|
|
}
|
|
func applyDesired(defaults map[string]json.RawMessage, desired map[string]string) {
|
|
for _, key := range managedKeys {
|
|
if value := desired[key]; value != "" {
|
|
data, _ := json.Marshal(value)
|
|
defaults[key] = data
|
|
} else {
|
|
delete(defaults, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
type managedValue struct {
|
|
Present bool `json:"present"`
|
|
Valid bool `json:"valid"`
|
|
Value string `json:"value,omitempty"`
|
|
}
|
|
|
|
func fingerprintDefaults(values map[string]string) string {
|
|
snapshot := make(map[string]managedValue, len(managedKeys))
|
|
for _, key := range managedKeys {
|
|
if value := values[key]; value != "" {
|
|
snapshot[key] = managedValue{Present: true, Valid: true, Value: value}
|
|
} else {
|
|
snapshot[key] = managedValue{}
|
|
}
|
|
}
|
|
return fingerprintSnapshot(snapshot)
|
|
}
|
|
|
|
func fingerprintOriginal(metadata recovery) string {
|
|
snapshot := make(map[string]managedValue, len(managedKeys))
|
|
for _, key := range managedKeys {
|
|
prior := metadata.Original[key]
|
|
value := managedValue{Present: prior.Present}
|
|
if prior.Present && json.Unmarshal(prior.Value, &value.Value) == nil {
|
|
value.Valid = true
|
|
}
|
|
snapshot[key] = value
|
|
}
|
|
return fingerprintSnapshot(snapshot)
|
|
}
|
|
|
|
func fingerprintCurrent(config map[string]json.RawMessage) string {
|
|
snapshot := make(map[string]managedValue, len(managedKeys))
|
|
proxies, _, proxyErr := childObject(config, "proxies")
|
|
defaults, _, defaultErr := childObject(proxies, "default")
|
|
for _, key := range managedKeys {
|
|
raw, present := defaults[key]
|
|
value := managedValue{Present: present}
|
|
if present && proxyErr == nil && defaultErr == nil && json.Unmarshal(raw, &value.Value) == nil {
|
|
value.Valid = true
|
|
}
|
|
snapshot[key] = value
|
|
}
|
|
return fingerprintSnapshot(snapshot)
|
|
}
|
|
|
|
func fingerprintSnapshot(snapshot map[string]managedValue) string {
|
|
data, _ := json.Marshal(snapshot)
|
|
sum := sha256.Sum256(data)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func fingerprintDocument(config map[string]json.RawMessage, present bool) string {
|
|
data, _ := json.Marshal(struct {
|
|
Present bool `json:"present"`
|
|
Config map[string]json.RawMessage `json:"config"`
|
|
}{Present: present, Config: config})
|
|
sum := sha256.Sum256(data)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func (a *Adapter) requireConfigUnchanged(expected string) error {
|
|
current, present, err := a.readConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("re-read Docker client configuration before atomic replacement: %w", err)
|
|
}
|
|
if fingerprintDocument(current, present) != expected {
|
|
return errors.New("Docker client configuration changed concurrently; refusing to overwrite external updates")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cloneRaw(value json.RawMessage) json.RawMessage { return append(json.RawMessage(nil), value...) }
|
|
func (a *Adapter) ownership() *Ownership {
|
|
if !a.configuration.OwnFiles {
|
|
return nil
|
|
}
|
|
return &Ownership{UID: a.configuration.UID, GID: a.configuration.GID}
|
|
}
|
|
func (a *Adapter) writeOwned(path string, data []byte) error {
|
|
return a.files.AtomicWrite(path, data, 0o600, a.ownership())
|
|
}
|
|
func (a *Adapter) writeConfig(config map[string]json.RawMessage) error {
|
|
data, err := json.MarshalIndent(config, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return a.writeOwned(a.configuration.ConfigFile, append(data, '\n'))
|
|
}
|
|
|
|
type redactedError struct {
|
|
message string
|
|
class error
|
|
}
|
|
|
|
func (e *redactedError) Error() string { return e.message }
|
|
func (e *redactedError) Is(other error) bool { return e.class != nil && errors.Is(e.class, other) }
|
|
func redactError(err error, active *profile.Profile) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
message := profile.RedactText(err.Error())
|
|
if active != nil {
|
|
for _, raw := range []string{active.SingleURL, active.HTTPURL, active.HTTPSURL, active.AllURL} {
|
|
if raw != "" {
|
|
message = strings.ReplaceAll(message, raw, profile.ObscureURL(raw))
|
|
}
|
|
}
|
|
}
|
|
var class error
|
|
for _, safe := range []error{os.ErrPermission, fs.ErrNotExist, fs.ErrExist, context.Canceled, context.DeadlineExceeded} {
|
|
if errors.Is(err, safe) {
|
|
class = safe
|
|
break
|
|
}
|
|
}
|
|
return &redactedError{message: message, class: class}
|
|
}
|