1
0
mirror of https://github.com/containers/podman.git synced 2026-02-05 15:45:08 +01:00

Update common, image, and storage deps to 94e31d2

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
This commit is contained in:
renovate[bot]
2025-11-30 01:50:27 +00:00
committed by GitHub
parent 89844e51d5
commit 89ddae8b12
39 changed files with 1384 additions and 998 deletions

View File

@@ -23,19 +23,28 @@ import (
)
func main() {
apiClient, err := client.New(client.FromEnv, client.WithAPIVersionNegotiation())
// Create a new client that handles common environment variables
// for configuration (DOCKER_HOST, DOCKER_API_VERSION), and does
// API-version negotiation to allow downgrading the API version
// when connecting with an older daemon version.
apiClient, err := client.New(client.FromEnv)
if err != nil {
panic(err)
}
defer apiClient.Close()
containers, err := apiClient.ContainerList(context.Background(), client.ContainerListOptions{All: true})
// List all containers (both stopped and running).
result, err := apiClient.ContainerList(context.Background(), client.ContainerListOptions{
All: true,
})
if err != nil {
panic(err)
}
for _, ctr := range containers {
fmt.Printf("%s %s (status: %s)\n", ctr.ID, ctr.Image, ctr.Status)
// Print each container's ID, status and the image it was created from.
fmt.Printf("%s %-22s %s\n", "ID", "STATUS", "IMAGE")
for _, ctr := range result.Items {
fmt.Printf("%s %-22s %s\n", ctr.ID, ctr.Status, ctr.Image)
}
}
```

View File

@@ -8,10 +8,8 @@ https://docs.docker.com/reference/api/engine/
You use the library by constructing a client object using [New]
and calling methods on it. The client can be configured from environment
variables by passing the [FromEnv] option, and the [WithAPIVersionNegotiation]
option to allow downgrading the API version used when connecting with an older
daemon version. Other options cen be configured manually by passing any of
the available [Opt] options.
variables by passing the [FromEnv] option. Other options can be configured
manually by passing any of the available [Opt] options.
For example, to list running containers (the equivalent of "docker ps"):
@@ -30,7 +28,7 @@ For example, to list running containers (the equivalent of "docker ps"):
// for configuration (DOCKER_HOST, DOCKER_API_VERSION), and does
// API-version negotiation to allow downgrading the API version
// when connecting with an older daemon version.
apiClient, err := client.New(client.FromEnv, client.WithAPIVersionNegotiation())
apiClient, err := client.New(client.FromEnv)
if err != nil {
log.Fatal(err)
}
@@ -103,18 +101,16 @@ import (
const DummyHost = "api.moby.localhost"
// MaxAPIVersion is the highest REST API version supported by the client.
// If API-version negotiation is enabled (see [WithAPIVersionNegotiation],
// [Client.NegotiateAPIVersion]), the client may downgrade its API version.
// Similarly, the [WithVersion] and [WithVersionFromEnv] allow overriding
// the version.
// If API-version negotiation is enabled, the client may downgrade its API version.
// Similarly, the [WithAPIVersion] and [WithAPIVersionFromEnv] options allow
// overriding the version and disable API-version negotiation.
//
// This version may be lower than the version of the api library module used.
const MaxAPIVersion = "1.52"
// fallbackAPIVersion is the version to fall back to if API-version negotiation
// fails. API versions below this version are not supported by the client,
// and not considered when negotiating.
const fallbackAPIVersion = "1.44"
// MinAPIVersion is the minimum API version supported by the client. API versions
// below this version are not considered when performing API-version negotiation.
const MinAPIVersion = "1.44"
// Ensure that Client always implements APIClient.
var _ APIClient = &Client{}
@@ -174,8 +170,13 @@ func NewClientWithOpts(ops ...Opt) (*Client, error) {
// It takes an optional list of [Opt] functional arguments, which are applied in
// the order they're provided, which allows modifying the defaults when creating
// the client. For example, the following initializes a client that configures
// itself with values from environment variables ([FromEnv]), and has automatic
// API version negotiation enabled ([WithAPIVersionNegotiation]).
// itself with values from environment variables ([FromEnv]).
//
// By default, the client automatically negotiates the API version to use when
// making requests. API version negotiation is performed on the first request;
// subsequent requests do not re-negotiate. Use [WithAPIVersion] or
// [WithAPIVersionFromEnv] to configure the client with a fixed API version
// and disable API version negotiation.
//
// cli, err := client.New(
// client.FromEnv,
@@ -213,6 +214,12 @@ func New(ops ...Opt) (*Client, error) {
}
}
if cfg.envAPIVersion != "" {
c.setAPIVersion(cfg.envAPIVersion)
} else if cfg.manualAPIVersion != "" {
c.setAPIVersion(cfg.manualAPIVersion)
}
if tr, ok := c.client.Transport.(*http.Transport); ok {
// Store the base transport before we wrap it in tracing libs below
// This is used, as an example, to close idle connections when the client is closed
@@ -278,7 +285,7 @@ func (cli *Client) Close() error {
// be negotiated when making the actual requests, and for which cases
// we cannot do the negotiation lazily.
func (cli *Client) checkVersion(ctx context.Context) error {
if cli.manualOverride || !cli.negotiateVersion || cli.negotiated.Load() {
if cli.negotiated.Load() {
return nil
}
_, err := cli.Ping(ctx, PingOptions{
@@ -306,36 +313,47 @@ func (cli *Client) ClientVersion() string {
}
// negotiateAPIVersion updates the version to match the API version from
// the ping response. It falls back to the lowest version supported if the
// API version is empty, or returns an error if the API version is lower than
// the lowest supported API version, in which case the version is not modified.
// the ping response.
//
// It returns an error if version is invalid, or lower than the minimum
// supported API version in which case the client's API version is not
// updated, and negotiation is not marked as completed.
func (cli *Client) negotiateAPIVersion(pingVersion string) error {
pingVersion = strings.TrimPrefix(pingVersion, "v")
if pingVersion == "" {
// TODO(thaJeztah): consider returning an error on empty value or not falling back; see https://github.com/moby/moby/pull/51119#discussion_r2413148487
pingVersion = fallbackAPIVersion
} else if versions.LessThan(pingVersion, fallbackAPIVersion) {
return cerrdefs.ErrInvalidArgument.WithMessage(fmt.Sprintf("API version %s is not supported by this client: the minimum supported API version is %s", pingVersion, fallbackAPIVersion))
var err error
pingVersion, err = parseAPIVersion(pingVersion)
if err != nil {
return err
}
if versions.LessThan(pingVersion, MinAPIVersion) {
return cerrdefs.ErrInvalidArgument.WithMessage(fmt.Sprintf("API version %s is not supported by this client: the minimum supported API version is %s", pingVersion, MinAPIVersion))
}
// if the client is not initialized with a version, start with the latest supported version
if cli.version == "" {
cli.version = MaxAPIVersion
negotiatedVersion := cli.version
if negotiatedVersion == "" {
negotiatedVersion = MaxAPIVersion
}
// if server version is lower than the client version, downgrade
if versions.LessThan(pingVersion, cli.version) {
cli.version = pingVersion
if versions.LessThan(pingVersion, negotiatedVersion) {
negotiatedVersion = pingVersion
}
// Store the results, so that automatic API version negotiation (if enabled)
// won't be performed on the next request.
if cli.negotiateVersion {
cli.negotiated.Store(true)
}
cli.setAPIVersion(negotiatedVersion)
return nil
}
// setAPIVersion sets the client's API version and marks API version negotiation
// as completed, so that automatic API version negotiation (if enabled) won't
// be performed on the next request.
func (cli *Client) setAPIVersion(version string) {
cli.version = version
cli.negotiated.Store(true)
}
// DaemonHost returns the host address used by the client
func (cli *Client) DaemonHost() string {
return cli.host

View File

@@ -38,14 +38,22 @@ type clientConfig struct {
userAgent *string
// custom HTTP headers configured by users.
customHTTPHeaders map[string]string
// manualOverride is set to true when the version was set by users.
manualOverride bool
// negotiateVersion indicates if the client should automatically negotiate
// the API version to use when making requests. API version negotiation is
// performed on the first request, after which negotiated is set to "true"
// so that subsequent requests do not re-negotiate.
negotiateVersion bool
// manualAPIVersion contains the API version set by users. This field
// will only be non-empty if a valid-formed version was set through
// [WithAPIVersion].
//
// If both manualAPIVersion and envAPIVersion are set, manualAPIVersion
// takes precedence. Either field disables API-version negotiation.
manualAPIVersion string
// envAPIVersion contains the API version set by users. This field
// will only be non-empty if a valid-formed version was set through
// [WithAPIVersionFromEnv].
//
// If both manualAPIVersion and envAPIVersion are set, manualAPIVersion
// takes precedence. Either field disables API-version negotiation.
envAPIVersion string
// traceOpts is a list of options to configure the tracing span.
traceOpts []otelhttp.Option
@@ -56,7 +64,7 @@ type Opt func(*clientConfig) error
// FromEnv configures the client with values from environment variables. It
// is the equivalent of using the [WithTLSClientConfigFromEnv], [WithHostFromEnv],
// and [WithVersionFromEnv] options.
// and [WithAPIVersionFromEnv] options.
//
// FromEnv uses the following environment variables:
//
@@ -71,7 +79,7 @@ func FromEnv(c *clientConfig) error {
ops := []Opt{
WithTLSClientConfigFromEnv(),
WithHostFromEnv(),
WithVersionFromEnv(),
WithAPIVersionFromEnv(),
}
for _, op := range ops {
if err := op(c); err != nil {
@@ -241,18 +249,59 @@ func WithTLSClientConfigFromEnv() Opt {
}
}
// WithVersion overrides the client version with the specified one. If an empty
// version is provided, the value is ignored to allow version negotiation
// (see [WithAPIVersionNegotiation]).
// WithAPIVersion overrides the client's API version with the specified one,
// and disables API version negotiation. If an empty version is provided,
// this option is ignored to allow version negotiation. The given version
// should be formatted "<major>.<minor>" (for example, "1.52"). It returns
// an error if the given value not in the correct format.
//
// WithVersion does not validate if the client supports the given version,
// and callers should verify if the version is in the correct format and
// lower than the maximum supported version as defined by [MaxAPIVersion].
func WithVersion(version string) Opt {
// WithAPIVersion does not validate if the client supports the given version,
// and callers should verify if the version lower than the maximum supported
// version as defined by [MaxAPIVersion].
//
// [WithAPIVersionFromEnv] takes precedence if [WithAPIVersion] and
// [WithAPIVersionFromEnv] are both set.
func WithAPIVersion(version string) Opt {
return func(c *clientConfig) error {
if v := strings.TrimPrefix(version, "v"); v != "" {
c.version = v
c.manualOverride = true
version = strings.TrimSpace(version)
if val := strings.TrimPrefix(version, "v"); val != "" {
ver, err := parseAPIVersion(val)
if err != nil {
return fmt.Errorf("invalid API version (%s): %w", version, err)
}
c.manualAPIVersion = ver
}
return nil
}
}
// WithVersion overrides the client version with the specified one.
//
// Deprecated: use [WithAPIVersion] instead.
func WithVersion(version string) Opt {
return WithAPIVersion(version)
}
// WithAPIVersionFromEnv overrides the client version with the version specified in
// the DOCKER_API_VERSION ([EnvOverrideAPIVersion]) environment variable.
// If DOCKER_API_VERSION is not set, or set to an empty value, the version
// is not modified.
//
// WithAPIVersion does not validate if the client supports the given version,
// and callers should verify if the version lower than the maximum supported
// version as defined by [MaxAPIVersion].
//
// [WithAPIVersionFromEnv] takes precedence if [WithAPIVersion] and
// [WithAPIVersionFromEnv] are both set.
func WithAPIVersionFromEnv() Opt {
return func(c *clientConfig) error {
version := strings.TrimSpace(os.Getenv(EnvOverrideAPIVersion))
if val := strings.TrimPrefix(version, "v"); val != "" {
ver, err := parseAPIVersion(val)
if err != nil {
return fmt.Errorf("invalid API version (%s): %w", version, err)
}
c.envAPIVersion = ver
}
return nil
}
@@ -260,25 +309,21 @@ func WithVersion(version string) Opt {
// WithVersionFromEnv overrides the client version with the version specified in
// the DOCKER_API_VERSION ([EnvOverrideAPIVersion]) environment variable.
// If DOCKER_API_VERSION is not set, or set to an empty value, the version
// is not modified.
//
// WithVersion does not validate if the client supports the given version,
// and callers should verify if the version is in the correct format and
// lower than the maximum supported version as defined by [MaxAPIVersion].
// Deprecated: use [WithAPIVersionFromEnv] instead.
func WithVersionFromEnv() Opt {
return func(c *clientConfig) error {
return WithVersion(os.Getenv(EnvOverrideAPIVersion))(c)
}
return WithAPIVersionFromEnv()
}
// WithAPIVersionNegotiation enables automatic API version negotiation for the client.
// With this option enabled, the client automatically negotiates the API version
// to use when making requests. API version negotiation is performed on the first
// request; subsequent requests do not re-negotiate.
//
// Deprecated: API-version negotiation is now enabled by default. Use [WithAPIVersion]
// or [WithAPIVersionFromEnv] to disable API version negotiation.
func WithAPIVersionNegotiation() Opt {
return func(c *clientConfig) error {
c.negotiateVersion = true
return nil
}
}

View File

@@ -13,7 +13,7 @@ const (
// be used to override the API version to use. Value must be
// formatted as MAJOR.MINOR, for example, "1.19".
//
// This env-var is read by [FromEnv] and [WithVersionFromEnv] and when set to a
// This env-var is read by [FromEnv] and [WithAPIVersionFromEnv] and when set to a
// non-empty value, takes precedence over API version negotiation.
//
// This environment variable should be used for debugging purposes only, as

View File

@@ -20,7 +20,7 @@ type PingOptions struct {
//
// If a manual override is in place, either through the "DOCKER_API_VERSION"
// ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized
// with a fixed version ([WithVersion]), no negotiation is performed.
// with a fixed version ([WithAPIVersion]), no negotiation is performed.
//
// If the API server's ping response does not contain an API version, or if the
// client did not get a successful ping response, it assumes it is connected with
@@ -29,9 +29,8 @@ type PingOptions struct {
NegotiateAPIVersion bool
// ForceNegotiate forces the client to re-negotiate the API version, even if
// API-version negotiation already happened. This option cannot be
// used if the client is configured with a fixed version using (using
// [WithVersion] or [WithVersionFromEnv]).
// API-version negotiation already happened or it the client is configured
// with a fixed version (using [WithAPIVersion] or [WithAPIVersionFromEnv]).
//
// This option has no effect if NegotiateAPIVersion is not set.
ForceNegotiate bool
@@ -72,10 +71,12 @@ type SwarmStatus struct {
// for other non-success status codes, failing to connect to the API, or failing
// to parse the API response.
func (cli *Client) Ping(ctx context.Context, options PingOptions) (PingResult, error) {
if cli.manualOverride {
if !options.NegotiateAPIVersion {
// No API version negotiation needed; just return ping response.
return cli.ping(ctx)
}
if !options.NegotiateAPIVersion && !cli.negotiateVersion {
if cli.negotiated.Load() && !options.ForceNegotiate {
// API version was already negotiated or manually set.
return cli.ping(ctx)
}
@@ -85,10 +86,19 @@ func (cli *Client) Ping(ctx context.Context, options PingOptions) (PingResult, e
ping, err := cli.ping(ctx)
if err != nil {
return cli.ping(ctx)
return ping, err
}
if cli.negotiated.Load() && !options.ForceNegotiate {
// API version was already negotiated or manually set.
//
// We check cli.negotiated again under lock, to account for race
// conditions with the check at the start of this function.
return ping, nil
}
if ping.APIVersion == "" {
cli.setAPIVersion(MaxAPIVersion)
return ping, nil
}
@@ -112,10 +122,15 @@ func (cli *Client) ping(ctx context.Context) (PingResult, error) {
// response-body to get error details from.
return newPingResult(resp), nil
}
// close to allow reusing connection.
ensureReaderClosed(resp)
// HEAD failed or returned a non-OK status; fallback to GET.
req.Method = http.MethodGet
resp, err = cli.doRequest(req)
req2, err := cli.buildRequest(ctx, http.MethodGet, path.Join(cli.basePath, "/_ping"), nil, nil)
if err != nil {
return PingResult{}, err
}
resp, err = cli.doRequest(req2)
defer ensureReaderClosed(resp)
if err != nil {
// Failed to connect.

View File

@@ -67,31 +67,22 @@ func (cli *Client) delete(ctx context.Context, path string, query url.Values, he
// prepareJSONRequest encodes the given body to JSON and returns it as an [io.Reader], and sets the Content-Type
// header. If body is nil, or a nil-interface, a "nil" body is returned without
// error.
//
// TODO(thaJeztah): should this return an error if a different Content-Type is already set?
// TODO(thaJeztah): is "nil" the appropriate approach for an empty body, or should we use [http.NoBody] (or similar)?
func prepareJSONRequest(body any, headers http.Header) (io.Reader, http.Header, error) {
if body == nil {
return nil, headers, nil
}
// encoding/json encodes a nil pointer as the JSON document `null`,
// irrespective of whether the type implements json.Marshaler or encoding.TextMarshaler.
// That is almost certainly not what the caller intended as the request body.
//
// TODO(thaJeztah): consider moving this to jsonEncode, which would also allow returning an (empty) reader instead of nil.
if reflect.TypeOf(body).Kind() == reflect.Ptr && reflect.ValueOf(body).IsNil() {
return nil, headers, nil
}
jsonBody, err := jsonEncode(body)
if err != nil {
return nil, headers, err
}
if jsonBody == nil || jsonBody == http.NoBody {
// no content-type is set on empty requests.
return jsonBody, headers, nil
}
hdr := http.Header{}
if headers != nil {
hdr = headers.Clone()
}
// TODO(thaJeztah): should this return an error if a different Content-Type is already set?
hdr.Set("Content-Type", "application/json")
return jsonBody, hdr, nil
}
@@ -110,9 +101,6 @@ func (cli *Client) buildRequest(ctx context.Context, method, path string, body i
req.Host = DummyHost
}
if body != nil && req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "text/plain")
}
return req, nil
}
@@ -248,7 +236,11 @@ func checkResponseErr(serverResp *http.Response) (retErr error) {
if statusMsg == "" {
statusMsg = http.StatusText(serverResp.StatusCode)
}
if serverResp.Body != nil {
var reqMethod string
if serverResp.Request != nil {
reqMethod = serverResp.Request.Method
}
if serverResp.Body != nil && reqMethod != http.MethodHead {
bodyMax := 1 * 1024 * 1024 // 1 MiB
bodyR := &io.LimitedReader{
R: serverResp.Body,
@@ -333,25 +325,49 @@ func (cli *Client) addHeaders(req *http.Request, headers http.Header) *http.Requ
}
func jsonEncode(data any) (io.Reader, error) {
var params bytes.Buffer
if data != nil {
if err := json.NewEncoder(&params).Encode(data); err != nil {
return nil, err
switch x := data.(type) {
case nil:
return http.NoBody, nil
case io.Reader:
// http.NoBody or other readers
return x, nil
case json.RawMessage:
if len(x) == 0 {
return http.NoBody, nil
}
return bytes.NewReader(x), nil
}
return &params, nil
// encoding/json encodes a nil pointer as the JSON document `null`,
// irrespective of whether the type implements json.Marshaler or encoding.TextMarshaler.
// That is almost certainly not what the caller intended as the request body.
if v := reflect.ValueOf(data); v.Kind() == reflect.Ptr && v.IsNil() {
return http.NoBody, nil
}
b, err := json.Marshal(data)
if err != nil {
return nil, err
}
return bytes.NewReader(b), nil
}
func ensureReaderClosed(response *http.Response) {
if response != nil && response.Body != nil {
// Drain up to 512 bytes and close the body to let the Transport reuse the connection
// see https://github.com/google/go-github/pull/317/files#r57536827
//
// TODO(thaJeztah): see if this optimization is still needed, or already implemented in stdlib,
// and check if context-cancellation should handle this as well. If still needed, consider
// wrapping response.Body, or returning a "closer()" from [Client.sendRequest] and related
// methods.
_, _ = io.CopyN(io.Discard, response.Body, 512)
_ = response.Body.Close()
if response == nil || response.Body == nil {
return
}
if response.ContentLength == 0 || (response.Request != nil && response.Request.Method == http.MethodHead) {
// No need to drain head requests or zero-length responses.
_ = response.Body.Close()
return
}
// Drain up to 512 bytes and close the body to let the Transport reuse the connection
// see https://github.com/google/go-github/pull/317/files#r57536827
//
// TODO(thaJeztah): see if this optimization is still needed, or already implemented in stdlib,
// and check if context-cancellation should handle this as well. If still needed, consider
// wrapping response.Body, or returning a "closer()" from [Client.sendRequest] and related
// methods.
_, _ = io.CopyN(io.Discard, response.Body, 512)
_ = response.Body.Close()
}

View File

@@ -59,16 +59,18 @@ func (cli *Client) ServiceCreate(ctx context.Context, options ServiceCreateOptio
options.Spec.TaskTemplate.ContainerSpec.Image = taggedImg
}
if options.QueryRegistry {
resolveWarning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth)
warnings = append(warnings, resolveWarning)
if warning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" {
warnings = append(warnings, warning)
}
}
case options.Spec.TaskTemplate.PluginSpec != nil:
if taggedImg := imageWithTagString(options.Spec.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
options.Spec.TaskTemplate.PluginSpec.Remote = taggedImg
}
if options.QueryRegistry {
resolveWarning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth)
warnings = append(warnings, resolveWarning)
if warning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" {
warnings = append(warnings, warning)
}
}
}
@@ -93,35 +95,33 @@ func (cli *Client) ServiceCreate(ctx context.Context, options ServiceCreateOptio
}
func resolveContainerSpecImage(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string {
var warning string
if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.ContainerSpec.Image, encodedAuth); err != nil {
warning = digestWarning(taskSpec.ContainerSpec.Image)
} else {
taskSpec.ContainerSpec.Image = img
if len(imgPlatforms) > 0 {
if taskSpec.Placement == nil {
taskSpec.Placement = &swarm.Placement{}
}
taskSpec.Placement.Platforms = imgPlatforms
}
img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.ContainerSpec.Image, encodedAuth)
if err != nil {
return digestWarning(taskSpec.ContainerSpec.Image)
}
return warning
taskSpec.ContainerSpec.Image = img
if len(imgPlatforms) > 0 {
if taskSpec.Placement == nil {
taskSpec.Placement = &swarm.Placement{}
}
taskSpec.Placement.Platforms = imgPlatforms
}
return ""
}
func resolvePluginSpecRemote(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string {
var warning string
if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.PluginSpec.Remote, encodedAuth); err != nil {
warning = digestWarning(taskSpec.PluginSpec.Remote)
} else {
taskSpec.PluginSpec.Remote = img
if len(imgPlatforms) > 0 {
if taskSpec.Placement == nil {
taskSpec.Placement = &swarm.Placement{}
}
taskSpec.Placement.Platforms = imgPlatforms
}
img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.PluginSpec.Remote, encodedAuth)
if err != nil {
return digestWarning(taskSpec.PluginSpec.Remote)
}
return warning
taskSpec.PluginSpec.Remote = img
if len(imgPlatforms) > 0 {
if taskSpec.Placement == nil {
taskSpec.Placement = &swarm.Placement{}
}
taskSpec.Placement.Platforms = imgPlatforms
}
return ""
}
func imageDigestAndPlatforms(ctx context.Context, cli DistributionAPIClient, image, encodedAuth string) (string, []swarm.Platform, error) {

View File

@@ -82,16 +82,18 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, options
options.Spec.TaskTemplate.ContainerSpec.Image = taggedImg
}
if options.QueryRegistry {
resolveWarning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth)
warnings = append(warnings, resolveWarning)
if warning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" {
warnings = append(warnings, warning)
}
}
case options.Spec.TaskTemplate.PluginSpec != nil:
if taggedImg := imageWithTagString(options.Spec.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
options.Spec.TaskTemplate.PluginSpec.Remote = taggedImg
}
if options.QueryRegistry {
resolveWarning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth)
warnings = append(warnings, resolveWarning)
if warning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" {
warnings = append(warnings, warning)
}
}
}

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"strconv"
"strings"
"sync"
@@ -32,6 +33,47 @@ func trimID(objType, id string) (string, error) {
return id, nil
}
// parseAPIVersion checks v to be a well-formed ("<major>.<minor>")
// API version. It returns an error if the value is empty or does not
// have the correct format, but does not validate if the API version is
// within the supported range ([MinAPIVersion] <= v <= [MaxAPIVersion]).
//
// It returns version after normalizing, or an error if validation failed.
func parseAPIVersion(version string) (string, error) {
if strings.TrimPrefix(strings.TrimSpace(version), "v") == "" {
return "", cerrdefs.ErrInvalidArgument.WithMessage("value is empty")
}
major, minor, err := parseMajorMinor(version)
if err != nil {
return "", err
}
return fmt.Sprintf("%d.%d", major, minor), nil
}
// parseMajorMinor is a helper for parseAPIVersion.
func parseMajorMinor(v string) (major, minor int, _ error) {
if strings.HasPrefix(v, "v") {
return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("must be formatted <major>.<minor>")
}
if strings.TrimSpace(v) == "" {
return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("value is empty")
}
majVer, minVer, ok := strings.Cut(v, ".")
if !ok {
return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("must be formatted <major>.<minor>")
}
major, err := strconv.Atoi(majVer)
if err != nil {
return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("invalid major version: must be formatted <major>.<minor>")
}
minor, err = strconv.Atoi(minVer)
if err != nil {
return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("invalid minor version: must be formatted <major>.<minor>")
}
return major, minor, nil
}
// encodePlatforms marshals the given platform(s) to JSON format, to
// be used for query-parameters for filtering / selecting platforms.
func encodePlatforms(platform ...ocispec.Platform) ([]string, error) {

View File

@@ -0,0 +1,3 @@
// Package linux provides minimal wrappers around Linux system calls, primarily
// to provide support for automatic EINTR-retries.
package linux

View File

@@ -0,0 +1,28 @@
package linux
import (
"errors"
"golang.org/x/sys/unix"
)
// retryOnEINTR takes a function that returns an error and calls it
// until the error returned is not EINTR.
func retryOnEINTR(fn func() error) error {
for {
err := fn()
if !errors.Is(err, unix.EINTR) {
return err
}
}
}
// retryOnEINTR2 is like retryOnEINTR, but it returns 2 values.
func retryOnEINTR2[T any](fn func() (T, error)) (T, error) {
for {
val, err := fn()
if !errors.Is(err, unix.EINTR) {
return val, err
}
}
}

View File

@@ -0,0 +1,126 @@
package linux
import (
"os"
"unsafe"
"golang.org/x/sys/unix"
)
// Dup3 wraps [unix.Dup3].
func Dup3(oldfd, newfd, flags int) error {
err := retryOnEINTR(func() error {
return unix.Dup3(oldfd, newfd, flags)
})
return os.NewSyscallError("dup3", err)
}
// Exec wraps [unix.Exec].
func Exec(cmd string, args []string, env []string) error {
err := retryOnEINTR(func() error {
return unix.Exec(cmd, args, env)
})
if err != nil {
return &os.PathError{Op: "exec", Path: cmd, Err: err}
}
return nil
}
// Getwd wraps [unix.Getwd].
func Getwd() (wd string, err error) {
wd, err = retryOnEINTR2(unix.Getwd)
return wd, os.NewSyscallError("getwd", err)
}
// Open wraps [unix.Open].
func Open(path string, mode int, perm uint32) (fd int, err error) {
fd, err = retryOnEINTR2(func() (int, error) {
return unix.Open(path, mode, perm)
})
if err != nil {
return -1, &os.PathError{Op: "open", Path: path, Err: err}
}
return fd, nil
}
// Openat wraps [unix.Openat].
func Openat(dirfd int, path string, mode int, perm uint32) (fd int, err error) {
fd, err = retryOnEINTR2(func() (int, error) {
return unix.Openat(dirfd, path, mode, perm)
})
if err != nil {
return -1, &os.PathError{Op: "openat", Path: path, Err: err}
}
return fd, nil
}
// Recvfrom wraps [unix.Recvfrom].
func Recvfrom(fd int, p []byte, flags int) (n int, from unix.Sockaddr, err error) {
err = retryOnEINTR(func() error {
n, from, err = unix.Recvfrom(fd, p, flags)
return err
})
if err != nil {
return 0, nil, os.NewSyscallError("recvfrom", err)
}
return n, from, err
}
// Sendmsg wraps [unix.Sendmsg].
func Sendmsg(fd int, p, oob []byte, to unix.Sockaddr, flags int) error {
err := retryOnEINTR(func() error {
return unix.Sendmsg(fd, p, oob, to, flags)
})
return os.NewSyscallError("sendmsg", err)
}
// SetMempolicy wraps set_mempolicy.
func SetMempolicy(mode uint, mask *unix.CPUSet) error {
err := retryOnEINTR(func() error {
_, _, errno := unix.Syscall(unix.SYS_SET_MEMPOLICY, uintptr(mode), uintptr(unsafe.Pointer(mask)), unsafe.Sizeof(*mask)*8)
if errno != 0 {
return errno
}
return nil
})
return os.NewSyscallError("set_mempolicy", err)
}
// Readlinkat wraps [unix.Readlinkat].
func Readlinkat(dir *os.File, path string) (string, error) {
size := 4096
for {
linkBuf := make([]byte, size)
n, err := retryOnEINTR2(func() (int, error) {
return unix.Readlinkat(int(dir.Fd()), path, linkBuf)
})
if err != nil {
return "", &os.PathError{Op: "readlinkat", Path: dir.Name() + "/" + path, Err: err}
}
if n != size {
return string(linkBuf[:n]), nil
}
// Possible truncation, resize the buffer.
size *= 2
}
}
// GetPtyPeer is a wrapper for ioctl(TIOCGPTPEER).
func GetPtyPeer(ptyFd uintptr, unsafePeerPath string, flags int) (*os.File, error) {
// Make sure O_NOCTTY is always set -- otherwise runc might accidentally
// gain it as a controlling terminal. O_CLOEXEC also needs to be set to
// make sure we don't leak the handle either.
flags |= unix.O_NOCTTY | unix.O_CLOEXEC
// There is no nice wrapper for this kind of ioctl in unix.
peerFd, _, errno := unix.Syscall(
unix.SYS_IOCTL,
ptyFd,
uintptr(unix.TIOCGPTPEER),
uintptr(flags),
)
if errno != 0 {
return nil, os.NewSyscallError("ioctl TIOCGPTPEER", errno)
}
return os.NewFile(peerFd, unsafePeerPath), nil
}

View File

@@ -0,0 +1,51 @@
// SPDX-License-Identifier: Apache-2.0
/*
* Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2024-2025 SUSE LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package pathrs
import (
"fmt"
"os"
"path/filepath"
)
// MkdirAllParentInRoot is like [MkdirAllInRoot] except that it only creates
// the parent directory of the target path, returning the trailing component so
// the caller has more flexibility around constructing the final inode.
//
// Callers need to be very careful operating on the trailing path, as trivial
// mistakes like following symlinks can cause security bugs. Most people
// should probably just use [MkdirAllInRoot] or [CreateInRoot].
func MkdirAllParentInRoot(root, unsafePath string, mode os.FileMode) (*os.File, string, error) {
// MkdirAllInRoot also does hallucinateUnsafePath, but we need to do it
// here first because when we split unsafePath into (dir, file) components
// we want to be doing so with the hallucinated path (so that trailing
// dangling symlinks are treated correctly).
unsafePath, err := hallucinateUnsafePath(root, unsafePath)
if err != nil {
return nil, "", fmt.Errorf("failed to construct hallucinated target path: %w", err)
}
dirPath, filename := filepath.Split(unsafePath)
if filepath.Join("/", filename) == "/" {
return nil, "", fmt.Errorf("create parent dir in root subpath %q has bad trailing component %q", unsafePath, filename)
}
dirFd, err := MkdirAllInRoot(root, dirPath, mode)
return dirFd, filename, err
}

View File

@@ -21,14 +21,13 @@ package pathrs
import (
"fmt"
"os"
"path/filepath"
"github.com/cyphar/filepath-securejoin/pathrs-lite"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
// MkdirAllInRootOpen attempts to make
// MkdirAllInRoot attempts to make
//
// path, _ := securejoin.SecureJoin(root, unsafePath)
// os.MkdirAll(path, mode)
@@ -49,19 +48,10 @@ import (
// handling if unsafePath has already been scoped within the rootfs (this is
// needed for a lot of runc callers and fixing this would require reworking a
// lot of path logic).
func MkdirAllInRootOpen(root, unsafePath string, mode os.FileMode) (*os.File, error) {
// If the path is already "within" the root, get the path relative to the
// root and use that as the unsafe path. This is necessary because a lot of
// MkdirAllInRootOpen callers have already done SecureJoin, and refactoring
// all of them to stop using these SecureJoin'd paths would require a fair
// amount of work.
// TODO(cyphar): Do the refactor to libpathrs once it's ready.
if IsLexicallyInRoot(root, unsafePath) {
subPath, err := filepath.Rel(root, unsafePath)
if err != nil {
return nil, err
}
unsafePath = subPath
func MkdirAllInRoot(root, unsafePath string, mode os.FileMode) (*os.File, error) {
unsafePath, err := hallucinateUnsafePath(root, unsafePath)
if err != nil {
return nil, fmt.Errorf("failed to construct hallucinated target path: %w", err)
}
// Check for any silly mode bits.
@@ -87,13 +77,3 @@ func MkdirAllInRootOpen(root, unsafePath string, mode os.FileMode) (*os.File, er
return pathrs.MkdirAllHandle(rootDir, unsafePath, mode)
})
}
// MkdirAllInRoot is a wrapper around MkdirAllInRootOpen which closes the
// returned handle, for callers that don't need to use it.
func MkdirAllInRoot(root, unsafePath string, mode os.FileMode) error {
f, err := MkdirAllInRootOpen(root, unsafePath, mode)
if err == nil {
_ = f.Close()
}
return err
}

View File

@@ -19,7 +19,11 @@
package pathrs
import (
"os"
"path/filepath"
"strings"
securejoin "github.com/cyphar/filepath-securejoin"
)
// IsLexicallyInRoot is shorthand for strings.HasPrefix(path+"/", root+"/"),
@@ -32,3 +36,81 @@ func IsLexicallyInRoot(root, path string) bool {
path = strings.TrimRight(path, "/")
return strings.HasPrefix(path+"/", root+"/")
}
// LexicallyCleanPath makes a path safe for use with filepath.Join. This is
// done by not only cleaning the path, but also (if the path is relative)
// adding a leading '/' and cleaning it (then removing the leading '/'). This
// ensures that a path resulting from prepending another path will always
// resolve to lexically be a subdirectory of the prefixed path. This is all
// done lexically, so paths that include symlinks won't be safe as a result of
// using CleanPath.
func LexicallyCleanPath(path string) string {
// Deal with empty strings nicely.
if path == "" {
return ""
}
// Ensure that all paths are cleaned (especially problematic ones like
// "/../../../../../" which can cause lots of issues).
if filepath.IsAbs(path) {
return filepath.Clean(path)
}
// If the path isn't absolute, we need to do more processing to fix paths
// such as "../../../../<etc>/some/path". We also shouldn't convert absolute
// paths to relative ones.
path = filepath.Clean(string(os.PathSeparator) + path)
// This can't fail, as (by definition) all paths are relative to root.
path, _ = filepath.Rel(string(os.PathSeparator), path)
return path
}
// LexicallyStripRoot returns the passed path, stripping the root path if it
// was (lexicially) inside it. Note that both passed paths will always be
// treated as absolute, and the returned path will also always be absolute. In
// addition, the paths are cleaned before stripping the root.
func LexicallyStripRoot(root, path string) string {
// Make the paths clean and absolute.
root, path = LexicallyCleanPath("/"+root), LexicallyCleanPath("/"+path)
switch {
case path == root:
path = "/"
case root == "/":
// do nothing
default:
path = strings.TrimPrefix(path, root+"/")
}
return LexicallyCleanPath("/" + path)
}
// hallucinateUnsafePath creates a new unsafePath which has all symlinks
// (including dangling symlinks) fully resolved and any non-existent components
// treated as though they are real. This is effectively just a wrapper around
// [securejoin.SecureJoin] that strips the root. This path *IS NOT* safe to use
// as-is, you *MUST* operate on the returned path with pathrs-lite.
//
// The reason for this methods is that in previous runc versions, we would
// tolerate nonsense paths with dangling symlinks as path components.
// pathrs-lite does not support this, so instead we have to emulate this
// behaviour by doing SecureJoin *purely to get a semi-reasonable path to use*
// and then we use pathrs-lite to operate on the path safely.
//
// It would be quite difficult to emulate this in a race-free way in
// pathrs-lite, so instead we use [securejoin.SecureJoin] to simply produce a
// new candidate path for operations like [MkdirAllInRoot] so they can then
// operate on the new unsafePath as if it was what the user requested.
//
// If unsafePath is already lexically inside root, it is stripped before
// re-resolving it (this is done to ensure compatibility with legacy callers
// within runc that call SecureJoin before calling into pathrs).
func hallucinateUnsafePath(root, unsafePath string) (string, error) {
unsafePath = LexicallyStripRoot(root, unsafePath)
weirdPath, err := securejoin.SecureJoin(root, unsafePath)
if err != nil {
return "", err
}
unsafePath = LexicallyStripRoot(root, weirdPath)
return unsafePath, nil
}

View File

@@ -19,12 +19,12 @@
package pathrs
import (
"fmt"
"os"
"path/filepath"
"github.com/cyphar/filepath-securejoin/pathrs-lite"
"golang.org/x/sys/unix"
"github.com/opencontainers/runc/internal/linux"
)
// OpenInRoot opens the given path inside the root with the provided flags. It
@@ -48,12 +48,7 @@ func OpenInRoot(root, subpath string, flags int) (*os.File, error) {
// include it in the passed flags. The fileMode argument uses unix.* mode bits,
// *not* os.FileMode.
func CreateInRoot(root, subpath string, flags int, fileMode uint32) (*os.File, error) {
dir, filename := filepath.Split(subpath)
if filepath.Join("/", filename) == "/" {
return nil, fmt.Errorf("create in root subpath %q has bad trailing component %q", subpath, filename)
}
dirFd, err := MkdirAllInRootOpen(root, dir, 0o755)
dirFd, filename, err := MkdirAllParentInRoot(root, subpath, 0o755)
if err != nil {
return nil, err
}
@@ -64,7 +59,7 @@ func CreateInRoot(root, subpath string, flags int, fileMode uint32) (*os.File, e
// trailing symlinks, so this is safe to do. libpathrs's Root::create_file
// works the same way.
flags |= unix.O_CREAT | unix.O_NOFOLLOW
fd, err := unix.Openat(int(dirFd.Fd()), filename, flags, fileMode)
fd, err := linux.Openat(int(dirFd.Fd()), filename, flags, fileMode)
if err != nil {
return nil, err
}

View File

@@ -2,15 +2,17 @@ package apparmor
import "errors"
var (
// IsEnabled returns true if apparmor is enabled for the host.
IsEnabled = isEnabled
// IsEnabled returns true if apparmor is enabled for the host.
func IsEnabled() bool {
return isEnabled()
}
// ApplyProfile will apply the profile with the specified name to the process after
// the next exec. It is only supported on Linux and produces an ErrApparmorNotEnabled
// on other platforms.
ApplyProfile = applyProfile
// ApplyProfile will apply the profile with the specified name to the process
// after the next exec. It is only supported on Linux and produces an
// [ErrApparmorNotEnabled] on other platforms.
func ApplyProfile(name string) error {
return applyProfile(name)
}
// ErrApparmorNotEnabled indicates that AppArmor is not enabled or not supported.
ErrApparmorNotEnabled = errors.New("apparmor: config provided but apparmor not supported")
)
// ErrApparmorNotEnabled indicates that AppArmor is not enabled or not supported.
var ErrApparmorNotEnabled = errors.New("apparmor: config provided but apparmor not supported")

View File

@@ -9,7 +9,6 @@ import (
"golang.org/x/sys/unix"
"github.com/opencontainers/runc/internal/pathrs"
"github.com/opencontainers/runc/libcontainer/utils"
)
var (
@@ -29,7 +28,7 @@ func isEnabled() bool {
}
func setProcAttr(attr, value string) error {
attr = utils.CleanPath(attr)
attr = pathrs.LexicallyCleanPath(attr)
attrSubPath := "attr/apparmor/" + attr
if _, err := os.Stat("/proc/self/" + attrSubPath); errors.Is(err, os.ErrNotExist) {
// fall back to the old convention
@@ -50,7 +49,7 @@ func setProcAttr(attr, value string) error {
return err
}
// changeOnExec reimplements aa_change_onexec from libapparmor in Go
// changeOnExec reimplements aa_change_onexec from libapparmor in Go.
func changeOnExec(name string) error {
if err := setProcAttr("exec", "exec "+name); err != nil {
return fmt.Errorf("apparmor failed to apply profile: %w", err)
@@ -58,9 +57,8 @@ func changeOnExec(name string) error {
return nil
}
// applyProfile will apply the profile with the specified name to the process after
// the next exec. It is only supported on Linux and produces an error on other
// platforms.
// applyProfile will apply the profile with the specified name to the process
// after the next exec.
func applyProfile(name string) error {
if name == "" {
return nil

View File

@@ -1,135 +0,0 @@
package utils
/*
* Copyright 2016, 2017 SUSE LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import (
"fmt"
"os"
"runtime"
"golang.org/x/sys/unix"
)
// MaxNameLen is the maximum length of the name of a file descriptor being sent
// using SendFile. The name of the file handle returned by RecvFile will never be
// larger than this value.
const MaxNameLen = 4096
// oobSpace is the size of the oob slice required to store a single FD. Note
// that unix.UnixRights appears to make the assumption that fd is always int32,
// so sizeof(fd) = 4.
var oobSpace = unix.CmsgSpace(4)
// RecvFile waits for a file descriptor to be sent over the given AF_UNIX
// socket. The file name of the remote file descriptor will be recreated
// locally (it is sent as non-auxiliary data in the same payload).
func RecvFile(socket *os.File) (_ *os.File, Err error) {
name := make([]byte, MaxNameLen)
oob := make([]byte, oobSpace)
sockfd := socket.Fd()
var (
n, oobn int
err error
)
for {
n, oobn, _, _, err = unix.Recvmsg(int(sockfd), name, oob, unix.MSG_CMSG_CLOEXEC)
if err != unix.EINTR { //nolint:errorlint // unix errors are bare
break
}
}
if err != nil {
return nil, os.NewSyscallError("recvmsg", err)
}
if n >= MaxNameLen || oobn != oobSpace {
return nil, fmt.Errorf("recvfile: incorrect number of bytes read (n=%d oobn=%d)", n, oobn)
}
// Truncate.
name = name[:n]
oob = oob[:oobn]
scms, err := unix.ParseSocketControlMessage(oob)
if err != nil {
return nil, err
}
// We cannot control how many SCM_RIGHTS we receive, and upon receiving
// them all of the descriptors are installed in our fd table, so we need to
// parse all of the SCM_RIGHTS we received in order to close all of the
// descriptors on error.
var fds []int
defer func() {
for i, fd := range fds {
if i == 0 && Err == nil {
// Only close the first one on error.
continue
}
// Always close extra ones.
_ = unix.Close(fd)
}
}()
var lastErr error
for _, scm := range scms {
if scm.Header.Type == unix.SCM_RIGHTS {
scmFds, err := unix.ParseUnixRights(&scm)
if err != nil {
lastErr = err
} else {
fds = append(fds, scmFds...)
}
}
}
if lastErr != nil {
return nil, lastErr
}
// We do this after collecting the fds to make sure we close them all when
// returning an error here.
if len(scms) != 1 {
return nil, fmt.Errorf("recvfd: number of SCMs is not 1: %d", len(scms))
}
if len(fds) != 1 {
return nil, fmt.Errorf("recvfd: number of fds is not 1: %d", len(fds))
}
return os.NewFile(uintptr(fds[0]), string(name)), nil
}
// SendFile sends a file over the given AF_UNIX socket. file.Name() is also
// included so that if the other end uses RecvFile, the file will have the same
// name information.
func SendFile(socket *os.File, file *os.File) error {
name := file.Name()
if len(name) >= MaxNameLen {
return fmt.Errorf("sendfd: filename too long: %s", name)
}
err := SendRawFd(socket, name, file.Fd())
runtime.KeepAlive(file)
return err
}
// SendRawFd sends a specific file descriptor over the given AF_UNIX socket.
func SendRawFd(socket *os.File, msg string, fd uintptr) error {
oob := unix.UnixRights(int(fd))
for {
err := unix.Sendmsg(int(socket.Fd()), []byte(msg), oob, nil, 0)
if err != unix.EINTR { //nolint:errorlint // unix errors are bare
return os.NewSyscallError("sendmsg", err)
}
}
}

View File

@@ -1,115 +0,0 @@
package utils
import (
"encoding/json"
"io"
"os"
"path/filepath"
"strings"
"golang.org/x/sys/unix"
)
const (
exitSignalOffset = 128
)
// ExitStatus returns the correct exit status for a process based on if it
// was signaled or exited cleanly
func ExitStatus(status unix.WaitStatus) int {
if status.Signaled() {
return exitSignalOffset + int(status.Signal())
}
return status.ExitStatus()
}
// WriteJSON writes the provided struct v to w using standard json marshaling
// without a trailing newline. This is used instead of json.Encoder because
// there might be a problem in json decoder in some cases, see:
// https://github.com/docker/docker/issues/14203#issuecomment-174177790
func WriteJSON(w io.Writer, v interface{}) error {
data, err := json.Marshal(v)
if err != nil {
return err
}
_, err = w.Write(data)
return err
}
// CleanPath makes a path safe for use with filepath.Join. This is done by not
// only cleaning the path, but also (if the path is relative) adding a leading
// '/' and cleaning it (then removing the leading '/'). This ensures that a
// path resulting from prepending another path will always resolve to lexically
// be a subdirectory of the prefixed path. This is all done lexically, so paths
// that include symlinks won't be safe as a result of using CleanPath.
func CleanPath(path string) string {
// Deal with empty strings nicely.
if path == "" {
return ""
}
// Ensure that all paths are cleaned (especially problematic ones like
// "/../../../../../" which can cause lots of issues).
if filepath.IsAbs(path) {
return filepath.Clean(path)
}
// If the path isn't absolute, we need to do more processing to fix paths
// such as "../../../../<etc>/some/path". We also shouldn't convert absolute
// paths to relative ones.
path = filepath.Clean(string(os.PathSeparator) + path)
// This can't fail, as (by definition) all paths are relative to root.
path, _ = filepath.Rel(string(os.PathSeparator), path)
return path
}
// StripRoot returns the passed path, stripping the root path if it was
// (lexicially) inside it. Note that both passed paths will always be treated
// as absolute, and the returned path will also always be absolute. In
// addition, the paths are cleaned before stripping the root.
func StripRoot(root, path string) string {
// Make the paths clean and absolute.
root, path = CleanPath("/"+root), CleanPath("/"+path)
switch {
case path == root:
path = "/"
case root == "/":
// do nothing
default:
path = strings.TrimPrefix(path, root+"/")
}
return CleanPath("/" + path)
}
// SearchLabels searches through a list of key=value pairs for a given key,
// returning its value, and the binary flag telling whether the key exist.
func SearchLabels(labels []string, key string) (string, bool) {
key += "="
for _, s := range labels {
if val, ok := strings.CutPrefix(s, key); ok {
return val, true
}
}
return "", false
}
// Annotations returns the bundle path and user defined annotations from the
// libcontainer state. We need to remove the bundle because that is a label
// added by libcontainer.
func Annotations(labels []string) (bundle string, userAnnotations map[string]string) {
userAnnotations = make(map[string]string)
for _, l := range labels {
name, value, ok := strings.Cut(l, "=")
if !ok {
continue
}
if name == "bundle" {
bundle = value
} else {
userAnnotations[name] = value
}
}
return bundle, userAnnotations
}

View File

@@ -1,277 +0,0 @@
//go:build !windows
package utils
import (
"fmt"
"math"
"os"
"path/filepath"
"runtime"
"strconv"
"sync"
_ "unsafe" // for go:linkname
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/opencontainers/runc/internal/pathrs"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
var (
haveCloseRangeCloexecBool bool
haveCloseRangeCloexecOnce sync.Once
)
func haveCloseRangeCloexec() bool {
haveCloseRangeCloexecOnce.Do(func() {
// Make sure we're not closing a random file descriptor.
tmpFd, err := unix.FcntlInt(0, unix.F_DUPFD_CLOEXEC, 0)
if err != nil {
return
}
defer unix.Close(tmpFd)
err = unix.CloseRange(uint(tmpFd), uint(tmpFd), unix.CLOSE_RANGE_CLOEXEC)
// Any error means we cannot use close_range(CLOSE_RANGE_CLOEXEC).
// -ENOSYS and -EINVAL ultimately mean we don't have support, but any
// other potential error would imply that even the most basic close
// operation wouldn't work.
haveCloseRangeCloexecBool = err == nil
})
return haveCloseRangeCloexecBool
}
type fdFunc func(fd int)
// fdRangeFrom calls the passed fdFunc for each file descriptor that is open in
// the current process.
func fdRangeFrom(minFd int, fn fdFunc) error {
fdDir, closer, err := pathrs.ProcThreadSelfOpen("fd/", unix.O_DIRECTORY|unix.O_CLOEXEC)
if err != nil {
return fmt.Errorf("get handle to /proc/thread-self/fd: %w", err)
}
defer closer()
defer fdDir.Close()
fdList, err := fdDir.Readdirnames(-1)
if err != nil {
return err
}
for _, fdStr := range fdList {
fd, err := strconv.Atoi(fdStr)
// Ignore non-numeric file names.
if err != nil {
continue
}
// Ignore descriptors lower than our specified minimum.
if fd < minFd {
continue
}
// Ignore the file descriptor we used for readdir, as it will be closed
// when we return.
if uintptr(fd) == fdDir.Fd() {
continue
}
// Run the closure.
fn(fd)
}
return nil
}
// CloseExecFrom sets the O_CLOEXEC flag on all file descriptors greater or
// equal to minFd in the current process.
func CloseExecFrom(minFd int) error {
// Use close_range(CLOSE_RANGE_CLOEXEC) if possible.
if haveCloseRangeCloexec() {
err := unix.CloseRange(uint(minFd), math.MaxInt32, unix.CLOSE_RANGE_CLOEXEC)
if err == nil {
return nil
}
logrus.Debugf("close_range failed, closing range one at a time (error: %v)", err)
// If close_range fails, we fall back to the standard loop.
}
// Otherwise, fall back to the standard loop.
return fdRangeFrom(minFd, unix.CloseOnExec)
}
//go:linkname runtime_IsPollDescriptor internal/poll.IsPollDescriptor
// In order to make sure we do not close the internal epoll descriptors the Go
// runtime uses, we need to ensure that we skip descriptors that match
// "internal/poll".IsPollDescriptor. Yes, this is a Go runtime internal thing,
// unfortunately there's no other way to be sure we're only keeping the file
// descriptors the Go runtime needs. Hopefully nothing blows up doing this...
func runtime_IsPollDescriptor(fd uintptr) bool //nolint:revive
// UnsafeCloseFrom closes all file descriptors greater or equal to minFd in the
// current process, except for those critical to Go's runtime (such as the
// netpoll management descriptors).
//
// NOTE: That this function is incredibly dangerous to use in most Go code, as
// closing file descriptors from underneath *os.File handles can lead to very
// bad behaviour (the closed file descriptor can be re-used and then any
// *os.File operations would apply to the wrong file). This function is only
// intended to be called from the last stage of runc init.
func UnsafeCloseFrom(minFd int) error {
// We cannot use close_range(2) even if it is available, because we must
// not close some file descriptors.
return fdRangeFrom(minFd, func(fd int) {
if runtime_IsPollDescriptor(uintptr(fd)) {
// These are the Go runtimes internal netpoll file descriptors.
// These file descriptors are operated on deep in the Go scheduler,
// and closing those files from underneath Go can result in panics.
// There is no issue with keeping them because they are not
// executable and are not useful to an attacker anyway. Also we
// don't have any choice.
return
}
// There's nothing we can do about errors from close(2), and the
// only likely error to be seen is EBADF which indicates the fd was
// already closed (in which case, we got what we wanted).
_ = unix.Close(fd)
})
}
// NewSockPair returns a new SOCK_STREAM unix socket pair.
func NewSockPair(name string) (parent, child *os.File, err error) {
fds, err := unix.Socketpair(unix.AF_LOCAL, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
if err != nil {
return nil, nil, err
}
return os.NewFile(uintptr(fds[1]), name+"-p"), os.NewFile(uintptr(fds[0]), name+"-c"), nil
}
// WithProcfd runs the passed closure with a procfd path (/proc/self/fd/...)
// corresponding to the unsafePath resolved within the root. Before passing the
// fd, this path is verified to have been inside the root -- so operating on it
// through the passed fdpath should be safe. Do not access this path through
// the original path strings, and do not attempt to use the pathname outside of
// the passed closure (the file handle will be freed once the closure returns).
func WithProcfd(root, unsafePath string, fn func(procfd string) error) error {
// Remove the root then forcefully resolve inside the root.
unsafePath = StripRoot(root, unsafePath)
fullPath, err := securejoin.SecureJoin(root, unsafePath)
if err != nil {
return fmt.Errorf("resolving path inside rootfs failed: %w", err)
}
procSelfFd, closer := ProcThreadSelf("fd/")
defer closer()
// Open the target path.
fh, err := os.OpenFile(fullPath, unix.O_PATH|unix.O_CLOEXEC, 0)
if err != nil {
return fmt.Errorf("open o_path procfd: %w", err)
}
defer fh.Close()
procfd := filepath.Join(procSelfFd, strconv.Itoa(int(fh.Fd())))
// Double-check the path is the one we expected.
if realpath, err := os.Readlink(procfd); err != nil {
return fmt.Errorf("procfd verification failed: %w", err)
} else if realpath != fullPath {
return fmt.Errorf("possibly malicious path detected -- refusing to operate on %s", realpath)
}
return fn(procfd)
}
// WithProcfdFile is a very minimal wrapper around [ProcThreadSelfFd], intended
// to make migrating from [WithProcfd] and [WithProcfdPath] usage easier. The
// caller is responsible for making sure that the provided file handle is
// actually safe to operate on.
func WithProcfdFile(file *os.File, fn func(procfd string) error) error {
fdpath, closer := ProcThreadSelfFd(file.Fd())
defer closer()
return fn(fdpath)
}
type ProcThreadSelfCloser func()
var (
haveProcThreadSelf bool
haveProcThreadSelfOnce sync.Once
)
// ProcThreadSelf returns a string that is equivalent to
// /proc/thread-self/<subpath>, with a graceful fallback on older kernels where
// /proc/thread-self doesn't exist. This method DOES NOT use SecureJoin,
// meaning that the passed string needs to be trusted. The caller _must_ call
// the returned procThreadSelfCloser function (which is runtime.UnlockOSThread)
// *only once* after it has finished using the returned path string.
func ProcThreadSelf(subpath string) (string, ProcThreadSelfCloser) {
haveProcThreadSelfOnce.Do(func() {
if _, err := os.Stat("/proc/thread-self/"); err == nil {
haveProcThreadSelf = true
} else {
logrus.Debugf("cannot stat /proc/thread-self (%v), falling back to /proc/self/task/<tid>", err)
}
})
// We need to lock our thread until the caller is done with the path string
// because any non-atomic operation on the path (such as opening a file,
// then reading it) could be interrupted by the Go runtime where the
// underlying thread is swapped out and the original thread is killed,
// resulting in pull-your-hair-out-hard-to-debug issues in the caller. In
// addition, the pre-3.17 fallback makes everything non-atomic because the
// same thing could happen between unix.Gettid() and the path operations.
//
// In theory, we don't need to lock in the atomic user case when using
// /proc/thread-self/, but it's better to be safe than sorry (and there are
// only one or two truly atomic users of /proc/thread-self/).
runtime.LockOSThread()
threadSelf := "/proc/thread-self/"
if !haveProcThreadSelf {
// Pre-3.17 kernels did not have /proc/thread-self, so do it manually.
threadSelf = "/proc/self/task/" + strconv.Itoa(unix.Gettid()) + "/"
if _, err := os.Stat(threadSelf); err != nil {
// Unfortunately, this code is called from rootfs_linux.go where we
// are running inside the pid namespace of the container but /proc
// is the host's procfs. Unfortunately there is no real way to get
// the correct tid to use here (the kernel age means we cannot do
// things like set up a private fsopen("proc") -- even scanning
// NSpid in all of the tasks in /proc/self/task/*/status requires
// Linux 4.1).
//
// So, we just have to assume that /proc/self is acceptable in this
// one specific case.
if os.Getpid() == 1 {
logrus.Debugf("/proc/thread-self (tid=%d) cannot be emulated inside the initial container setup -- using /proc/self instead: %v", unix.Gettid(), err)
} else {
// This should never happen, but the fallback should work in most cases...
logrus.Warnf("/proc/thread-self could not be emulated for pid=%d (tid=%d) -- using more buggy /proc/self fallback instead: %v", os.Getpid(), unix.Gettid(), err)
}
threadSelf = "/proc/self/"
}
}
return threadSelf + subpath, runtime.UnlockOSThread
}
// ProcThreadSelfFd is small wrapper around ProcThreadSelf to make it easier to
// create a /proc/thread-self handle for given file descriptor.
//
// It is basically equivalent to ProcThreadSelf(fmt.Sprintf("fd/%d", fd)), but
// without using fmt.Sprintf to avoid unneeded overhead.
func ProcThreadSelfFd(fd uintptr) (string, ProcThreadSelfCloser) {
return ProcThreadSelf("fd/" + strconv.FormatUint(uint64(fd), 10))
}
// Openat is a Go-friendly openat(2) wrapper.
func Openat(dir *os.File, path string, flags int, mode uint32) (*os.File, error) {
dirFd := unix.AT_FDCWD
if dir != nil {
dirFd = int(dir.Fd())
}
flags |= unix.O_CLOEXEC
fd, err := unix.Openat(dirFd, path, flags, mode)
if err != nil {
return nil, &os.PathError{Op: "openat", Path: path, Err: err}
}
return os.NewFile(uintptr(fd), dir.Name()+"/"+path), nil
}

View File

@@ -20,8 +20,6 @@ import (
"go.podman.io/storage/pkg/fileutils"
)
const version = "Directory Transport Version: 1.1\n"
// ErrNotContainerImageDir indicates that the directory doesn't match the expected contents of a directory created
// using the 'dir' transport
var ErrNotContainerImageDir = errors.New("not a containers image directory, don't want to overwrite important data")
@@ -33,7 +31,8 @@ type dirImageDestination struct {
stubs.NoPutBlobPartialInitialize
stubs.AlwaysSupportsSignatures
ref dirReference
ref dirReference
usesNonSHA256Digest bool
}
// newImageDestination returns an ImageDestination for writing to a directory.
@@ -76,9 +75,14 @@ func newImageDestination(sys *types.SystemContext, ref dirReference) (private.Im
return nil, err
}
// check if contents of version file is what we expect it to be
if string(contents) != version {
versionStr := string(contents)
parsedVersion, err := parseVersion(versionStr)
if err != nil {
return nil, ErrNotContainerImageDir
}
if parsedVersion.isGreaterThan(maxSupportedVersion) {
return nil, UnsupportedVersionError{Version: versionStr, Path: ref.resolvedPath}
}
} else {
return nil, ErrNotContainerImageDir
}
@@ -94,11 +98,6 @@ func newImageDestination(sys *types.SystemContext, ref dirReference) (private.Im
return nil, fmt.Errorf("unable to create directory %q: %w", ref.resolvedPath, err)
}
}
// create version file
err = os.WriteFile(ref.versionPath(), []byte(version), 0o644)
if err != nil {
return nil, fmt.Errorf("creating version file %q: %w", ref.versionPath(), err)
}
d := &dirImageDestination{
PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{
@@ -151,13 +150,17 @@ func (d *dirImageDestination) PutBlobWithOptions(ctx context.Context, stream io.
}
}()
digester, stream := putblobdigest.DigestIfCanonicalUnknown(stream, inputInfo)
digester, stream := putblobdigest.DigestIfUnknown(stream, inputInfo)
// TODO: This can take quite some time, and should ideally be cancellable using ctx.Done().
size, err := io.Copy(blobFile, stream)
if err != nil {
return private.UploadedBlob{}, err
}
blobDigest := digester.Digest()
if blobDigest.Algorithm() != digest.Canonical { // compare the special case in layerPath
d.usesNonSHA256Digest = true
}
if inputInfo.Size != -1 && size != inputInfo.Size {
return private.UploadedBlob{}, fmt.Errorf("Size mismatch when copying %s, expected %d, got %d", blobDigest, inputInfo.Size, size)
}
@@ -257,6 +260,14 @@ func (d *dirImageDestination) PutSignaturesWithFormat(ctx context.Context, signa
// - Uploaded data MAY be visible to others before CommitWithOptions() is called
// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed)
func (d *dirImageDestination) CommitWithOptions(ctx context.Context, options private.CommitOptions) error {
versionToWrite := version1_1
if d.usesNonSHA256Digest {
versionToWrite = version1_2
}
err := os.WriteFile(d.ref.versionPath(), []byte(versionToWrite.String()), 0o644)
if err != nil {
return fmt.Errorf("writing version file %q: %w", d.ref.versionPath(), err)
}
return nil
}

View File

@@ -26,7 +26,24 @@ type dirImageSource struct {
// newImageSource returns an ImageSource reading from an existing directory.
// The caller must call .Close() on the returned ImageSource.
func newImageSource(ref dirReference) private.ImageSource {
func newImageSource(ref dirReference) (private.ImageSource, error) {
versionPath := ref.versionPath()
contents, err := os.ReadFile(versionPath)
if err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("reading version file %q: %w", versionPath, err)
}
} else {
versionStr := string(contents)
parsedVersion, err := parseVersion(versionStr)
if err != nil {
return nil, fmt.Errorf("invalid version file content: %q", versionStr)
}
if parsedVersion.isGreaterThan(maxSupportedVersion) {
return nil, UnsupportedVersionError{Version: versionStr, Path: ref.resolvedPath}
}
}
s := &dirImageSource{
PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{
HasThreadSafeGetBlob: false,
@@ -36,7 +53,7 @@ func newImageSource(ref dirReference) private.ImageSource {
ref: ref,
}
s.Compat = impl.AddCompat(s)
return s
return s, nil
}
// Reference returns the reference used to set up this source, _as specified by the user_

View File

@@ -146,7 +146,7 @@ func (ref dirReference) NewImage(ctx context.Context, sys *types.SystemContext)
// NewImageSource returns a types.ImageSource for this reference.
// The caller must call .Close() on the returned ImageSource.
func (ref dirReference) NewImageSource(ctx context.Context, sys *types.SystemContext) (types.ImageSource, error) {
return newImageSource(ref), nil
return newImageSource(ref)
}
// NewImageDestination returns a types.ImageDestination for this reference.
@@ -172,12 +172,19 @@ func (ref dirReference) manifestPath(instanceDigest *digest.Digest) (string, err
}
// layerPath returns a path for a layer tarball within a directory using our conventions.
func (ref dirReference) layerPath(digest digest.Digest) (string, error) {
if err := digest.Validate(); err != nil { // digest.Digest.Encoded() panics on failure, and could possibly result in a path with ../, so validate explicitly.
func (ref dirReference) layerPath(d digest.Digest) (string, error) {
if err := d.Validate(); err != nil { // digest.Digest.Encoded() panics on failure, and could possibly result in a path with ../, so validate explicitly.
return "", err
}
// FIXME: Should we keep the digest identification?
return filepath.Join(ref.path, digest.Encoded()), nil
var filename string
if d.Algorithm() == digest.Canonical {
filename = d.Encoded()
} else {
filename = d.Algorithm().String() + "-" + d.Encoded()
}
return filepath.Join(ref.path, filename), nil
}
// signaturePath returns a path for a signature within a directory using our conventions.

62
vendor/go.podman.io/image/v5/directory/version.go generated vendored Normal file
View File

@@ -0,0 +1,62 @@
package directory
import (
"fmt"
)
const (
versionPrefix = "Directory Transport Version: "
)
// version represents a parsed directory transport version
type version struct {
major int
minor int
}
// Supported versions
// Write version file based on digest algorithm used.
// 1.1 for sha256-only images, 1.2 otherwise.
var (
version1_1 = version{major: 1, minor: 1}
version1_2 = version{major: 1, minor: 2}
maxSupportedVersion = version1_2
)
// String formats a version as a string suitable for writing to the version file
func (v version) String() string {
return fmt.Sprintf("%s%d.%d\n", versionPrefix, v.major, v.minor)
}
// parseVersion parses a version string into major and minor components.
// Returns an error if the format is invalid.
func parseVersion(versionStr string) (version, error) {
var v version
expectedFormat := versionPrefix + "%d.%d\n"
// Sscanf parsing is a bit loose (treats spaces specially), but a strict check immediately follows
n, err := fmt.Sscanf(versionStr, expectedFormat, &v.major, &v.minor)
if err != nil || n != 2 || versionStr != v.String() {
return version{}, fmt.Errorf("invalid version format")
}
return v, nil
}
// TODO: Potential refactor for better interoperability with `cmp`
// https://github.com/containers/container-libs/pull/475#discussion_r2571131267
// isGreaterThan returns true if v is greater than other
func (v version) isGreaterThan(other version) bool {
if v.major != other.major {
return v.major > other.major
}
return v.minor > other.minor
}
// UnsupportedVersionError indicates that the directory uses a version newer than we support
type UnsupportedVersionError struct {
Version string // The unsupported version string found
Path string // The path to the directory
}
func (e UnsupportedVersionError) Error() string {
return fmt.Sprintf("unsupported directory transport version %q at %s", e.Version, e.Path)
}

View File

@@ -19,7 +19,6 @@ func newDockerClient(sys *types.SystemContext) (*dockerclient.Client, error) {
opts := []dockerclient.Opt{
dockerclient.WithHost(host),
dockerclient.WithAPIVersionNegotiation(),
}
// We conditionalize building the TLS configuration only to TLS sockets:

View File

@@ -47,7 +47,6 @@ type CreateOpts struct {
MountLabel string
StorageOpt map[string]string
*idtools.IDMappings
ignoreChownErrors bool
}
// MountOpts contains optional arguments for Driver.Get() methods.
@@ -184,7 +183,7 @@ type DiffDriver interface {
// layer with the specified id and parent, returning the size of the
// new layer in bytes.
// The io.Reader must be an uncompressed stream.
ApplyDiff(id string, parent string, options ApplyDiffOpts) (size int64, err error)
ApplyDiff(id string, options ApplyDiffOpts) (size int64, err error)
// DiffSize calculates the changes between the specified id
// and its parent and returns the size in bytes of the changes
// relative to its base filesystem directory.
@@ -299,6 +298,19 @@ type DriverWithDiffer interface {
DifferTarget(id string) (string, error)
}
// ApplyDiffStaging is an interface for driver who can apply the diff without holding the main storage lock.
// This API is experimental and can be changed without bumping the major version number.
type ApplyDiffStaging interface {
// StartStagingDiffToApply applies the new layer into a temporary directory.
// It returns a CleanupTempDirFunc which can be nil or set regardless if the function return an error or not.
// StagedAddition is only set when there is no error returned and the int64 value returns the size of the layer.
// This can be done without holding the storage lock, if a parent is given the caller must check for existence
// beforehand while holding a lock.
StartStagingDiffToApply(parent string, options ApplyDiffOpts) (tempdir.CleanupTempDirFunc, *tempdir.StagedAddition, int64, error)
// CommitStagedLayer commits the staged layer from StartStagingDiffToApply(). This must be done while holding the storage lock.
CommitStagedLayer(id string, commit *tempdir.StagedAddition) error
}
// Capabilities defines a list of capabilities a driver may implement.
// These capabilities are not required; however, they do determine how a
// graphdriver can be used.

View File

@@ -151,7 +151,7 @@ func (gdw *NaiveDiffDriver) Changes(id string, idMappings *idtools.IDMappings, p
// ApplyDiff extracts the changeset from the given diff into the
// layer with the specified id and parent, returning the size of the
// new layer in bytes.
func (gdw *NaiveDiffDriver) ApplyDiff(id, parent string, options ApplyDiffOpts) (int64, error) {
func (gdw *NaiveDiffDriver) ApplyDiff(id string, options ApplyDiffOpts) (int64, error) {
driver := gdw.ProtoDriver
if options.Mappings == nil {

View File

@@ -995,6 +995,49 @@ func (d *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) (retErr
return d.create(id, parent, opts, true)
}
// getLayerPermissions returns the base permissions to use for the layer directories.
// The first return value is the idPair to create the possible parent directories with.
// The second return value is the mode how it should be stored on disk.
// The third return value is the mode the layer expects to have which may be stored
// in an xattr when using forceMask, without forceMask both values are the same.
func (d *Driver) getLayerPermissions(parent string, uidMaps, gidMaps []idtools.IDMap) (idtools.IDPair, idtools.Stat, idtools.Stat, error) {
rootUID, rootGID, err := idtools.GetRootUIDGID(uidMaps, gidMaps)
if err != nil {
return idtools.IDPair{}, idtools.Stat{}, idtools.Stat{}, err
}
idPair := idtools.IDPair{
UID: rootUID,
GID: rootGID,
}
st := idtools.Stat{IDs: idPair, Mode: defaultPerms}
if parent != "" {
parentBase := d.dir(parent)
parentDiff := filepath.Join(parentBase, "diff")
if xSt, err := idtools.GetContainersOverrideXattr(parentDiff); err == nil {
st = xSt
} else {
systemSt, err := system.Stat(parentDiff)
if err != nil {
return idtools.IDPair{}, idtools.Stat{}, idtools.Stat{}, err
}
st.IDs.UID = int(systemSt.UID())
st.IDs.GID = int(systemSt.GID())
st.Mode = os.FileMode(systemSt.Mode())
}
}
forcedSt := st
if d.options.forceMask != nil {
forcedSt.IDs = idPair
forcedSt.Mode = *d.options.forceMask
}
return idPair, forcedSt, st, nil
}
func (d *Driver) create(id, parent string, opts *graphdriver.CreateOpts, readOnly bool) (retErr error) {
dir, homedir, _ := d.dir2(id, readOnly)
@@ -1013,38 +1056,15 @@ func (d *Driver) create(id, parent string, opts *graphdriver.CreateOpts, readOnl
return err
}
rootUID, rootGID, err := idtools.GetRootUIDGID(uidMaps, gidMaps)
idPair, forcedSt, st, err := d.getLayerPermissions(parent, uidMaps, gidMaps)
if err != nil {
return err
}
idPair := idtools.IDPair{
UID: rootUID,
GID: rootGID,
}
if err := idtools.MkdirAllAndChownNew(path.Dir(dir), 0o755, idPair); err != nil {
return err
}
st := idtools.Stat{IDs: idPair, Mode: defaultPerms}
if parent != "" {
parentBase := d.dir(parent)
parentDiff := filepath.Join(parentBase, "diff")
if xSt, err := idtools.GetContainersOverrideXattr(parentDiff); err == nil {
st = xSt
} else {
systemSt, err := system.Stat(parentDiff)
if err != nil {
return err
}
st.IDs.UID = int(systemSt.UID())
st.IDs.GID = int(systemSt.GID())
st.Mode = os.FileMode(systemSt.Mode())
}
}
if err := fileutils.Lexists(dir); err == nil {
logrus.Warnf("Trying to create a layer %#v while directory %q already exists; removing it first", id, dir)
// Dont just os.RemoveAll(dir) here; d.Remove also removes the link in linkDir,
@@ -1088,12 +1108,6 @@ func (d *Driver) create(id, parent string, opts *graphdriver.CreateOpts, readOnl
}
}
forcedSt := st
if d.options.forceMask != nil {
forcedSt.IDs = idPair
forcedSt.Mode = *d.options.forceMask
}
diff := path.Join(dir, "diff")
if err := idtools.MkdirAs(diff, forcedSt.Mode, forcedSt.IDs.UID, forcedSt.IDs.GID); err != nil {
return err
@@ -1356,6 +1370,14 @@ func (d *Driver) getTempDirRoot(id string) string {
return filepath.Join(d.home, tempDirName)
}
// getTempDirRootForNewLayer returns the correct temp directory root based on where
// the layer should be created.
//
// This must be kept in sync with GetTempDirRootDirs().
func (d *Driver) getTempDirRootForNewLayer() string {
return filepath.Join(d.homeDirForImageStore(), tempDirName)
}
func (d *Driver) DeferredRemove(id string) (tempdir.CleanupTempDirFunc, error) {
tempDirRoot := d.getTempDirRoot(id)
t, err := tempdir.NewTempDir(tempDirRoot)
@@ -2369,31 +2391,94 @@ func (d *Driver) DifferTarget(id string) (string, error) {
return d.getDiffPath(id)
}
// ApplyDiff applies the new layer into a root
func (d *Driver) ApplyDiff(id, parent string, options graphdriver.ApplyDiffOpts) (size int64, err error) {
if !d.isParent(id, parent) {
if d.options.ignoreChownErrors {
options.IgnoreChownErrors = d.options.ignoreChownErrors
}
if d.options.forceMask != nil {
options.ForceMask = d.options.forceMask
}
return d.naiveDiff.ApplyDiff(id, parent, options)
// StartStagingDiffToApply applies the new layer into a temporary directory.
// It returns a CleanupTempDirFunc which can be nil or set regardless if the function return an error or not.
// StagedAddition is only set when there is no error returned and the int64 value returns the size of the layer.
// This can be done without holding the storage lock, if a parent is given the caller must check for existence
// beforehand while holding a lock.
//
// This API is experimental and can be changed without bumping the major version number.
func (d *Driver) StartStagingDiffToApply(parent string, options graphdriver.ApplyDiffOpts) (tempdir.CleanupTempDirFunc, *tempdir.StagedAddition, int64, error) {
tempDirRoot := d.getTempDirRootForNewLayer()
t, err := tempdir.NewTempDir(tempDirRoot)
if err != nil {
return nil, nil, -1, err
}
sa, err := t.StageAddition()
if err != nil {
return t.Cleanup, nil, -1, err
}
_, forcedSt, st, err := d.getLayerPermissions(parent, options.Mappings.UIDs(), options.Mappings.GIDs())
if err != nil {
// If we have a ENOENT it means the parent was removed which can happen as we are unlocked here.
// In this case also wrap ErrLayerUnknown which some callers can handle to retry after recreating the parent.
if errors.Is(err, fs.ErrNotExist) {
err = fmt.Errorf("parent layer %q: %w: %w", parent, graphdriver.ErrLayerUnknown, err)
}
return t.Cleanup, nil, -1, err
}
if err := idtools.MkdirAs(sa.Path, forcedSt.Mode, forcedSt.IDs.UID, forcedSt.IDs.GID); err != nil {
return t.Cleanup, nil, -1, err
}
if d.options.forceMask != nil {
st.Mode |= os.ModeDir
if err := idtools.SetContainersOverrideXattr(sa.Path, st); err != nil {
return t.Cleanup, nil, -1, err
}
}
size, err := d.applyDiff(sa.Path, options)
if err != nil {
return t.Cleanup, nil, -1, err
}
return t.Cleanup, sa, size, nil
}
// CommitStagedLayer that was created with StartStagingDiffToApply().
//
// This API is experimental and can be changed without bumping the major version number.
func (d *Driver) CommitStagedLayer(id string, sa *tempdir.StagedAddition) error {
applyDir, err := d.getDiffPath(id)
if err != nil {
return err
}
// The os.Rename() function used by CommitFunc errors when the target directory already
// exists, as such delete the dir. The create() function creates it and it would be more
// complicated to code in a way that it didn't create it.
if err := os.Remove(applyDir); err != nil {
return err
}
return sa.Commit(applyDir)
}
// ApplyDiff applies the new layer into a root
func (d *Driver) ApplyDiff(id string, options graphdriver.ApplyDiffOpts) (size int64, err error) {
applyDir, err := d.getDiffPath(id)
if err != nil {
return 0, err
}
return d.applyDiff(applyDir, options)
}
// ApplyDiff applies the new layer into a root.
// This can run concurrently with any other driver operations, as such it is the
// callers responsibility to ensure the target path passed is safe to use if that is the case.
func (d *Driver) applyDiff(target string, options graphdriver.ApplyDiffOpts) (size int64, err error) {
idMappings := options.Mappings
if idMappings == nil {
idMappings = &idtools.IDMappings{}
}
applyDir, err := d.getDiffPath(id)
if err != nil {
return 0, err
}
logrus.Debugf("Applying tar in %s", applyDir)
logrus.Debugf("Applying tar in %s", target)
// Overlay doesn't need the parent id to apply the diff
if err := untar(options.Diff, applyDir, &archive.TarOptions{
if err := untar(options.Diff, target, &archive.TarOptions{
UIDMaps: idMappings.UIDs(),
GIDMaps: idMappings.GIDs(),
IgnoreChownErrors: d.options.ignoreChownErrors,
@@ -2404,7 +2489,7 @@ func (d *Driver) ApplyDiff(id, parent string, options graphdriver.ApplyDiffOpts)
return 0, err
}
return directory.Size(applyDir)
return directory.Size(target)
}
func (d *Driver) getComposefsData(id string) string {

View File

@@ -1,52 +0,0 @@
package graphdriver
import (
"github.com/sirupsen/logrus"
"go.podman.io/storage/pkg/idtools"
)
// TemplateDriver is just barely enough of a driver that we can implement a
// naive version of CreateFromTemplate on top of it.
type TemplateDriver interface {
DiffDriver
CreateReadWrite(id, parent string, opts *CreateOpts) error
Create(id, parent string, opts *CreateOpts) error
Remove(id string) error
}
// CreateFromTemplate creates a layer with the same contents and parent as
// another layer. Internally, it may even depend on that other layer
// continuing to exist, as if it were actually a child of the child layer.
func NaiveCreateFromTemplate(d TemplateDriver, id, template string, templateIDMappings *idtools.IDMappings, parent string, parentIDMappings *idtools.IDMappings, opts *CreateOpts, readWrite bool) error {
var err error
if readWrite {
err = d.CreateReadWrite(id, parent, opts)
} else {
err = d.Create(id, parent, opts)
}
if err != nil {
return err
}
diff, err := d.Diff(template, templateIDMappings, parent, parentIDMappings, opts.MountLabel)
if err != nil {
if err2 := d.Remove(id); err2 != nil {
logrus.Errorf("Removing layer %q: %v", id, err2)
}
return err
}
defer diff.Close()
applyOptions := ApplyDiffOpts{
Diff: diff,
Mappings: templateIDMappings,
MountLabel: opts.MountLabel,
IgnoreChownErrors: opts.ignoreChownErrors,
}
if _, err = d.ApplyDiff(id, parent, applyOptions); err != nil {
if err2 := d.Remove(id); err2 != nil {
logrus.Errorf("Removing layer %q: %v", id, err2)
}
return err
}
return nil
}

View File

@@ -132,11 +132,11 @@ func (d *Driver) CreateFromTemplate(id, template string, templateIDMappings *idt
}
// ApplyDiff applies the new layer into a root
func (d *Driver) ApplyDiff(id, parent string, options graphdriver.ApplyDiffOpts) (size int64, err error) {
func (d *Driver) ApplyDiff(id string, options graphdriver.ApplyDiffOpts) (size int64, err error) {
if d.ignoreChownErrors {
options.IgnoreChownErrors = d.ignoreChownErrors
}
return d.naiveDiff.ApplyDiff(id, parent, options)
return d.naiveDiff.ApplyDiff(id, options)
}
// CreateReadWrite creates a layer that is writable for use as a container

View File

@@ -6,6 +6,7 @@ import (
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/sirupsen/logrus"
@@ -91,6 +92,26 @@ type TempDir struct {
counter uint64
}
// StagedAddition is a temporary object which holds the information of where to
// put the data into and then use Commit() to move the data into the final location.
type StagedAddition struct {
// Path is the temporary path. The path is not created so caller must create
// a file or directory on it in order to use Commit(). The path is only valid
// until Commit() is called or until the TempDir instance Cleanup() method is used.
Path string
}
// Commit the staged content into its final destination by using os.Rename().
// That means the dest must be on the same on the same fs as the root directory
// that was given to NewTempDir() and the dest must not exist yet.
// Commit must only be called once per instance returned from the
// StagedAddition() call.
func (s *StagedAddition) Commit(destination string) error {
err := os.Rename(s.Path, destination)
s.Path = "" // invalidate Path to avoid reuse
return err
}
// CleanupTempDirFunc is a function type that can be returned by operations
// which need to perform cleanup actions later.
type CleanupTempDirFunc func() error
@@ -190,6 +211,23 @@ func NewTempDir(rootDir string) (*TempDir, error) {
return td, nil
}
// StageAddition creates a new temporary path that is returned as field in the StagedAddition
// struct. The returned type StagedAddition has a Commit() function to move the content from
// the temporary location to the final one.
//
// The caller MUST call Commit() before Cleanup() is called on the TempDir, otherwise the
// staged content will be deleted and the Commit() will fail.
// If the TempDir has been cleaned up already, this method will return an error.
func (td *TempDir) StageAddition() (*StagedAddition, error) {
if td.tempDirLock == nil {
return nil, fmt.Errorf("temp dir instance not initialized or already cleaned up")
}
fileName := strconv.FormatUint(td.counter, 10) + "-addition"
tmpAddPath := filepath.Join(td.tempDirPath, fileName)
td.counter++
return &StagedAddition{Path: tmpAddPath}, nil
}
// StageDeletion moves the specified file into the instance's temporary directory.
// The temporary directory must already exist (created during NewTempDir).
// Files are renamed with a counter-based prefix (e.g., "0-filename", "1-filename") to ensure uniqueness.

408
vendor/go.podman.io/storage/layers.go generated vendored
View File

@@ -31,6 +31,7 @@ import (
"go.podman.io/storage/pkg/ioutils"
"go.podman.io/storage/pkg/lockfile"
"go.podman.io/storage/pkg/mount"
"go.podman.io/storage/pkg/pools"
"go.podman.io/storage/pkg/stringid"
"go.podman.io/storage/pkg/system"
"go.podman.io/storage/pkg/tarlog"
@@ -195,11 +196,53 @@ type DiffOptions struct {
Compression *archive.Compression
}
// stagedLayerOptions are the options passed to .create to populate a staged
// layerCreationContents are the options passed to .create to populate a staged
// layer
type stagedLayerOptions struct {
type layerCreationContents struct {
// These are used via the zstd:chunked pull paths
DiffOutput *drivers.DriverWithDifferOutput
DiffOptions *drivers.ApplyDiffWithDifferOpts
// stagedLayerExtraction is used by the normal tar layer extraction.
stagedLayerExtraction *maybeStagedLayerExtraction
}
// maybeStagedLayerExtraction is a helper to encapsulate details around extracting
// a layer potentially before we even take a look if the driver implements the
// ApplyDiffStaging interface.
// This should be initialized with layerStore.newMaybeStagedLayerExtraction()
type maybeStagedLayerExtraction struct {
// diff contains the tar archive, can be compressed, must be non nil, but can be at EOF when the content was already staged
diff io.Reader
// staging interface of the storage driver, set when the driver supports staging and nil otherwise
staging drivers.ApplyDiffStaging
// result is a placeholder for the applyDiff() result so we can pass that down the stack easily.
// If result is not nil the layer was staged successfully, if this is set stagedTarSplit and
// stagedLayer must be set as well.
result *applyDiffResult
// stagedTarSplit is the temp file where we staged the tar split file
stagedTarSplit *tempdir.StagedAddition
// stagedLayer is the temp directory where we staged the extracted layer content
stagedLayer *tempdir.StagedAddition
// cleanupFuncs contains the set of tempdir cleanup function that get executed in cleanup()
cleanupFuncs []tempdir.CleanupTempDirFunc
}
type applyDiffResult struct {
compressedDigest digest.Digest
compressedSize int64
compressionType archive.Compression
uncompressedDigest digest.Digest
uncompressedSize int64
// size of the data, including the full size of sparse files, and excluding all metadata
// It is neither compressedSize nor uncompressedSize.
// The use case for this seems unclear, it gets returned in PutLayer() but in the Podman
// stack at least that value is never used so maybe we can look into removing this.
size int64
uids []uint32
gids []uint32
}
// roLayerStore wraps a graph driver, adding the ability to refer to layers by
@@ -216,6 +259,11 @@ type roLayerStore interface {
// stopReading releases locks obtained by startReading.
stopReading()
// checkIdOrNameConflict checks if the id or names are already in use and returns an
// error in that case. As Special case if the layer already exists it returns it as
// well together with the error.
checkIdOrNameConflict(id string, names []string) (*Layer, error)
// Exists checks if a layer with the specified name or ID is known.
Exists(id string) bool
@@ -288,7 +336,7 @@ type rwLayerStore interface {
// underlying drivers do not themselves distinguish between writeable
// and read-only layers. Returns the new layer structure and the size of the
// diff which was applied to its parent to initialize its contents.
create(id string, parent *Layer, names []string, mountLabel string, options map[string]string, moreOptions *LayerOptions, writeable bool, diff io.Reader, slo *stagedLayerOptions) (*Layer, int64, error)
create(id string, parent *Layer, names []string, mountLabel string, options map[string]string, moreOptions *LayerOptions, writeable bool, contents *layerCreationContents) (*Layer, int64, error)
// updateNames modifies names associated with a layer based on (op, names).
updateNames(id string, names []string, op updateNameOperation) error
@@ -354,6 +402,14 @@ type rwLayerStore interface {
// Dedup deduplicates layers in the store.
dedup(drivers.DedupArgs) (drivers.DedupResult, error)
// newMaybeStagedLayerExtraction initializes a new maybeStagedLayerExtraction. The caller
// must call maybeStagedLayerExtraction.cleanup() to remove any temporary files.
newMaybeStagedLayerExtraction(diff io.Reader) *maybeStagedLayerExtraction
// stageWithUnlockedStore stages the layer content without needing the store locked.
// If the driver does not support stage addition then this is a NOP and does nothing.
stageWithUnlockedStore(m *maybeStagedLayerExtraction, parent string, options *LayerOptions) error
}
type multipleLockFile struct {
@@ -1307,13 +1363,8 @@ func (r *layerStore) Status() ([][2]string, error) {
// Requires startWriting.
func (r *layerStore) PutAdditionalLayer(id string, parentLayer *Layer, names []string, aLayer drivers.AdditionalLayer) (layer *Layer, err error) {
if duplicateLayer, idInUse := r.byid[id]; idInUse {
return duplicateLayer, ErrDuplicateID
}
for _, name := range names {
if _, nameInUse := r.byname[name]; nameInUse {
return nil, ErrDuplicateName
}
if layer, err := r.checkIdOrNameConflict(id, names); err != nil {
return layer, err
}
parent := ""
@@ -1378,8 +1429,25 @@ func (r *layerStore) pickStoreLocation(volatile, writeable bool) layerLocations
}
}
// checkIdOrNameConflict checks if the id or names are already in use and returns an
// error in that case. As Special case if the layer already exists it returns it as
// well together with the error.
//
// Requires startReading or startWriting.
func (r *layerStore) checkIdOrNameConflict(id string, names []string) (*Layer, error) {
if duplicateLayer, idInUse := r.byid[id]; idInUse {
return duplicateLayer, ErrDuplicateID
}
for _, name := range names {
if _, nameInUse := r.byname[name]; nameInUse {
return nil, ErrDuplicateName
}
}
return nil, nil
}
// Requires startWriting.
func (r *layerStore) create(id string, parentLayer *Layer, names []string, mountLabel string, options map[string]string, moreOptions *LayerOptions, writeable bool, diff io.Reader, slo *stagedLayerOptions) (layer *Layer, size int64, err error) {
func (r *layerStore) create(id string, parentLayer *Layer, names []string, mountLabel string, options map[string]string, moreOptions *LayerOptions, writeable bool, contents *layerCreationContents) (layer *Layer, size int64, err error) {
if moreOptions == nil {
moreOptions = &LayerOptions{}
}
@@ -1400,14 +1468,8 @@ func (r *layerStore) create(id string, parentLayer *Layer, names []string, mount
_, idInUse = r.byid[id]
}
}
if duplicateLayer, idInUse := r.byid[id]; idInUse {
return duplicateLayer, -1, ErrDuplicateID
}
names = dedupeStrings(names)
for _, name := range names {
if _, nameInUse := r.byname[name]; nameInUse {
return nil, -1, ErrDuplicateName
}
if layer, err := r.checkIdOrNameConflict(id, names); err != nil {
return layer, -1, err
}
parent := ""
if parentLayer != nil {
@@ -1568,18 +1630,31 @@ func (r *layerStore) create(id string, parentLayer *Layer, names []string, mount
}
size = -1
if diff != nil {
if size, err = r.applyDiffWithOptions(layer.ID, moreOptions, diff); err != nil {
cleanupFailureContext = "applying layer diff"
return nil, -1, err
}
} else if slo != nil {
if err := r.applyDiffFromStagingDirectory(layer.ID, slo.DiffOutput, slo.DiffOptions); err != nil {
cleanupFailureContext = "applying staged directory diff"
return nil, -1, err
if contents != nil {
if contents.stagedLayerExtraction != nil {
if contents.stagedLayerExtraction.result != nil {
// The layer is staged, just commit it and update the metadata.
if err := contents.stagedLayerExtraction.commitLayer(r, layer.ID); err != nil {
cleanupFailureContext = "committing staged layer diff"
return nil, -1, err
}
r.applyDiffResultToLayer(layer, contents.stagedLayerExtraction.result)
} else {
// The diff was not staged, apply it now here instead.
if size, err = r.applyDiffWithOptions(layer.ID, moreOptions, contents.stagedLayerExtraction.diff); err != nil {
cleanupFailureContext = "applying layer diff"
return nil, -1, err
}
}
} else {
// staging logic for the chunked pull path
if err := r.applyDiffFromStagingDirectory(layer.ID, contents.DiffOutput, contents.DiffOptions); err != nil {
cleanupFailureContext = "applying staged directory diff"
return nil, -1, err
}
}
} else {
// applyDiffWithOptions() would have updated r.bycompressedsum
// The layer creation content above would have updated r.bycompressedsum
// and r.byuncompressedsum for us, but if we used a template
// layer, we didn't call it, so add the new layer as candidates
// for searches for layers by checksum
@@ -2398,37 +2473,118 @@ func (r *layerStore) ApplyDiff(to string, diff io.Reader) (size int64, err error
return r.applyDiffWithOptions(to, nil, diff)
}
func createTarSplitFile(r *layerStore, layerID string) (*os.File, error) {
if err := os.MkdirAll(filepath.Dir(r.tspath(layerID)), 0o700); err != nil {
return nil, err
}
return os.OpenFile(r.tspath(layerID), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
}
// newMaybeStagedLayerExtraction initializes a new maybeStagedLayerExtraction. The caller
// must call maybeStagedLayerExtraction.cleanup() to remove any temporary files.
func (r *layerStore) newMaybeStagedLayerExtraction(diff io.Reader) *maybeStagedLayerExtraction {
m := &maybeStagedLayerExtraction{
diff: diff,
}
if d, ok := r.driver.(drivers.ApplyDiffStaging); ok {
m.staging = d
}
return m
}
func (sl *maybeStagedLayerExtraction) cleanup() error {
return tempdir.CleanupTemporaryDirectories(sl.cleanupFuncs...)
}
// stageWithUnlockedStore stages the layer content without needing the store locked.
// If the driver does not support stage addition then this is a NOP and does nothing.
// This should be done without holding the storage lock, if a parent is given the caller
// must check for existence beforehand while holding a lock.
func (r *layerStore) stageWithUnlockedStore(sl *maybeStagedLayerExtraction, parent string, layerOptions *LayerOptions) (retErr error) {
if sl.staging == nil {
return nil
}
td, err := tempdir.NewTempDir(filepath.Join(r.layerdir, tempDirPath))
if err != nil {
return err
}
sl.cleanupFuncs = append(sl.cleanupFuncs, td.Cleanup)
stagedTarSplit, err := td.StageAddition()
if err != nil {
return err
}
sl.stagedTarSplit = stagedTarSplit
f, err := os.OpenFile(stagedTarSplit.Path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
if err != nil {
return err
}
// make sure to check for errors on close and return that one.
defer func() {
closeErr := f.Close()
if retErr == nil {
retErr = closeErr
}
}()
result, err := applyDiff(layerOptions, sl.diff, f, func(payload io.Reader) (int64, error) {
cleanup, stagedLayer, size, err := sl.staging.StartStagingDiffToApply(parent, drivers.ApplyDiffOpts{
Diff: payload,
Mappings: idtools.NewIDMappingsFromMaps(layerOptions.UIDMap, layerOptions.GIDMap),
// MountLabel is not supported for the unlocked extraction, see the comment in (*store).PutLayer()
MountLabel: "",
})
sl.cleanupFuncs = append(sl.cleanupFuncs, cleanup)
sl.stagedLayer = stagedLayer
return size, err
})
if err != nil {
return err
}
if err := f.Sync(); err != nil {
return fmt.Errorf("sync staged tar-split file: %w", err)
}
sl.result = result
return nil
}
// commitLayer() commits the content that was staged in stageWithUnlockedStore()
//
// Requires startWriting.
func (r *layerStore) applyDiffWithOptions(to string, layerOptions *LayerOptions, diff io.Reader) (size int64, err error) {
if !r.lockfile.IsReadWrite() {
return -1, fmt.Errorf("not allowed to modify layer contents at %q: %w", r.layerdir, ErrStoreIsReadOnly)
}
layer, ok := r.lookup(to)
if !ok {
return -1, ErrLayerUnknown
func (sl *maybeStagedLayerExtraction) commitLayer(r *layerStore, layerID string) error {
err := sl.stagedTarSplit.Commit(r.tspath(layerID))
if err != nil {
return err
}
return sl.staging.CommitStagedLayer(layerID, sl.stagedLayer)
}
// applyDiff can be called without holding any store locks so if the supplied
// applyDriverFunc requires locking the caller must ensure proper locking.
func applyDiff(layerOptions *LayerOptions, diff io.Reader, tarSplitFile *os.File, applyDriverFunc func(io.Reader) (int64, error)) (*applyDiffResult, error) {
header := make([]byte, 10240)
n, err := diff.Read(header)
if err != nil && err != io.EOF {
return -1, err
return nil, err
}
compression := archive.DetectCompression(header[:n])
defragmented := io.MultiReader(bytes.NewReader(header[:n]), diff)
// Decide if we need to compute digests
var compressedDigest, uncompressedDigest digest.Digest // = ""
result := applyDiffResult{}
var compressedDigester, uncompressedDigester digest.Digester // = nil
if layerOptions != nil && layerOptions.OriginalDigest != "" &&
layerOptions.OriginalDigest.Algorithm() == digest.Canonical {
compressedDigest = layerOptions.OriginalDigest
result.compressedDigest = layerOptions.OriginalDigest
} else {
compressedDigester = digest.Canonical.Digester()
}
if layerOptions != nil && layerOptions.UncompressedDigest != "" &&
layerOptions.UncompressedDigest.Algorithm() == digest.Canonical {
uncompressedDigest = layerOptions.UncompressedDigest
result.uncompressedDigest = layerOptions.UncompressedDigest
} else if compression != archive.Uncompressed {
uncompressedDigester = digest.Canonical.Digester()
}
@@ -2442,13 +2598,15 @@ func (r *layerStore) applyDiffWithOptions(to string, layerOptions *LayerOptions,
compressedCounter := ioutils.NewWriteCounter(compressedWriter)
defragmented = io.TeeReader(defragmented, compressedCounter)
tsdata := bytes.Buffer{}
tarSplitWriter := pools.BufioWriter32KPool.Get(tarSplitFile)
defer pools.BufioWriter32KPool.Put(tarSplitWriter)
uidLog := make(map[uint32]struct{})
gidLog := make(map[uint32]struct{})
var uncompressedCounter *ioutils.WriteCounter
size, err = func() (int64, error) { // A scope for defer
compressor, err := pgzip.NewWriterLevel(&tsdata, pgzip.BestSpeed)
size, err := func() (int64, error) { // A scope for defer
compressor, err := pgzip.NewWriterLevel(tarSplitWriter, pgzip.BestSpeed)
if err != nil {
return -1, err
}
@@ -2481,62 +2639,108 @@ func (r *layerStore) applyDiffWithOptions(to string, layerOptions *LayerOptions,
if err != nil {
return -1, err
}
return applyDriverFunc(payload)
}()
if err != nil {
return nil, err
}
if err := tarSplitWriter.Flush(); err != nil {
return nil, fmt.Errorf("failed to flush tar-split writer buffer: %w", err)
}
if compressedDigester != nil {
result.compressedDigest = compressedDigester.Digest()
}
if uncompressedDigester != nil {
result.uncompressedDigest = uncompressedDigester.Digest()
}
if result.uncompressedDigest == "" && compression == archive.Uncompressed {
result.uncompressedDigest = result.compressedDigest
}
if layerOptions != nil && layerOptions.OriginalDigest != "" && layerOptions.OriginalSize != nil {
result.compressedSize = *layerOptions.OriginalSize
} else {
result.compressedSize = compressedCounter.Count
}
result.uncompressedSize = uncompressedCounter.Count
result.compressionType = compression
result.uids = make([]uint32, 0, len(uidLog))
for uid := range uidLog {
result.uids = append(result.uids, uid)
}
slices.Sort(result.uids)
result.gids = make([]uint32, 0, len(gidLog))
for gid := range gidLog {
result.gids = append(result.gids, gid)
}
slices.Sort(result.gids)
result.size = size
return &result, err
}
// Requires startWriting.
func (r *layerStore) applyDiffWithOptions(to string, layerOptions *LayerOptions, diff io.Reader) (_ int64, retErr error) {
if !r.lockfile.IsReadWrite() {
return -1, fmt.Errorf("not allowed to modify layer contents at %q: %w", r.layerdir, ErrStoreIsReadOnly)
}
layer, ok := r.lookup(to)
if !ok {
return -1, ErrLayerUnknown
}
tarSplitFile, err := createTarSplitFile(r, layer.ID)
if err != nil {
return -1, err
}
// make sure to check for errors on close and return that one.
defer func() {
closeErr := tarSplitFile.Close()
if retErr == nil {
retErr = closeErr
}
}()
result, err := applyDiff(layerOptions, diff, tarSplitFile, func(payload io.Reader) (int64, error) {
options := drivers.ApplyDiffOpts{
Diff: payload,
Mappings: r.layerMappings(layer),
MountLabel: layer.MountLabel,
}
size, err := r.driver.ApplyDiff(layer.ID, layer.Parent, options)
if err != nil {
return -1, err
}
return size, err
}()
return r.driver.ApplyDiff(layer.ID, options)
})
if err != nil {
return -1, err
}
if err := os.MkdirAll(filepath.Dir(r.tspath(layer.ID)), 0o700); err != nil {
return -1, err
}
if err := ioutils.AtomicWriteFile(r.tspath(layer.ID), tsdata.Bytes(), 0o600); err != nil {
return -1, err
}
if compressedDigester != nil {
compressedDigest = compressedDigester.Digest()
}
if uncompressedDigester != nil {
uncompressedDigest = uncompressedDigester.Digest()
}
if uncompressedDigest == "" && compression == archive.Uncompressed {
uncompressedDigest = compressedDigest
if err := tarSplitFile.Sync(); err != nil {
return -1, fmt.Errorf("sync tar-split file: %w", err)
}
updateDigestMap(&r.bycompressedsum, layer.CompressedDigest, compressedDigest, layer.ID)
layer.CompressedDigest = compressedDigest
if layerOptions != nil && layerOptions.OriginalDigest != "" && layerOptions.OriginalSize != nil {
layer.CompressedSize = *layerOptions.OriginalSize
} else {
layer.CompressedSize = compressedCounter.Count
}
updateDigestMap(&r.byuncompressedsum, layer.UncompressedDigest, uncompressedDigest, layer.ID)
layer.UncompressedDigest = uncompressedDigest
layer.UncompressedSize = uncompressedCounter.Count
layer.CompressionType = compression
layer.UIDs = make([]uint32, 0, len(uidLog))
for uid := range uidLog {
layer.UIDs = append(layer.UIDs, uid)
}
slices.Sort(layer.UIDs)
layer.GIDs = make([]uint32, 0, len(gidLog))
for gid := range gidLog {
layer.GIDs = append(layer.GIDs, gid)
}
slices.Sort(layer.GIDs)
r.applyDiffResultToLayer(layer, result)
err = r.saveFor(layer)
return size, err
return result.size, err
}
// Requires startWriting.
func (r *layerStore) applyDiffResultToLayer(layer *Layer, result *applyDiffResult) {
updateDigestMap(&r.bycompressedsum, layer.CompressedDigest, result.compressedDigest, layer.ID)
layer.CompressedDigest = result.compressedDigest
layer.CompressedSize = result.compressedSize
updateDigestMap(&r.byuncompressedsum, layer.UncompressedDigest, result.uncompressedDigest, layer.ID)
layer.UncompressedDigest = result.uncompressedDigest
layer.UncompressedSize = result.uncompressedSize
layer.CompressionType = result.compressionType
layer.UIDs = result.uids
layer.GIDs = result.gids
}
// Requires (startReading or?) startWriting.
@@ -2553,7 +2757,7 @@ func (r *layerStore) DifferTarget(id string) (string, error) {
}
// Requires startWriting.
func (r *layerStore) applyDiffFromStagingDirectory(id string, diffOutput *drivers.DriverWithDifferOutput, options *drivers.ApplyDiffWithDifferOpts) error {
func (r *layerStore) applyDiffFromStagingDirectory(id string, diffOutput *drivers.DriverWithDifferOutput, options *drivers.ApplyDiffWithDifferOpts) (retErr error) {
ddriver, ok := r.driver.(drivers.DriverWithDiffer)
if !ok {
return ErrNotSupported
@@ -2597,10 +2801,23 @@ func (r *layerStore) applyDiffFromStagingDirectory(id string, diffOutput *driver
}
if diffOutput.TarSplit != nil {
tsdata := bytes.Buffer{}
compressor, err := pgzip.NewWriterLevel(&tsdata, pgzip.BestSpeed)
tarSplitFile, err := createTarSplitFile(r, layer.ID)
if err != nil {
compressor = pgzip.NewWriter(&tsdata)
return err
}
// make sure to check for errors on close and return that one.
defer func() {
closeErr := tarSplitFile.Close()
if retErr == nil {
retErr = closeErr
}
}()
tarSplitWriter := pools.BufioWriter32KPool.Get(tarSplitFile)
defer pools.BufioWriter32KPool.Put(tarSplitWriter)
compressor, err := pgzip.NewWriterLevel(tarSplitWriter, pgzip.BestSpeed)
if err != nil {
compressor = pgzip.NewWriter(tarSplitWriter)
}
if _, err := diffOutput.TarSplit.Seek(0, io.SeekStart); err != nil {
return err
@@ -2614,11 +2831,12 @@ func (r *layerStore) applyDiffFromStagingDirectory(id string, diffOutput *driver
return err
}
compressor.Close()
if err := os.MkdirAll(filepath.Dir(r.tspath(layer.ID)), 0o700); err != nil {
return err
if err := tarSplitWriter.Flush(); err != nil {
return fmt.Errorf("failed to flush tar-split writer buffer: %w", err)
}
if err := ioutils.AtomicWriteFile(r.tspath(layer.ID), tsdata.Bytes(), 0o600); err != nil {
return err
if err := tarSplitFile.Sync(); err != nil {
return fmt.Errorf("sync tar-split file: %w", err)
}
}
for k, v := range diffOutput.BigData {

View File

@@ -420,7 +420,10 @@ func (l *LockFile) tryLock(lType rawfilelock.LockType) error {
if !success {
return fmt.Errorf("resource temporarily unavailable")
}
l.stateMutex.Lock()
if !l.stateMutex.TryLock() {
rwMutexUnlocker()
return fmt.Errorf("resource temporarily unavailable")
}
defer l.stateMutex.Unlock()
if l.counter == 0 {
// If we're the first reference on the lock, we need to open the file again.

175
vendor/go.podman.io/storage/store.go generated vendored
View File

@@ -1449,12 +1449,48 @@ func (s *store) canUseShifting(uidmap, gidmap []idtools.IDMap) bool {
return s.graphDriver.SupportsShifting(uidmap, gidmap)
}
// On entry:
// - rlstore must be locked for reading or writing
// - rlstores MUST NOT be locked
// Returns an extra unlock function to unlock any potentially read locked rlstores by this function.
// The unlock function is always set and thus must always be called.
func getParentLayer(rlstore roLayerStore, rlstores []roLayerStore, parent string) (*Layer, func(), error) {
// function we return to the caller so the caller gets the right stores locked and can unlock at the proper time themselves
var lockedLayerStores []roLayerStore
unlock := func() {
for _, i := range lockedLayerStores {
i.stopReading()
}
}
for _, l := range append([]roLayerStore{rlstore}, rlstores...) {
lstore := l
if lstore != rlstore {
if err := lstore.startReading(); err != nil {
return nil, unlock, err
}
lockedLayerStores = append(lockedLayerStores, lstore)
}
if l, err := lstore.Get(parent); err == nil && l != nil {
return l, unlock, nil
}
}
return nil, unlock, ErrLayerUnknown
}
// On entry:
// - rlstore must be locked for writing
// - rlstores MUST NOT be locked
func (s *store) putLayer(rlstore rwLayerStore, rlstores []roLayerStore, id, parent string, names []string, mountLabel string, writeable bool, lOptions *LayerOptions, diff io.Reader, slo *stagedLayerOptions) (*Layer, int64, error) {
//
// Returns the new copied LayerOptions with mappings set, the parent Layer and
// an extra unlock function to unlock any potentially read locked rlstores by this function.
// The unlock function is always set and thus must always be called.
func populateLayerOptions(s *store, rlstore rwLayerStore, rlstores []roLayerStore, parent string, lOptions *LayerOptions) (*LayerOptions, *Layer, func(), error) {
// WARNING: Update also the freshLayer checks in store.PutLayer if adding more logic here.
var parentLayer *Layer
var options LayerOptions
// make sure we always return a valid func instead of nil so the caller can call it without checking
unlock := func() {}
if lOptions != nil {
options = *lOptions
options.BigData = slices.Clone(lOptions.BigData)
@@ -1469,53 +1505,32 @@ func (s *store) putLayer(rlstore rwLayerStore, rlstores []roLayerStore, id, pare
uidMap := options.UIDMap
gidMap := options.GIDMap
if parent != "" {
var ilayer *Layer
for _, l := range append([]roLayerStore{rlstore}, rlstores...) {
lstore := l
if lstore != rlstore {
if err := lstore.startReading(); err != nil {
return nil, -1, err
}
defer lstore.stopReading()
}
if l, err := lstore.Get(parent); err == nil && l != nil {
ilayer = l
parent = ilayer.ID
break
}
var err error
parentLayer, unlock, err = getParentLayer(rlstore, rlstores, parent)
if err != nil {
return nil, nil, unlock, err
}
if ilayer == nil {
return nil, -1, ErrLayerUnknown
}
parentLayer = ilayer
if err := s.containerStore.startWriting(); err != nil {
return nil, -1, err
return nil, nil, unlock, err
}
defer s.containerStore.stopWriting()
containers, err := s.containerStore.Containers()
if err != nil {
return nil, -1, err
return nil, nil, unlock, err
}
for _, container := range containers {
if container.LayerID == parent {
return nil, -1, ErrParentIsContainer
return nil, nil, unlock, ErrParentIsContainer
}
}
if !options.HostUIDMapping && len(options.UIDMap) == 0 {
uidMap = ilayer.UIDMap
uidMap = parentLayer.UIDMap
}
if !options.HostGIDMapping && len(options.GIDMap) == 0 {
gidMap = ilayer.GIDMap
gidMap = parentLayer.GIDMap
}
} else {
// FIXME? Its unclear why we are holding containerStore locked here at all
// (and because we are not modifying it, why it is a write lock, not a read lock).
if err := s.containerStore.startWriting(); err != nil {
return nil, -1, err
}
defer s.containerStore.stopWriting()
if !options.HostUIDMapping && len(options.UIDMap) == 0 {
uidMap = s.uidMap
}
@@ -1533,7 +1548,7 @@ func (s *store) putLayer(rlstore rwLayerStore, rlstores []roLayerStore, id, pare
GIDMap: copySlicePreferringNil(gidMap),
}
}
return rlstore.create(id, parentLayer, names, mountLabel, nil, &options, writeable, diff, slo)
return &options, parentLayer, unlock, nil
}
func (s *store) PutLayer(id, parent string, names []string, mountLabel string, writeable bool, lOptions *LayerOptions, diff io.Reader) (*Layer, int64, error) {
@@ -1541,11 +1556,92 @@ func (s *store) PutLayer(id, parent string, names []string, mountLabel string, w
if err != nil {
return nil, -1, err
}
var (
contents *layerCreationContents
options *LayerOptions
parentLayer *Layer
)
if diff != nil {
m := rlstore.newMaybeStagedLayerExtraction(diff)
defer func() {
if err := m.cleanup(); err != nil {
logrus.Errorf("Error cleaning up temporary directories: %v", err)
}
}()
// driver can do unlocked staging so do that without holding the layer lock
// Special case we only support it when no mount label is used. c/image doesn't set it for layers
// and the overlay driver doesn't use it for extract today so it would be safe even when set but
// that is not exactly obvious and if someone would implement the ApplyDiffStaging interface for
// another driver that may be no longer true. So for now simply fall back to the locked extract path
// to ensure we don't cause any weird issues here.
if m.staging != nil && mountLabel == "" {
// func so we have a scope for defer, we don't want to hold the lock for stageWithUnlockedStore()
layer, err := func() (*Layer, error) {
if err := rlstore.startWriting(); err != nil {
return nil, err
}
defer rlstore.stopWriting()
if layer, err := rlstore.checkIdOrNameConflict(id, names); err != nil {
return layer, err
}
var unlockLayerStores func()
options, parentLayer, unlockLayerStores, err = populateLayerOptions(s, rlstore, rlstores, parent, lOptions)
unlockLayerStores()
return nil, err
}()
if err != nil {
return layer, -1, err
}
// make sure to use the resolved full ID if there is a parent
if parentLayer != nil {
parent = parentLayer.ID
}
if err := rlstore.stageWithUnlockedStore(m, parent, options); err != nil {
return nil, -1, err
}
}
contents = &layerCreationContents{
stagedLayerExtraction: m,
}
}
if err := rlstore.startWriting(); err != nil {
return nil, -1, err
}
defer rlstore.stopWriting()
return s.putLayer(rlstore, rlstores, id, parent, names, mountLabel, writeable, lOptions, diff, nil)
if options == nil {
var unlockLayerStores func()
options, parentLayer, unlockLayerStores, err = populateLayerOptions(s, rlstore, rlstores, parent, lOptions)
defer unlockLayerStores()
if err != nil {
return nil, -1, err
}
} else if parent != "" {
// We used the staged extraction without holding the lock.
// Check again that the parent layer is still valid and exists.
freshLayer, unlockLayerStores, err := getParentLayer(rlstore, rlstores, parent)
defer unlockLayerStores()
if err != nil {
return nil, -1, err
}
// In populateLayerOptions() we get the ID mappings in order to extract correctly, ensure the freshly
// looked up parent Layer still has the same mappings to prevent silent UID/GID corruption.
if !slices.Equal(freshLayer.UIDMap, parentLayer.UIDMap) || !slices.Equal(freshLayer.GIDMap, parentLayer.GIDMap) {
// Fatal problem. Mappings changed so the parent must be considered different now.
// Since we consumed the diff there is no we to recover, return error to caller. The caller would need to retry.
// How likely is that and would need to return a special error so c/image could do the retries?
return nil, -1, fmt.Errorf("error during staged layer apply, parent layer %q changed id mappings while the content was extracted, must retry layer creation", parent)
}
}
return rlstore.create(id, parentLayer, names, mountLabel, nil, options, writeable, contents)
}
func (s *store) CreateLayer(id, parent string, names []string, mountLabel string, writeable bool, options *LayerOptions) (*Layer, error) {
@@ -1753,7 +1849,7 @@ func (s *store) imageTopLayerForMapping(image *Image, ristore roImageStore, rlst
}
}
layerOptions.TemplateLayer = layer.ID
mappedLayer, _, err := rlstore.create("", parentLayer, nil, layer.MountLabel, nil, &layerOptions, false, nil, nil)
mappedLayer, _, err := rlstore.create("", parentLayer, nil, layer.MountLabel, nil, &layerOptions, false, nil)
if err != nil {
return nil, fmt.Errorf("creating an ID-mapped copy of layer %q: %w", layer.ID, err)
}
@@ -1924,7 +2020,7 @@ func (s *store) CreateContainer(id string, names []string, image, layer, metadat
options.Flags[mountLabelFlag] = mountLabel
}
clayer, _, err := rlstore.create(layer, imageTopLayer, nil, mlabel, options.StorageOpt, layerOptions, true, nil, nil)
clayer, _, err := rlstore.create(layer, imageTopLayer, nil, mlabel, options.StorageOpt, layerOptions, true, nil)
if err != nil {
return nil, err
}
@@ -3182,11 +3278,16 @@ func (s *store) ApplyStagedLayer(args ApplyStagedLayerOptions) (*Layer, error) {
// if the layer doesn't exist yet, try to create it.
slo := stagedLayerOptions{
contents := layerCreationContents{
DiffOutput: args.DiffOutput,
DiffOptions: args.DiffOptions,
}
layer, _, err = s.putLayer(rlstore, rlstores, args.ID, args.ParentLayer, args.Names, args.MountLabel, args.Writeable, args.LayerOptions, nil, &slo)
options, parentLayer, unlockLayerStores, err := populateLayerOptions(s, rlstore, rlstores, args.ParentLayer, args.LayerOptions)
defer unlockLayerStores()
if err != nil {
return nil, err
}
layer, _, err = rlstore.create(args.ID, parentLayer, args.Names, args.MountLabel, nil, options, args.Writeable, &contents)
return layer, err
}

View File

@@ -197,7 +197,7 @@ outer:
// We need to create a temporary layer so we can mount it and lookup the
// maximum IDs used.
clayer, _, err := rlstore.create("", topLayer, nil, "", nil, layerOptions, false, nil, nil)
clayer, _, err := rlstore.create("", topLayer, nil, "", nil, layerOptions, false, nil)
if err != nil {
return 0, err
}

14
vendor/modules.txt vendored
View File

@@ -475,7 +475,7 @@ github.com/moby/moby/api/types/storage
github.com/moby/moby/api/types/swarm
github.com/moby/moby/api/types/system
github.com/moby/moby/api/types/volume
# github.com/moby/moby/client v0.1.0
# github.com/moby/moby/client v0.2.1
## explicit; go 1.24.0
github.com/moby/moby/client
github.com/moby/moby/client/internal
@@ -575,12 +575,12 @@ github.com/opencontainers/go-digest
## explicit; go 1.18
github.com/opencontainers/image-spec/specs-go
github.com/opencontainers/image-spec/specs-go/v1
# github.com/opencontainers/runc v1.3.3
## explicit; go 1.23.0
# github.com/opencontainers/runc v1.4.0
## explicit; go 1.24.0
github.com/opencontainers/runc/internal/linux
github.com/opencontainers/runc/internal/pathrs
github.com/opencontainers/runc/libcontainer/apparmor
github.com/opencontainers/runc/libcontainer/devices
github.com/opencontainers/runc/libcontainer/utils
# github.com/opencontainers/runtime-spec v1.3.0
## explicit
github.com/opencontainers/runtime-spec/specs-go
@@ -773,7 +773,7 @@ go.opentelemetry.io/otel/trace
go.opentelemetry.io/otel/trace/embedded
go.opentelemetry.io/otel/trace/internal/telemetry
go.opentelemetry.io/otel/trace/noop
# go.podman.io/common v0.66.1-0.20251126122123-4fc82df3fdc0
# go.podman.io/common v0.66.1-0.20251128185259-94e31d2e45ba
## explicit; go 1.24.6
go.podman.io/common/internal
go.podman.io/common/internal/attributedstring
@@ -843,7 +843,7 @@ go.podman.io/common/pkg/umask
go.podman.io/common/pkg/util
go.podman.io/common/pkg/version
go.podman.io/common/version
# go.podman.io/image/v5 v5.38.1-0.20251126122123-4fc82df3fdc0
# go.podman.io/image/v5 v5.38.1-0.20251128185259-94e31d2e45ba
## explicit; go 1.24.6
go.podman.io/image/v5/copy
go.podman.io/image/v5/directory
@@ -917,7 +917,7 @@ go.podman.io/image/v5/transports
go.podman.io/image/v5/transports/alltransports
go.podman.io/image/v5/types
go.podman.io/image/v5/version
# go.podman.io/storage v1.61.1-0.20251125064110-c4e25180a61d
# go.podman.io/storage v1.61.1-0.20251128185259-94e31d2e45ba
## explicit; go 1.24.0
go.podman.io/storage
go.podman.io/storage/drivers