diff --git a/common/pkg/json-proxy/handler.go b/common/pkg/json-proxy/handler.go index 718f673230..546f3ca608 100644 --- a/common/pkg/json-proxy/handler.go +++ b/common/pkg/json-proxy/handler.go @@ -10,10 +10,12 @@ import ( "os" "sync" "syscall" + "time" "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" + "go.podman.io/common/pkg/config" "go.podman.io/image/v5/image" "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/pkg/blobinfocache" @@ -106,6 +108,54 @@ func (h *handler) Initialize(ctx context.Context, args []any) (replyBuf, error) return r, nil } +// GetEngineConfig returns a curated subset of the host containers.conf +// [engine] settings (new in 0.2.9). Today this is the retry policy; clients +// use it to drive retries with the same defaults as `podman pull` without +// having to read containers.conf themselves. +// +// Note the proxy only advertises the configuration; it does not perform +// retries itself, since a partially streamed blob cannot be transparently +// resumed. +func (h *handler) GetEngineConfig(_ context.Context, args []any) (replyBuf, error) { + h.lock.Lock() + defer h.lock.Unlock() + + var ret replyBuf + + if h.sysctx == nil { + return ret, errors.New("client error: must invoke Initialize") + } + if len(args) != 0 { + return ret, errors.New("invalid request, expecting zero arguments") + } + + // config.Default() memoizes internally, so this is cheap to call here. + cfg, err := config.Default() + if err != nil { + return ret, fmt.Errorf("loading containers.conf: %w", err) + } + + ec := engineConfig{ + Retry: cfg.Engine.Retry, + } + // An empty retry_delay means "use exponential backoff"; only advertise a + // concrete value when one is configured. We normalize to milliseconds so + // clients don't have to parse Go's duration syntax. + if cfg.Engine.RetryDelay != "" { + d, err := time.ParseDuration(cfg.Engine.RetryDelay) + if err != nil { + return ret, fmt.Errorf("parsing containers.conf retry_delay %q: %w", cfg.Engine.RetryDelay, err) + } + // A negative delay is meaningless; treat it as unset (exponential + // backoff) rather than advertising a nonsensical value to clients. + if ms := d.Milliseconds(); ms > 0 { + ec.RetryDelayMS = &ms + } + } + + return replyBuf{value: ec}, nil +} + // OpenImage accepts a string image reference i.e. TRANSPORT:REF - like `skopeo copy`. // The return value is an opaque integer handle. func (h *handler) OpenImage(ctx context.Context, args []any) (replyBuf, error) { @@ -706,6 +756,8 @@ func (h *handler) processRequest(ctx context.Context, readBytes []byte) (rb repl switch req.Method { case "Initialize": rb, err = h.Initialize(ctx, req.Args) + case "GetEngineConfig": + rb, err = h.GetEngineConfig(ctx, req.Args) case "OpenImage": rb, err = h.OpenImage(ctx, req.Args) case "OpenImageOptional": diff --git a/common/pkg/json-proxy/proxy_test.go b/common/pkg/json-proxy/proxy_test.go index dfe44d5ba6..aed8254da4 100644 --- a/common/pkg/json-proxy/proxy_test.go +++ b/common/pkg/json-proxy/proxy_test.go @@ -506,6 +506,32 @@ func TestProxyGetBlob(t *testing.T) { assert.NoError(t, err) } +func TestProxyGetEngineConfig(t *testing.T) { + p := newProxy(t) + + v, err := p.callNoFd("GetEngineConfig", nil) + require.NoError(t, err) + + // The reply value is a JSON object; it arrives as a map here. + m, ok := v.(map[string]any) + require.True(t, ok, "GetEngineConfig return value is %T", v) + + // retry is always present. We don't assert a specific number since it + // comes from the host containers.conf, but it must be a non-negative + // integer (default 3). + retry, ok := m["retry"].(float64) + require.True(t, ok, "retry is %T", m["retry"]) + assert.GreaterOrEqual(t, retry, float64(0)) + + // retry_delay_ms is optional (omitted when unset => exponential backoff). + // When present it must be a non-negative integer count of milliseconds. + if raw, present := m["retry_delay_ms"]; present { + delay, ok := raw.(float64) + require.True(t, ok, "retry_delay_ms is %T", raw) + assert.GreaterOrEqual(t, delay, float64(0)) + } +} + func TestProxyPolicyVerification(t *testing.T) { for _, tc := range []struct { name string diff --git a/common/pkg/json-proxy/types.go b/common/pkg/json-proxy/types.go index fd878392da..fb5ac3d242 100644 --- a/common/pkg/json-proxy/types.go +++ b/common/pkg/json-proxy/types.go @@ -17,7 +17,11 @@ import ( // departure from the original code which used HTTP. // // When bumping this, please also update the man page. -const protocolVersion = "0.2.8" +// +// 0.2.9: Added the GetEngineConfig method, which exposes the host +// containers.conf engine settings (e.g. retry policy) so clients can apply +// them without re-reading the config themselves. +const protocolVersion = "0.2.9" // maxMsgSize is the current limit on a packet size. // Note that all non-metadata (i.e. payload data) is sent over a pipe. @@ -71,6 +75,23 @@ type reply struct { Error string `json:"error"` } +// engineConfig is the value returned by GetEngineConfig (new in 0.2.9). It +// exposes a curated subset of the host containers.conf [engine] settings that +// are relevant to a client driving image pulls through the proxy. +// +// This is intentionally extensible: fields are additive and optional, so +// clients using serde(default)-style decoding remain forward compatible as new +// settings are surfaced here. +type engineConfig struct { + // Retry is the number of times a failed pull should be retried + // (containers.conf [engine] retry; default 3). + Retry uint `json:"retry"` + // RetryDelayMS is the fixed delay between retries in milliseconds + // (containers.conf [engine] retry_delay). It is omitted when unset, in + // which case the client should use exponential backoff. + RetryDelayMS *int64 `json:"retry_delay_ms,omitempty"` +} + // replyBuf is our internal deserialization of reply plus optional fd. type replyBuf struct { // value will be converted to a reply Value.