mirror of https://github.com/containers/podman.git
Merge pull request #15437 from mheon/default_volume_timeout
Add support for containers.conf volume timeouts
This commit is contained in:
commit
0f92cf22a6
2
go.mod
2
go.mod
|
@ -12,7 +12,7 @@ require (
|
||||||
github.com/containernetworking/cni v1.1.2
|
github.com/containernetworking/cni v1.1.2
|
||||||
github.com/containernetworking/plugins v1.1.1
|
github.com/containernetworking/plugins v1.1.1
|
||||||
github.com/containers/buildah v1.27.0
|
github.com/containers/buildah v1.27.0
|
||||||
github.com/containers/common v0.49.2-0.20220817132854-f6679f170eca
|
github.com/containers/common v0.49.2-0.20220823130605-72a7da3358ac
|
||||||
github.com/containers/conmon v2.0.20+incompatible
|
github.com/containers/conmon v2.0.20+incompatible
|
||||||
github.com/containers/image/v5 v5.22.0
|
github.com/containers/image/v5 v5.22.0
|
||||||
github.com/containers/ocicrypt v1.1.5
|
github.com/containers/ocicrypt v1.1.5
|
||||||
|
|
10
go.sum
10
go.sum
|
@ -140,8 +140,9 @@ github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwT
|
||||||
github.com/Microsoft/hcsshim v0.8.22/go.mod h1:91uVCVzvX2QD16sMCenoxxXo6L1wJnLMX2PSufFMtF0=
|
github.com/Microsoft/hcsshim v0.8.22/go.mod h1:91uVCVzvX2QD16sMCenoxxXo6L1wJnLMX2PSufFMtF0=
|
||||||
github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg=
|
github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg=
|
||||||
github.com/Microsoft/hcsshim v0.9.2/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc=
|
github.com/Microsoft/hcsshim v0.9.2/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc=
|
||||||
github.com/Microsoft/hcsshim v0.9.3 h1:k371PzBuRrz2b+ebGuI2nVgVhgsVX60jMfSw80NECxo=
|
|
||||||
github.com/Microsoft/hcsshim v0.9.3/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc=
|
github.com/Microsoft/hcsshim v0.9.3/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc=
|
||||||
|
github.com/Microsoft/hcsshim v0.9.4 h1:mnUj0ivWy6UzbB1uLFqKR6F+ZyiDc7j4iGgHTpO+5+I=
|
||||||
|
github.com/Microsoft/hcsshim v0.9.4/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc=
|
||||||
github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU=
|
github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU=
|
||||||
github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY=
|
github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY=
|
||||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||||
|
@ -323,8 +324,9 @@ github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0
|
||||||
github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s=
|
github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s=
|
||||||
github.com/containerd/containerd v1.5.9/go.mod h1:fvQqCfadDGga5HZyn3j4+dx56qj2I9YwBrlSdalvJYQ=
|
github.com/containerd/containerd v1.5.9/go.mod h1:fvQqCfadDGga5HZyn3j4+dx56qj2I9YwBrlSdalvJYQ=
|
||||||
github.com/containerd/containerd v1.6.1/go.mod h1:1nJz5xCZPusx6jJU8Frfct988y0NpumIq9ODB0kLtoE=
|
github.com/containerd/containerd v1.6.1/go.mod h1:1nJz5xCZPusx6jJU8Frfct988y0NpumIq9ODB0kLtoE=
|
||||||
github.com/containerd/containerd v1.6.6 h1:xJNPhbrmz8xAMDNoVjHy9YHtWwEQNS+CDkcIRh7t8Y0=
|
|
||||||
github.com/containerd/containerd v1.6.6/go.mod h1:ZoP1geJldzCVY3Tonoz7b1IXk8rIX0Nltt5QE4OMNk0=
|
github.com/containerd/containerd v1.6.6/go.mod h1:ZoP1geJldzCVY3Tonoz7b1IXk8rIX0Nltt5QE4OMNk0=
|
||||||
|
github.com/containerd/containerd v1.6.8 h1:h4dOFDwzHmqFEP754PgfgTeVXFnLiRc6kiqC7tplDJs=
|
||||||
|
github.com/containerd/containerd v1.6.8/go.mod h1:By6p5KqPK0/7/CgO/A6t/Gz+CUYUu2zf1hUaaymVXB0=
|
||||||
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||||
github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||||
github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||||
|
@ -395,8 +397,8 @@ github.com/containernetworking/plugins v1.1.1/go.mod h1:Sr5TH/eBsGLXK/h71HeLfX19
|
||||||
github.com/containers/buildah v1.27.0 h1:LJ1ks7vKxwPzJGr5BWVvigbtVL9w7XeHtNEmiIOPJqI=
|
github.com/containers/buildah v1.27.0 h1:LJ1ks7vKxwPzJGr5BWVvigbtVL9w7XeHtNEmiIOPJqI=
|
||||||
github.com/containers/buildah v1.27.0/go.mod h1:anH3ExvDXRNP9zLQCrOc1vWb5CrhqLF/aYFim4tslvA=
|
github.com/containers/buildah v1.27.0/go.mod h1:anH3ExvDXRNP9zLQCrOc1vWb5CrhqLF/aYFim4tslvA=
|
||||||
github.com/containers/common v0.49.1/go.mod h1:ueM5hT0itKqCQvVJDs+EtjornAQtrHYxQJzP2gxeGIg=
|
github.com/containers/common v0.49.1/go.mod h1:ueM5hT0itKqCQvVJDs+EtjornAQtrHYxQJzP2gxeGIg=
|
||||||
github.com/containers/common v0.49.2-0.20220817132854-f6679f170eca h1:OjhEBVpFskIJ6Vq9nikYW7M6YXfkTxOBu+EQBoCyhuM=
|
github.com/containers/common v0.49.2-0.20220823130605-72a7da3358ac h1:rLbTzosxPKrQd+EgMRxfC1WYm3azPiQfig+Lr7mCQ4k=
|
||||||
github.com/containers/common v0.49.2-0.20220817132854-f6679f170eca/go.mod h1:eT2iSsNzjOlF5VFLkyj9OU2SXznURvEYndsioQImuoE=
|
github.com/containers/common v0.49.2-0.20220823130605-72a7da3358ac/go.mod h1:xC4qkLfW9R+YSDknlT9xU+NDNxIw017U8AyohGtr9Ec=
|
||||||
github.com/containers/conmon v2.0.20+incompatible h1:YbCVSFSCqFjjVwHTPINGdMX1F6JXHGTUje2ZYobNrkg=
|
github.com/containers/conmon v2.0.20+incompatible h1:YbCVSFSCqFjjVwHTPINGdMX1F6JXHGTUje2ZYobNrkg=
|
||||||
github.com/containers/conmon v2.0.20+incompatible/go.mod h1:hgwZ2mtuDrppv78a/cOBNiCm6O0UMWGx1mu7P00nu5I=
|
github.com/containers/conmon v2.0.20+incompatible/go.mod h1:hgwZ2mtuDrppv78a/cOBNiCm6O0UMWGx1mu7P00nu5I=
|
||||||
github.com/containers/image/v5 v5.22.0 h1:KemxPmD4D2YYOFZN2SgoTk7nBFcnwPiPW0MqjYtknSE=
|
github.com/containers/image/v5 v5.22.0 h1:KemxPmD4D2YYOFZN2SgoTk7nBFcnwPiPW0MqjYtknSE=
|
||||||
|
|
|
@ -57,7 +57,7 @@ type InspectVolumeData struct {
|
||||||
// UID/GID.
|
// UID/GID.
|
||||||
NeedsChown bool `json:"NeedsChown,omitempty"`
|
NeedsChown bool `json:"NeedsChown,omitempty"`
|
||||||
// Timeout is the specified driver timeout if given
|
// Timeout is the specified driver timeout if given
|
||||||
Timeout int `json:"Timeout,omitempty"`
|
Timeout uint `json:"Timeout,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VolumeReload struct {
|
type VolumeReload struct {
|
||||||
|
|
|
@ -1695,14 +1695,22 @@ func withSetAnon() VolumeCreateOption {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithVolumeDriverTimeout sets the volume creation timeout period
|
// WithVolumeDriverTimeout sets the volume creation timeout period.
|
||||||
func WithVolumeDriverTimeout(timeout int) VolumeCreateOption {
|
// Only usable if a non-local volume driver is in use.
|
||||||
|
func WithVolumeDriverTimeout(timeout uint) VolumeCreateOption {
|
||||||
return func(volume *Volume) error {
|
return func(volume *Volume) error {
|
||||||
if volume.valid {
|
if volume.valid {
|
||||||
return define.ErrVolumeFinalized
|
return define.ErrVolumeFinalized
|
||||||
}
|
}
|
||||||
|
|
||||||
volume.config.Timeout = timeout
|
if volume.config.Driver == "" || volume.config.Driver == define.VolumeDriverLocal {
|
||||||
|
return fmt.Errorf("Volume driver timeout can only be used with non-local volume drivers: %w", define.ErrInvalidArg)
|
||||||
|
}
|
||||||
|
|
||||||
|
tm := timeout
|
||||||
|
|
||||||
|
volume.config.Timeout = &tm
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package plugin
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
|
@ -13,8 +14,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"errors"
|
"github.com/containers/common/pkg/config"
|
||||||
|
|
||||||
"github.com/containers/podman/v4/libpod/define"
|
"github.com/containers/podman/v4/libpod/define"
|
||||||
"github.com/docker/go-plugins-helpers/sdk"
|
"github.com/docker/go-plugins-helpers/sdk"
|
||||||
"github.com/docker/go-plugins-helpers/volume"
|
"github.com/docker/go-plugins-helpers/volume"
|
||||||
|
@ -40,7 +40,6 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultTimeout = 5 * time.Second
|
|
||||||
volumePluginType = "VolumeDriver"
|
volumePluginType = "VolumeDriver"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -129,7 +128,7 @@ func validatePlugin(newPlugin *VolumePlugin) error {
|
||||||
|
|
||||||
// GetVolumePlugin gets a single volume plugin, with the given name, at the
|
// GetVolumePlugin gets a single volume plugin, with the given name, at the
|
||||||
// given path.
|
// given path.
|
||||||
func GetVolumePlugin(name string, path string, timeout int) (*VolumePlugin, error) {
|
func GetVolumePlugin(name string, path string, timeout *uint, cfg *config.Config) (*VolumePlugin, error) {
|
||||||
pluginsLock.Lock()
|
pluginsLock.Lock()
|
||||||
defer pluginsLock.Unlock()
|
defer pluginsLock.Unlock()
|
||||||
|
|
||||||
|
@ -152,13 +151,11 @@ func GetVolumePlugin(name string, path string, timeout int) (*VolumePlugin, erro
|
||||||
// Need an HTTP client to force a Unix connection.
|
// Need an HTTP client to force a Unix connection.
|
||||||
// And since we can reuse it, might as well cache it.
|
// And since we can reuse it, might as well cache it.
|
||||||
client := new(http.Client)
|
client := new(http.Client)
|
||||||
client.Timeout = defaultTimeout
|
client.Timeout = 5 * time.Second
|
||||||
// if the user specified a non-zero timeout, use their value. Else, keep the default.
|
if timeout != nil {
|
||||||
if timeout != 0 {
|
client.Timeout = time.Duration(*timeout) * time.Second
|
||||||
if time.Duration(timeout)*time.Second < defaultTimeout {
|
} else if cfg != nil {
|
||||||
logrus.Warnf("the default timeout for volume creation is %d seconds, setting a time less than that may break this feature.", defaultTimeout)
|
client.Timeout = time.Duration(cfg.Engine.VolumePluginTimeout) * time.Second
|
||||||
}
|
|
||||||
client.Timeout = time.Duration(timeout) * time.Second
|
|
||||||
}
|
}
|
||||||
// This bit borrowed from pkg/bindings/connection.go
|
// This bit borrowed from pkg/bindings/connection.go
|
||||||
client.Transport = &http.Transport{
|
client.Transport = &http.Transport{
|
||||||
|
|
|
@ -1097,7 +1097,7 @@ func (r *Runtime) getVolumePlugin(volConfig *VolumeConfig) (*plugin.VolumePlugin
|
||||||
return nil, fmt.Errorf("no volume plugin with name %s available: %w", name, define.ErrMissingPlugin)
|
return nil, fmt.Errorf("no volume plugin with name %s available: %w", name, define.ErrMissingPlugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
return plugin.GetVolumePlugin(name, pluginPath, timeout)
|
return plugin.GetVolumePlugin(name, pluginPath, timeout, r.config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSecretsStorageDir returns the directory that the secrets manager should take
|
// GetSecretsStorageDir returns the directory that the secrets manager should take
|
||||||
|
|
|
@ -184,7 +184,7 @@ func (r *Runtime) UpdateVolumePlugins(ctx context.Context) *define.VolumeReload
|
||||||
)
|
)
|
||||||
|
|
||||||
for driverName, socket := range r.config.Engine.VolumePlugins {
|
for driverName, socket := range r.config.Engine.VolumePlugins {
|
||||||
driver, err := volplugin.GetVolumePlugin(driverName, socket, 0)
|
driver, err := volplugin.GetVolumePlugin(driverName, socket, nil, r.config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -56,7 +56,7 @@ type VolumeConfig struct {
|
||||||
// quota tracking.
|
// quota tracking.
|
||||||
DisableQuota bool `json:"disableQuota,omitempty"`
|
DisableQuota bool `json:"disableQuota,omitempty"`
|
||||||
// Timeout allows users to override the default driver timeout of 5 seconds
|
// Timeout allows users to override the default driver timeout of 5 seconds
|
||||||
Timeout int
|
Timeout *uint `json:"timeout,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// VolumeState holds the volume's mutable state.
|
// VolumeState holds the volume's mutable state.
|
||||||
|
|
|
@ -64,7 +64,12 @@ func (v *Volume) Inspect() (*define.InspectVolumeData, error) {
|
||||||
data.MountCount = v.state.MountCount
|
data.MountCount = v.state.MountCount
|
||||||
data.NeedsCopyUp = v.state.NeedsCopyUp
|
data.NeedsCopyUp = v.state.NeedsCopyUp
|
||||||
data.NeedsChown = v.state.NeedsChown
|
data.NeedsChown = v.state.NeedsChown
|
||||||
data.Timeout = v.config.Timeout
|
|
||||||
|
if v.config.Timeout != nil {
|
||||||
|
data.Timeout = *v.config.Timeout
|
||||||
|
} else if v.UsesVolumeDriver() {
|
||||||
|
data.Timeout = v.runtime.config.Engine.VolumePluginTimeout
|
||||||
|
}
|
||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,8 +86,11 @@ func VolumeOptions(opts map[string]string) ([]libpod.VolumeCreateOption, error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot convert Timeout %s to an integer: %w", splitO[1], err)
|
return nil, fmt.Errorf("cannot convert Timeout %s to an integer: %w", splitO[1], err)
|
||||||
}
|
}
|
||||||
|
if intTimeout < 0 {
|
||||||
|
return nil, fmt.Errorf("volume timeout cannot be negative (got %d)", intTimeout)
|
||||||
|
}
|
||||||
logrus.Debugf("Removing timeout from options and adding WithTimeout for Timeout %d", intTimeout)
|
logrus.Debugf("Removing timeout from options and adding WithTimeout for Timeout %d", intTimeout)
|
||||||
libpodOptions = append(libpodOptions, libpod.WithVolumeDriverTimeout(intTimeout))
|
libpodOptions = append(libpodOptions, libpod.WithVolumeDriverTimeout(uint(intTimeout)))
|
||||||
default:
|
default:
|
||||||
finalVal = append(finalVal, o)
|
finalVal = append(finalVal, o)
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,8 @@ no_hosts=true
|
||||||
network_cmd_options=["allow_host_loopback=true"]
|
network_cmd_options=["allow_host_loopback=true"]
|
||||||
service_timeout=1234
|
service_timeout=1234
|
||||||
|
|
||||||
|
volume_plugin_timeout = 15
|
||||||
|
|
||||||
# We need to ensure each test runs on a separate plugin instance...
|
# We need to ensure each test runs on a separate plugin instance...
|
||||||
# For now, let's just make a bunch of plugin paths and have each test use one.
|
# For now, let's just make a bunch of plugin paths and have each test use one.
|
||||||
[engine.volume_plugins]
|
[engine.volume_plugins]
|
||||||
|
|
|
@ -162,19 +162,4 @@ var _ = Describe("Podman volume create", func() {
|
||||||
Expect(inspectOpts).Should(Exit(0))
|
Expect(inspectOpts).Should(Exit(0))
|
||||||
Expect(inspectOpts.OutputToString()).To(Equal(optionStrFormatExpect))
|
Expect(inspectOpts.OutputToString()).To(Equal(optionStrFormatExpect))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("podman create volume with o=timeout", func() {
|
|
||||||
volName := "testVol"
|
|
||||||
timeout := 10
|
|
||||||
timeoutStr := "10"
|
|
||||||
session := podmanTest.Podman([]string{"volume", "create", "--opt", fmt.Sprintf("o=timeout=%d", timeout), volName})
|
|
||||||
session.WaitWithDefaultTimeout()
|
|
||||||
Expect(session).Should(Exit(0))
|
|
||||||
|
|
||||||
inspectTimeout := podmanTest.Podman([]string{"volume", "inspect", "--format", "{{ .Timeout }}", volName})
|
|
||||||
inspectTimeout.WaitWithDefaultTimeout()
|
|
||||||
Expect(inspectTimeout).Should(Exit(0))
|
|
||||||
Expect(inspectTimeout.OutputToString()).To(Equal(timeoutStr))
|
|
||||||
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -256,4 +256,38 @@ Removed:
|
||||||
Expect(session.OutputToStringArray()).To(ContainElements(localvol, vol2))
|
Expect(session.OutputToStringArray()).To(ContainElements(localvol, vol2))
|
||||||
Expect(session.ErrorToString()).To(Equal("")) // make no errors are shown
|
Expect(session.ErrorToString()).To(Equal("")) // make no errors are shown
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("volume driver timeouts test", func() {
|
||||||
|
podmanTest.AddImageToRWStore(volumeTest)
|
||||||
|
|
||||||
|
pluginStatePath := filepath.Join(podmanTest.TempDir, "volumes")
|
||||||
|
err := os.Mkdir(pluginStatePath, 0755)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Keep this distinct within tests to avoid multiple tests using the same plugin.
|
||||||
|
pluginName := "testvol6"
|
||||||
|
plugin := podmanTest.Podman([]string{"run", "--security-opt", "label=disable", "-v", "/run/docker/plugins:/run/docker/plugins", "-v", fmt.Sprintf("%v:%v", pluginStatePath, pluginStatePath), "-d", volumeTest, "--sock-name", pluginName, "--path", pluginStatePath})
|
||||||
|
plugin.WaitWithDefaultTimeout()
|
||||||
|
Expect(plugin).Should(Exit(0))
|
||||||
|
|
||||||
|
volName := "testVolume1"
|
||||||
|
create := podmanTest.Podman([]string{"volume", "create", "--driver", pluginName, volName})
|
||||||
|
create.WaitWithDefaultTimeout()
|
||||||
|
Expect(create).Should(Exit(0))
|
||||||
|
|
||||||
|
volInspect := podmanTest.Podman([]string{"volume", "inspect", "--format", "{{ .Timeout }}", volName})
|
||||||
|
volInspect.WaitWithDefaultTimeout()
|
||||||
|
Expect(volInspect).Should(Exit(0))
|
||||||
|
Expect(volInspect.OutputToString()).To(ContainSubstring("15"))
|
||||||
|
|
||||||
|
volName2 := "testVolume2"
|
||||||
|
create2 := podmanTest.Podman([]string{"volume", "create", "--driver", pluginName, "--opt", "o=timeout=3", volName2})
|
||||||
|
create2.WaitWithDefaultTimeout()
|
||||||
|
Expect(create2).Should(Exit(0))
|
||||||
|
|
||||||
|
volInspect2 := podmanTest.Podman([]string{"volume", "inspect", "--format", "{{ .Timeout }}", volName2})
|
||||||
|
volInspect2.WaitWithDefaultTimeout()
|
||||||
|
Expect(volInspect2).Should(Exit(0))
|
||||||
|
Expect(volInspect2.OutputToString()).To(ContainSubstring("3"))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -25,5 +25,5 @@ func getPluginName(pathOrName string) string {
|
||||||
func getPlugin(sockNameOrPath string) (*plugin.VolumePlugin, error) {
|
func getPlugin(sockNameOrPath string) (*plugin.VolumePlugin, error) {
|
||||||
path := getSocketPath(sockNameOrPath)
|
path := getSocketPath(sockNameOrPath)
|
||||||
name := getPluginName(sockNameOrPath)
|
name := getPluginName(sockNameOrPath)
|
||||||
return plugin.GetVolumePlugin(name, path, 0)
|
return plugin.GetVolumePlugin(name, path, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ func pollIOCP(ctx context.Context, iocpHandle windows.Handle) {
|
||||||
}).Warn("failed to parse job object message")
|
}).Warn("failed to parse job object message")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := msq.Write(notification); err == queue.ErrQueueClosed {
|
if err := msq.Enqueue(notification); err == queue.ErrQueueClosed {
|
||||||
// Write will only return an error when the queue is closed.
|
// Write will only return an error when the queue is closed.
|
||||||
// The only time a queue would ever be closed is when we call `Close` on
|
// The only time a queue would ever be closed is when we call `Close` on
|
||||||
// the job it belongs to which also removes it from the jobMap, so something
|
// the job it belongs to which also removes it from the jobMap, so something
|
||||||
|
|
|
@ -68,6 +68,9 @@ type Options struct {
|
||||||
// `UseNTVariant` specifies if we should use the `Nt` variant of Open/CreateJobObject.
|
// `UseNTVariant` specifies if we should use the `Nt` variant of Open/CreateJobObject.
|
||||||
// Defaults to false.
|
// Defaults to false.
|
||||||
UseNTVariant bool
|
UseNTVariant bool
|
||||||
|
// `IOTracking` enables tracking I/O statistics on the job object. More specifically this
|
||||||
|
// calls SetInformationJobObject with the JobObjectIoAttribution class.
|
||||||
|
EnableIOTracking bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create creates a job object.
|
// Create creates a job object.
|
||||||
|
@ -134,6 +137,12 @@ func Create(ctx context.Context, options *Options) (_ *JobObject, err error) {
|
||||||
job.mq = mq
|
job.mq = mq
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if options.EnableIOTracking {
|
||||||
|
if err := enableIOTracking(jobHandle); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return job, nil
|
return job, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,7 +244,7 @@ func (job *JobObject) PollNotification() (interface{}, error) {
|
||||||
if job.mq == nil {
|
if job.mq == nil {
|
||||||
return nil, ErrNotRegistered
|
return nil, ErrNotRegistered
|
||||||
}
|
}
|
||||||
return job.mq.ReadOrWait()
|
return job.mq.Dequeue()
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateProcThreadAttribute updates the passed in ProcThreadAttributeList to contain what is necessary to
|
// UpdateProcThreadAttribute updates the passed in ProcThreadAttributeList to contain what is necessary to
|
||||||
|
@ -330,7 +339,7 @@ func (job *JobObject) Pids() ([]uint32, error) {
|
||||||
err := winapi.QueryInformationJobObject(
|
err := winapi.QueryInformationJobObject(
|
||||||
job.handle,
|
job.handle,
|
||||||
winapi.JobObjectBasicProcessIdList,
|
winapi.JobObjectBasicProcessIdList,
|
||||||
uintptr(unsafe.Pointer(&info)),
|
unsafe.Pointer(&info),
|
||||||
uint32(unsafe.Sizeof(info)),
|
uint32(unsafe.Sizeof(info)),
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
@ -356,7 +365,7 @@ func (job *JobObject) Pids() ([]uint32, error) {
|
||||||
if err = winapi.QueryInformationJobObject(
|
if err = winapi.QueryInformationJobObject(
|
||||||
job.handle,
|
job.handle,
|
||||||
winapi.JobObjectBasicProcessIdList,
|
winapi.JobObjectBasicProcessIdList,
|
||||||
uintptr(unsafe.Pointer(&buf[0])),
|
unsafe.Pointer(&buf[0]),
|
||||||
uint32(len(buf)),
|
uint32(len(buf)),
|
||||||
nil,
|
nil,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
@ -384,7 +393,7 @@ func (job *JobObject) QueryMemoryStats() (*winapi.JOBOBJECT_MEMORY_USAGE_INFORMA
|
||||||
if err := winapi.QueryInformationJobObject(
|
if err := winapi.QueryInformationJobObject(
|
||||||
job.handle,
|
job.handle,
|
||||||
winapi.JobObjectMemoryUsageInformation,
|
winapi.JobObjectMemoryUsageInformation,
|
||||||
uintptr(unsafe.Pointer(&info)),
|
unsafe.Pointer(&info),
|
||||||
uint32(unsafe.Sizeof(info)),
|
uint32(unsafe.Sizeof(info)),
|
||||||
nil,
|
nil,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
@ -406,7 +415,7 @@ func (job *JobObject) QueryProcessorStats() (*winapi.JOBOBJECT_BASIC_ACCOUNTING_
|
||||||
if err := winapi.QueryInformationJobObject(
|
if err := winapi.QueryInformationJobObject(
|
||||||
job.handle,
|
job.handle,
|
||||||
winapi.JobObjectBasicAccountingInformation,
|
winapi.JobObjectBasicAccountingInformation,
|
||||||
uintptr(unsafe.Pointer(&info)),
|
unsafe.Pointer(&info),
|
||||||
uint32(unsafe.Sizeof(info)),
|
uint32(unsafe.Sizeof(info)),
|
||||||
nil,
|
nil,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
@ -415,7 +424,9 @@ func (job *JobObject) QueryProcessorStats() (*winapi.JOBOBJECT_BASIC_ACCOUNTING_
|
||||||
return &info, nil
|
return &info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryStorageStats gets the storage (I/O) stats for the job object.
|
// QueryStorageStats gets the storage (I/O) stats for the job object. This call will error
|
||||||
|
// if either `EnableIOTracking` wasn't set to true on creation of the job, or SetIOTracking()
|
||||||
|
// hasn't been called since creation of the job.
|
||||||
func (job *JobObject) QueryStorageStats() (*winapi.JOBOBJECT_IO_ATTRIBUTION_INFORMATION, error) {
|
func (job *JobObject) QueryStorageStats() (*winapi.JOBOBJECT_IO_ATTRIBUTION_INFORMATION, error) {
|
||||||
job.handleLock.RLock()
|
job.handleLock.RLock()
|
||||||
defer job.handleLock.RUnlock()
|
defer job.handleLock.RUnlock()
|
||||||
|
@ -430,7 +441,7 @@ func (job *JobObject) QueryStorageStats() (*winapi.JOBOBJECT_IO_ATTRIBUTION_INFO
|
||||||
if err := winapi.QueryInformationJobObject(
|
if err := winapi.QueryInformationJobObject(
|
||||||
job.handle,
|
job.handle,
|
||||||
winapi.JobObjectIoAttribution,
|
winapi.JobObjectIoAttribution,
|
||||||
uintptr(unsafe.Pointer(&info)),
|
unsafe.Pointer(&info),
|
||||||
uint32(unsafe.Sizeof(info)),
|
uint32(unsafe.Sizeof(info)),
|
||||||
nil,
|
nil,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
@ -476,7 +487,7 @@ func (job *JobObject) QueryPrivateWorkingSet() (uint64, error) {
|
||||||
status := winapi.NtQueryInformationProcess(
|
status := winapi.NtQueryInformationProcess(
|
||||||
h,
|
h,
|
||||||
winapi.ProcessVmCounters,
|
winapi.ProcessVmCounters,
|
||||||
uintptr(unsafe.Pointer(&vmCounters)),
|
unsafe.Pointer(&vmCounters),
|
||||||
uint32(unsafe.Sizeof(vmCounters)),
|
uint32(unsafe.Sizeof(vmCounters)),
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
@ -497,3 +508,31 @@ func (job *JobObject) QueryPrivateWorkingSet() (uint64, error) {
|
||||||
|
|
||||||
return jobWorkingSetSize, nil
|
return jobWorkingSetSize, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetIOTracking enables IO tracking for processes in the job object.
|
||||||
|
// This enables use of the QueryStorageStats method.
|
||||||
|
func (job *JobObject) SetIOTracking() error {
|
||||||
|
job.handleLock.RLock()
|
||||||
|
defer job.handleLock.RUnlock()
|
||||||
|
|
||||||
|
if job.handle == 0 {
|
||||||
|
return ErrAlreadyClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
return enableIOTracking(job.handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func enableIOTracking(job windows.Handle) error {
|
||||||
|
info := winapi.JOBOBJECT_IO_ATTRIBUTION_INFORMATION{
|
||||||
|
ControlFlags: winapi.JOBOBJECT_IO_ATTRIBUTION_CONTROL_ENABLE,
|
||||||
|
}
|
||||||
|
if _, err := windows.SetInformationJobObject(
|
||||||
|
job,
|
||||||
|
winapi.JobObjectIoAttribution,
|
||||||
|
uintptr(unsafe.Pointer(&info)),
|
||||||
|
uint32(unsafe.Sizeof(info)),
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable IO tracking on job object: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -202,7 +202,7 @@ func (job *JobObject) getExtendedInformation() (*windows.JOBOBJECT_EXTENDED_LIMI
|
||||||
if err := winapi.QueryInformationJobObject(
|
if err := winapi.QueryInformationJobObject(
|
||||||
job.handle,
|
job.handle,
|
||||||
windows.JobObjectExtendedLimitInformation,
|
windows.JobObjectExtendedLimitInformation,
|
||||||
uintptr(unsafe.Pointer(&info)),
|
unsafe.Pointer(&info),
|
||||||
uint32(unsafe.Sizeof(info)),
|
uint32(unsafe.Sizeof(info)),
|
||||||
nil,
|
nil,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
@ -224,7 +224,7 @@ func (job *JobObject) getCPURateControlInformation() (*winapi.JOBOBJECT_CPU_RATE
|
||||||
if err := winapi.QueryInformationJobObject(
|
if err := winapi.QueryInformationJobObject(
|
||||||
job.handle,
|
job.handle,
|
||||||
windows.JobObjectCpuRateControlInformation,
|
windows.JobObjectCpuRateControlInformation,
|
||||||
uintptr(unsafe.Pointer(&info)),
|
unsafe.Pointer(&info),
|
||||||
uint32(unsafe.Sizeof(info)),
|
uint32(unsafe.Sizeof(info)),
|
||||||
nil,
|
nil,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
|
@ -5,10 +5,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var ErrQueueClosed = errors.New("the queue is closed for reading and writing")
|
||||||
ErrQueueClosed = errors.New("the queue is closed for reading and writing")
|
|
||||||
ErrQueueEmpty = errors.New("the queue is empty")
|
|
||||||
)
|
|
||||||
|
|
||||||
// MessageQueue represents a threadsafe message queue to be used to retrieve or
|
// MessageQueue represents a threadsafe message queue to be used to retrieve or
|
||||||
// write messages to.
|
// write messages to.
|
||||||
|
@ -29,8 +26,8 @@ func NewMessageQueue() *MessageQueue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write writes `msg` to the queue.
|
// Enqueue writes `msg` to the queue.
|
||||||
func (mq *MessageQueue) Write(msg interface{}) error {
|
func (mq *MessageQueue) Enqueue(msg interface{}) error {
|
||||||
mq.m.Lock()
|
mq.m.Lock()
|
||||||
defer mq.m.Unlock()
|
defer mq.m.Unlock()
|
||||||
|
|
||||||
|
@ -43,55 +40,37 @@ func (mq *MessageQueue) Write(msg interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read will read a value from the queue if available, otherwise return an error.
|
// Dequeue will read a value from the queue and remove it. If the queue
|
||||||
func (mq *MessageQueue) Read() (interface{}, error) {
|
// is empty, this will block until the queue is closed or a value gets enqueued.
|
||||||
|
func (mq *MessageQueue) Dequeue() (interface{}, error) {
|
||||||
mq.m.Lock()
|
mq.m.Lock()
|
||||||
defer mq.m.Unlock()
|
defer mq.m.Unlock()
|
||||||
|
|
||||||
|
for !mq.closed && mq.size() == 0 {
|
||||||
|
mq.c.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// We got woken up, check if it's because the queue got closed.
|
||||||
if mq.closed {
|
if mq.closed {
|
||||||
return nil, ErrQueueClosed
|
return nil, ErrQueueClosed
|
||||||
}
|
}
|
||||||
if mq.isEmpty() {
|
|
||||||
return nil, ErrQueueEmpty
|
|
||||||
}
|
|
||||||
val := mq.messages[0]
|
val := mq.messages[0]
|
||||||
mq.messages[0] = nil
|
mq.messages[0] = nil
|
||||||
mq.messages = mq.messages[1:]
|
mq.messages = mq.messages[1:]
|
||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadOrWait will read a value from the queue if available, else it will wait for a
|
// Size returns the size of the queue.
|
||||||
// value to become available. This will block forever if nothing gets written or until
|
func (mq *MessageQueue) Size() int {
|
||||||
// the queue gets closed.
|
|
||||||
func (mq *MessageQueue) ReadOrWait() (interface{}, error) {
|
|
||||||
mq.m.Lock()
|
|
||||||
if mq.closed {
|
|
||||||
mq.m.Unlock()
|
|
||||||
return nil, ErrQueueClosed
|
|
||||||
}
|
|
||||||
if mq.isEmpty() {
|
|
||||||
for !mq.closed && mq.isEmpty() {
|
|
||||||
mq.c.Wait()
|
|
||||||
}
|
|
||||||
mq.m.Unlock()
|
|
||||||
return mq.Read()
|
|
||||||
}
|
|
||||||
val := mq.messages[0]
|
|
||||||
mq.messages[0] = nil
|
|
||||||
mq.messages = mq.messages[1:]
|
|
||||||
mq.m.Unlock()
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEmpty returns if the queue is empty
|
|
||||||
func (mq *MessageQueue) IsEmpty() bool {
|
|
||||||
mq.m.RLock()
|
mq.m.RLock()
|
||||||
defer mq.m.RUnlock()
|
defer mq.m.RUnlock()
|
||||||
return len(mq.messages) == 0
|
return mq.size()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nonexported empty check that doesn't lock so we can call this in Read and Write.
|
// Nonexported size check to check if the queue is empty inside already locked functions.
|
||||||
func (mq *MessageQueue) isEmpty() bool {
|
func (mq *MessageQueue) size() int {
|
||||||
return len(mq.messages) == 0
|
return len(mq.messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the queue for future writes or reads. Any attempts to read or write from the
|
// Close closes the queue for future writes or reads. Any attempts to read or write from the
|
||||||
|
@ -99,13 +78,15 @@ func (mq *MessageQueue) isEmpty() bool {
|
||||||
func (mq *MessageQueue) Close() {
|
func (mq *MessageQueue) Close() {
|
||||||
mq.m.Lock()
|
mq.m.Lock()
|
||||||
defer mq.m.Unlock()
|
defer mq.m.Unlock()
|
||||||
// Already closed
|
|
||||||
|
// Already closed, noop
|
||||||
if mq.closed {
|
if mq.closed {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mq.messages = nil
|
mq.messages = nil
|
||||||
mq.closed = true
|
mq.closed = true
|
||||||
// If there's anybody currently waiting on a value from ReadOrWait, we need to
|
// If there's anybody currently waiting on a value from Dequeue, we need to
|
||||||
// broadcast so the read(s) can return ErrQueueClosed.
|
// broadcast so the read(s) can return ErrQueueClosed.
|
||||||
mq.c.Broadcast()
|
mq.c.Broadcast()
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,7 +175,7 @@ type JOBOBJECT_ASSOCIATE_COMPLETION_PORT struct {
|
||||||
// LPDWORD lpReturnLength
|
// LPDWORD lpReturnLength
|
||||||
// );
|
// );
|
||||||
//
|
//
|
||||||
//sys QueryInformationJobObject(jobHandle windows.Handle, infoClass uint32, jobObjectInfo uintptr, jobObjectInformationLength uint32, lpReturnLength *uint32) (err error) = kernel32.QueryInformationJobObject
|
//sys QueryInformationJobObject(jobHandle windows.Handle, infoClass uint32, jobObjectInfo unsafe.Pointer, jobObjectInformationLength uint32, lpReturnLength *uint32) (err error) = kernel32.QueryInformationJobObject
|
||||||
|
|
||||||
// HANDLE OpenJobObjectW(
|
// HANDLE OpenJobObjectW(
|
||||||
// DWORD dwDesiredAccess,
|
// DWORD dwDesiredAccess,
|
||||||
|
|
|
@ -18,7 +18,7 @@ const ProcessVmCounters = 3
|
||||||
// [out, optional] PULONG ReturnLength
|
// [out, optional] PULONG ReturnLength
|
||||||
// );
|
// );
|
||||||
//
|
//
|
||||||
//sys NtQueryInformationProcess(processHandle windows.Handle, processInfoClass uint32, processInfo uintptr, processInfoLength uint32, returnLength *uint32) (status uint32) = ntdll.NtQueryInformationProcess
|
//sys NtQueryInformationProcess(processHandle windows.Handle, processInfoClass uint32, processInfo unsafe.Pointer, processInfoLength uint32, returnLength *uint32) (status uint32) = ntdll.NtQueryInformationProcess
|
||||||
|
|
||||||
// typedef struct _VM_COUNTERS_EX
|
// typedef struct _VM_COUNTERS_EX
|
||||||
// {
|
// {
|
||||||
|
|
|
@ -12,7 +12,8 @@ const STATUS_INFO_LENGTH_MISMATCH = 0xC0000004
|
||||||
// ULONG SystemInformationLength,
|
// ULONG SystemInformationLength,
|
||||||
// PULONG ReturnLength
|
// PULONG ReturnLength
|
||||||
// );
|
// );
|
||||||
//sys NtQuerySystemInformation(systemInfoClass int, systemInformation uintptr, systemInfoLength uint32, returnLength *uint32) (status uint32) = ntdll.NtQuerySystemInformation
|
//
|
||||||
|
//sys NtQuerySystemInformation(systemInfoClass int, systemInformation unsafe.Pointer, systemInfoLength uint32, returnLength *uint32) (status uint32) = ntdll.NtQuerySystemInformation
|
||||||
|
|
||||||
type SYSTEM_PROCESS_INFORMATION struct {
|
type SYSTEM_PROCESS_INFORMATION struct {
|
||||||
NextEntryOffset uint32 // ULONG
|
NextEntryOffset uint32 // ULONG
|
||||||
|
|
|
@ -100,7 +100,7 @@ func resizePseudoConsole(hPc windows.Handle, size uint32) (hr error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func NtQuerySystemInformation(systemInfoClass int, systemInformation uintptr, systemInfoLength uint32, returnLength *uint32) (status uint32) {
|
func NtQuerySystemInformation(systemInfoClass int, systemInformation unsafe.Pointer, systemInfoLength uint32, returnLength *uint32) (status uint32) {
|
||||||
r0, _, _ := syscall.Syscall6(procNtQuerySystemInformation.Addr(), 4, uintptr(systemInfoClass), uintptr(systemInformation), uintptr(systemInfoLength), uintptr(unsafe.Pointer(returnLength)), 0, 0)
|
r0, _, _ := syscall.Syscall6(procNtQuerySystemInformation.Addr(), 4, uintptr(systemInfoClass), uintptr(systemInformation), uintptr(systemInfoLength), uintptr(unsafe.Pointer(returnLength)), 0, 0)
|
||||||
status = uint32(r0)
|
status = uint32(r0)
|
||||||
return
|
return
|
||||||
|
@ -152,7 +152,7 @@ func IsProcessInJob(procHandle windows.Handle, jobHandle windows.Handle, result
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func QueryInformationJobObject(jobHandle windows.Handle, infoClass uint32, jobObjectInfo uintptr, jobObjectInformationLength uint32, lpReturnLength *uint32) (err error) {
|
func QueryInformationJobObject(jobHandle windows.Handle, infoClass uint32, jobObjectInfo unsafe.Pointer, jobObjectInformationLength uint32, lpReturnLength *uint32) (err error) {
|
||||||
r1, _, e1 := syscall.Syscall6(procQueryInformationJobObject.Addr(), 5, uintptr(jobHandle), uintptr(infoClass), uintptr(jobObjectInfo), uintptr(jobObjectInformationLength), uintptr(unsafe.Pointer(lpReturnLength)), 0)
|
r1, _, e1 := syscall.Syscall6(procQueryInformationJobObject.Addr(), 5, uintptr(jobHandle), uintptr(infoClass), uintptr(jobObjectInfo), uintptr(jobObjectInformationLength), uintptr(unsafe.Pointer(lpReturnLength)), 0)
|
||||||
if r1 == 0 {
|
if r1 == 0 {
|
||||||
if e1 != 0 {
|
if e1 != 0 {
|
||||||
|
@ -244,7 +244,7 @@ func LocalFree(ptr uintptr) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func NtQueryInformationProcess(processHandle windows.Handle, processInfoClass uint32, processInfo uintptr, processInfoLength uint32, returnLength *uint32) (status uint32) {
|
func NtQueryInformationProcess(processHandle windows.Handle, processInfoClass uint32, processInfo unsafe.Pointer, processInfoLength uint32, returnLength *uint32) (status uint32) {
|
||||||
r0, _, _ := syscall.Syscall6(procNtQueryInformationProcess.Addr(), 5, uintptr(processHandle), uintptr(processInfoClass), uintptr(processInfo), uintptr(processInfoLength), uintptr(unsafe.Pointer(returnLength)), 0)
|
r0, _, _ := syscall.Syscall6(procNtQueryInformationProcess.Addr(), 5, uintptr(processHandle), uintptr(processInfoClass), uintptr(processInfo), uintptr(processInfoLength), uintptr(unsafe.Pointer(returnLength)), 0)
|
||||||
status = uint32(r0)
|
status = uint32(r0)
|
||||||
return
|
return
|
||||||
|
|
|
@ -190,7 +190,7 @@ func (i *Image) Inspect(ctx context.Context, options *InspectOptions) (*ImageDat
|
||||||
// NOTE: Health checks may be listed in the container config or
|
// NOTE: Health checks may be listed in the container config or
|
||||||
// the config.
|
// the config.
|
||||||
data.HealthCheck = dockerManifest.ContainerConfig.Healthcheck
|
data.HealthCheck = dockerManifest.ContainerConfig.Healthcheck
|
||||||
if data.HealthCheck == nil {
|
if data.HealthCheck == nil && dockerManifest.Config != nil {
|
||||||
data.HealthCheck = dockerManifest.Config.Healthcheck
|
data.HealthCheck = dockerManifest.Config.Healthcheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,7 +99,7 @@ func (r *Runtime) Load(ctx context.Context, path string, options *LoadOptions) (
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadMultiImageDockerArchive loads the docker archive specified by ref. In
|
// loadMultiImageDockerArchive loads the docker archive specified by ref. In
|
||||||
// case the path@reference notation was used, only the specifiec image will be
|
// case the path@reference notation was used, only the specified image will be
|
||||||
// loaded. Otherwise, all images will be loaded.
|
// loaded. Otherwise, all images will be loaded.
|
||||||
func (r *Runtime) loadMultiImageDockerArchive(ctx context.Context, ref types.ImageReference, options *CopyOptions) ([]string, error) {
|
func (r *Runtime) loadMultiImageDockerArchive(ctx context.Context, ref types.ImageReference, options *CopyOptions) ([]string, error) {
|
||||||
// If we cannot stat the path, it either does not exist OR the correct
|
// If we cannot stat the path, it either does not exist OR the correct
|
||||||
|
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"github.com/containers/common/pkg/config"
|
"github.com/containers/common/pkg/config"
|
||||||
"github.com/containers/storage/pkg/lockfile"
|
"github.com/containers/storage/pkg/lockfile"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
type cniNetwork struct {
|
type cniNetwork struct {
|
||||||
|
@ -62,6 +63,8 @@ type InitConfig struct {
|
||||||
CNIConfigDir string
|
CNIConfigDir string
|
||||||
// CNIPluginDirs is a list of directories where cni should look for the plugins.
|
// CNIPluginDirs is a list of directories where cni should look for the plugins.
|
||||||
CNIPluginDirs []string
|
CNIPluginDirs []string
|
||||||
|
// RunDir is a directory where temporary files can be stored.
|
||||||
|
RunDir string
|
||||||
|
|
||||||
// DefaultNetwork is the name for the default network.
|
// DefaultNetwork is the name for the default network.
|
||||||
DefaultNetwork string
|
DefaultNetwork string
|
||||||
|
@ -81,7 +84,16 @@ func NewCNINetworkInterface(conf *InitConfig) (types.ContainerNetwork, error) {
|
||||||
// TODO: consider using a shared memory lock
|
// TODO: consider using a shared memory lock
|
||||||
lock, err := lockfile.GetLockfile(filepath.Join(conf.CNIConfigDir, "cni.lock"))
|
lock, err := lockfile.GetLockfile(filepath.Join(conf.CNIConfigDir, "cni.lock"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
// If we're on a read-only filesystem, there is no risk of
|
||||||
|
// contention. Fall back to a local lockfile.
|
||||||
|
if errors.Is(err, unix.EROFS) {
|
||||||
|
lock, err = lockfile.GetLockfile(filepath.Join(conf.RunDir, "cni.lock"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultNetworkName := conf.DefaultNetwork
|
defaultNetworkName := conf.DefaultNetwork
|
||||||
|
|
|
@ -169,6 +169,7 @@ func getCniInterface(conf *config.Config) (types.ContainerNetwork, error) {
|
||||||
return cni.NewCNINetworkInterface(&cni.InitConfig{
|
return cni.NewCNINetworkInterface(&cni.InitConfig{
|
||||||
CNIConfigDir: confDir,
|
CNIConfigDir: confDir,
|
||||||
CNIPluginDirs: conf.Network.CNIPluginDirs,
|
CNIPluginDirs: conf.Network.CNIPluginDirs,
|
||||||
|
RunDir: conf.Engine.TmpDir,
|
||||||
DefaultNetwork: conf.Network.DefaultNetwork,
|
DefaultNetwork: conf.Network.DefaultNetwork,
|
||||||
DefaultSubnet: conf.Network.DefaultSubnet,
|
DefaultSubnet: conf.Network.DefaultSubnet,
|
||||||
DefaultsubnetPools: conf.Network.DefaultSubnetPools,
|
DefaultsubnetPools: conf.Network.DefaultSubnetPools,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -27,6 +28,8 @@ const (
|
||||||
_configPath = "containers/containers.conf"
|
_configPath = "containers/containers.conf"
|
||||||
// UserOverrideContainersConfig holds the containers config path overridden by the rootless user
|
// UserOverrideContainersConfig holds the containers config path overridden by the rootless user
|
||||||
UserOverrideContainersConfig = ".config/" + _configPath
|
UserOverrideContainersConfig = ".config/" + _configPath
|
||||||
|
// Token prefix for looking for helper binary under $BINDIR
|
||||||
|
bindirPrefix = "$BINDIR"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RuntimeStateStore is a constant indicating which state store implementation
|
// RuntimeStateStore is a constant indicating which state store implementation
|
||||||
|
@ -454,6 +457,13 @@ type EngineConfig struct {
|
||||||
// may not be by other drivers.
|
// may not be by other drivers.
|
||||||
VolumePath string `toml:"volume_path,omitempty"`
|
VolumePath string `toml:"volume_path,omitempty"`
|
||||||
|
|
||||||
|
// VolumePluginTimeout sets the default timeout, in seconds, for
|
||||||
|
// operations that must contact a volume plugin. Plugins are external
|
||||||
|
// programs accessed via REST API; this sets a timeout for requests to
|
||||||
|
// that API.
|
||||||
|
// A value of 0 is treated as no timeout.
|
||||||
|
VolumePluginTimeout uint `toml:"volume_plugin_timeout,omitempty,omitzero"`
|
||||||
|
|
||||||
// VolumePlugins is a set of plugins that can be used as the backend for
|
// VolumePlugins is a set of plugins that can be used as the backend for
|
||||||
// Podman named volumes. Each volume is specified as a name (what Podman
|
// Podman named volumes. Each volume is specified as a name (what Podman
|
||||||
// will refer to the plugin as) mapped to a path, which must point to a
|
// will refer to the plugin as) mapped to a path, which must point to a
|
||||||
|
@ -815,6 +825,18 @@ func (c *Config) Validate() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// URI returns the URI Path to the machine image
|
||||||
|
func (m *MachineConfig) URI() string {
|
||||||
|
uri := m.Image
|
||||||
|
for _, val := range []string{"$ARCH", "$arch"} {
|
||||||
|
uri = strings.Replace(uri, val, runtime.GOARCH, 1)
|
||||||
|
}
|
||||||
|
for _, val := range []string{"$OS", "$os"} {
|
||||||
|
uri = strings.Replace(uri, val, runtime.GOOS, 1)
|
||||||
|
}
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
|
||||||
func (c *EngineConfig) findRuntime() string {
|
func (c *EngineConfig) findRuntime() string {
|
||||||
// Search for crun first followed by runc, kata, runsc
|
// Search for crun first followed by runc, kata, runsc
|
||||||
for _, name := range []string{"crun", "runc", "runj", "kata", "runsc"} {
|
for _, name := range []string{"crun", "runc", "runj", "kata", "runsc"} {
|
||||||
|
@ -1241,10 +1263,37 @@ func (c *Config) ActiveDestination() (uri, identity string, err error) {
|
||||||
return "", "", errors.New("no service destination configured")
|
return "", "", errors.New("no service destination configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
bindirFailed = false
|
||||||
|
bindirCached = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
func findBindir() string {
|
||||||
|
if bindirCached != "" || bindirFailed {
|
||||||
|
return bindirCached
|
||||||
|
}
|
||||||
|
execPath, err := os.Executable()
|
||||||
|
if err == nil {
|
||||||
|
// Resolve symbolic links to find the actual binary file path.
|
||||||
|
execPath, err = filepath.EvalSymlinks(execPath)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// If failed to find executable (unlikely to happen), warn about it.
|
||||||
|
// The bindirFailed flag will track this, so we only warn once.
|
||||||
|
logrus.Warnf("Failed to find $BINDIR: %v", err)
|
||||||
|
bindirFailed = true
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
bindirCached = filepath.Dir(execPath)
|
||||||
|
return bindirCached
|
||||||
|
}
|
||||||
|
|
||||||
// FindHelperBinary will search the given binary name in the configured directories.
|
// FindHelperBinary will search the given binary name in the configured directories.
|
||||||
// If searchPATH is set to true it will also search in $PATH.
|
// If searchPATH is set to true it will also search in $PATH.
|
||||||
func (c *Config) FindHelperBinary(name string, searchPATH bool) (string, error) {
|
func (c *Config) FindHelperBinary(name string, searchPATH bool) (string, error) {
|
||||||
dirList := c.Engine.HelperBinariesDir
|
dirList := c.Engine.HelperBinariesDir
|
||||||
|
bindirPath := ""
|
||||||
|
bindirSearched := false
|
||||||
|
|
||||||
// If set, search this directory first. This is used in testing.
|
// If set, search this directory first. This is used in testing.
|
||||||
if dir, found := os.LookupEnv("CONTAINERS_HELPER_BINARY_DIR"); found {
|
if dir, found := os.LookupEnv("CONTAINERS_HELPER_BINARY_DIR"); found {
|
||||||
|
@ -1252,6 +1301,24 @@ func (c *Config) FindHelperBinary(name string, searchPATH bool) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, path := range dirList {
|
for _, path := range dirList {
|
||||||
|
if path == bindirPrefix || strings.HasPrefix(path, bindirPrefix+string(filepath.Separator)) {
|
||||||
|
// Calculate the path to the executable first time we encounter a $BINDIR prefix.
|
||||||
|
if !bindirSearched {
|
||||||
|
bindirSearched = true
|
||||||
|
bindirPath = findBindir()
|
||||||
|
}
|
||||||
|
// If there's an error, don't stop the search for the helper binary.
|
||||||
|
// findBindir() will have warned once during the first failure.
|
||||||
|
if bindirPath == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Replace the $BINDIR prefix with the path to the directory of the current binary.
|
||||||
|
if path == bindirPrefix {
|
||||||
|
path = bindirPath
|
||||||
|
} else {
|
||||||
|
path = filepath.Join(bindirPath, strings.TrimPrefix(path, bindirPrefix+string(filepath.Separator)))
|
||||||
|
}
|
||||||
|
}
|
||||||
fullpath := filepath.Join(path, name)
|
fullpath := filepath.Join(path, name)
|
||||||
if fi, err := os.Stat(fullpath); err == nil && fi.Mode().IsRegular() {
|
if fi, err := os.Stat(fullpath); err == nil && fi.Mode().IsRegular() {
|
||||||
return fullpath, nil
|
return fullpath, nil
|
||||||
|
|
|
@ -35,4 +35,6 @@ var defaultHelperBinariesDir = []string{
|
||||||
"/usr/local/lib/podman",
|
"/usr/local/lib/podman",
|
||||||
"/usr/libexec/podman",
|
"/usr/libexec/podman",
|
||||||
"/usr/lib/podman",
|
"/usr/lib/podman",
|
||||||
|
// Relative to the binary directory
|
||||||
|
"$BINDIR/../libexec/podman",
|
||||||
}
|
}
|
||||||
|
|
|
@ -605,6 +605,12 @@ default_sysctls = [
|
||||||
#
|
#
|
||||||
#volume_path = "/var/lib/containers/storage/volumes"
|
#volume_path = "/var/lib/containers/storage/volumes"
|
||||||
|
|
||||||
|
# Default timeout (in seconds) for volume plugin operations.
|
||||||
|
# Plugins are external programs accessed via a REST API; this sets a timeout
|
||||||
|
# for requests to that API.
|
||||||
|
# A value of 0 is treated as no timeout.
|
||||||
|
#volume_plugin_timeout = 5
|
||||||
|
|
||||||
# Paths to look for a valid OCI runtime (crun, runc, kata, runsc, krun, etc)
|
# Paths to look for a valid OCI runtime (crun, runc, kata, runsc, krun, etc)
|
||||||
[engine.runtimes]
|
[engine.runtimes]
|
||||||
#crun = [
|
#crun = [
|
||||||
|
@ -665,9 +671,16 @@ default_sysctls = [
|
||||||
#
|
#
|
||||||
#disk_size=10
|
#disk_size=10
|
||||||
|
|
||||||
# The image used when creating a podman-machine VM.
|
# Default image URI when creating a new VM using `podman machine init`.
|
||||||
|
# Options: On Linux/Mac, `testing`, `stable`, `next`. On Windows, the major
|
||||||
|
# version of the OS (e.g `36`) for Fedora 36. For all platforms you can
|
||||||
|
# alternatively specify a custom download URL to an image. Container engines
|
||||||
|
# translate URIs $OS and $ARCH to the native OS and ARCH. URI
|
||||||
|
# "https://example.com/$OS/$ARCH/foobar.ami" becomes
|
||||||
|
# "https://example.com/linux/amd64/foobar.ami" on a Linux AMD machine.
|
||||||
|
# The default value is `testing`.
|
||||||
#
|
#
|
||||||
#image = "testing"
|
# image = "testing"
|
||||||
|
|
||||||
# Memory in MB a machine is created with.
|
# Memory in MB a machine is created with.
|
||||||
#
|
#
|
||||||
|
|
|
@ -168,6 +168,8 @@ const (
|
||||||
SeccompOverridePath = _etcDir + "/containers/seccomp.json"
|
SeccompOverridePath = _etcDir + "/containers/seccomp.json"
|
||||||
// SeccompDefaultPath defines the default seccomp path.
|
// SeccompDefaultPath defines the default seccomp path.
|
||||||
SeccompDefaultPath = _installPrefix + "/share/containers/seccomp.json"
|
SeccompDefaultPath = _installPrefix + "/share/containers/seccomp.json"
|
||||||
|
// DefaultVolumePluginTimeout is the default volume plugin timeout, in seconds
|
||||||
|
DefaultVolumePluginTimeout = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultConfig defines the default values from containers.conf.
|
// DefaultConfig defines the default values from containers.conf.
|
||||||
|
@ -264,7 +266,7 @@ func defaultMachineConfig() MachineConfig {
|
||||||
Image: getDefaultMachineImage(),
|
Image: getDefaultMachineImage(),
|
||||||
Memory: 2048,
|
Memory: 2048,
|
||||||
User: getDefaultMachineUser(),
|
User: getDefaultMachineUser(),
|
||||||
Volumes: []string{"$HOME:$HOME"},
|
Volumes: getDefaultMachineVolumes(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,6 +306,8 @@ func defaultConfigFromMemory() (*EngineConfig, error) {
|
||||||
c.StaticDir = filepath.Join(storeOpts.GraphRoot, "libpod")
|
c.StaticDir = filepath.Join(storeOpts.GraphRoot, "libpod")
|
||||||
c.VolumePath = filepath.Join(storeOpts.GraphRoot, "volumes")
|
c.VolumePath = filepath.Join(storeOpts.GraphRoot, "volumes")
|
||||||
|
|
||||||
|
c.VolumePluginTimeout = DefaultVolumePluginTimeout
|
||||||
|
|
||||||
c.HelperBinariesDir = defaultHelperBinariesDir
|
c.HelperBinariesDir = defaultHelperBinariesDir
|
||||||
if additionalHelperBinariesDir != "" {
|
if additionalHelperBinariesDir != "" {
|
||||||
c.HelperBinariesDir = append(c.HelperBinariesDir, additionalHelperBinariesDir)
|
c.HelperBinariesDir = append(c.HelperBinariesDir, additionalHelperBinariesDir)
|
||||||
|
|
|
@ -11,3 +11,8 @@ func getDefaultLockType() string {
|
||||||
func getLibpodTmpDir() string {
|
func getLibpodTmpDir() string {
|
||||||
return "/run/libpod"
|
return "/run/libpod"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDefaultMachineVolumes returns default mounted volumes (possibly with env vars, which will be expanded)
|
||||||
|
func getDefaultMachineVolumes() []string {
|
||||||
|
return []string{"$HOME:$HOME"}
|
||||||
|
}
|
||||||
|
|
|
@ -18,3 +18,8 @@ func getDefaultLockType() string {
|
||||||
func getLibpodTmpDir() string {
|
func getLibpodTmpDir() string {
|
||||||
return "/var/run/libpod"
|
return "/var/run/libpod"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDefaultMachineVolumes returns default mounted volumes (possibly with env vars, which will be expanded)
|
||||||
|
func getDefaultMachineVolumes() []string {
|
||||||
|
return []string{"$HOME:$HOME"}
|
||||||
|
}
|
||||||
|
|
|
@ -70,3 +70,8 @@ func getDefaultLockType() string {
|
||||||
func getLibpodTmpDir() string {
|
func getLibpodTmpDir() string {
|
||||||
return "/run/libpod"
|
return "/run/libpod"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDefaultMachineVolumes returns default mounted volumes (possibly with env vars, which will be expanded)
|
||||||
|
func getDefaultMachineVolumes() []string {
|
||||||
|
return []string{"$HOME:$HOME"}
|
||||||
|
}
|
||||||
|
|
|
@ -44,3 +44,8 @@ func getDefaultLockType() string {
|
||||||
func getLibpodTmpDir() string {
|
func getLibpodTmpDir() string {
|
||||||
return "/run/libpod"
|
return "/run/libpod"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDefaultMachineVolumes returns default mounted volumes (possibly with env vars, which will be expanded)
|
||||||
|
func getDefaultMachineVolumes() []string {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
|
@ -372,7 +372,7 @@ func mountExists(mounts []rspec.Mount, dest string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveSymbolicLink resolves a possbile symlink path. If the path is a symlink, returns resolved
|
// resolveSymbolicLink resolves symlink paths. If the path is a symlink, returns resolved
|
||||||
// path; if not, returns the original path.
|
// path; if not, returns the original path.
|
||||||
func resolveSymbolicLink(path string) (string, error) {
|
func resolveSymbolicLink(path string) (string, error) {
|
||||||
info, err := os.Lstat(path)
|
info, err := os.Lstat(path)
|
||||||
|
|
|
@ -11,7 +11,7 @@ github.com/Microsoft/go-winio/backuptar
|
||||||
github.com/Microsoft/go-winio/pkg/guid
|
github.com/Microsoft/go-winio/pkg/guid
|
||||||
github.com/Microsoft/go-winio/pkg/security
|
github.com/Microsoft/go-winio/pkg/security
|
||||||
github.com/Microsoft/go-winio/vhd
|
github.com/Microsoft/go-winio/vhd
|
||||||
# github.com/Microsoft/hcsshim v0.9.3
|
# github.com/Microsoft/hcsshim v0.9.4
|
||||||
github.com/Microsoft/hcsshim
|
github.com/Microsoft/hcsshim
|
||||||
github.com/Microsoft/hcsshim/computestorage
|
github.com/Microsoft/hcsshim/computestorage
|
||||||
github.com/Microsoft/hcsshim/internal/cow
|
github.com/Microsoft/hcsshim/internal/cow
|
||||||
|
@ -67,7 +67,7 @@ github.com/container-orchestrated-devices/container-device-interface/pkg/cdi
|
||||||
github.com/container-orchestrated-devices/container-device-interface/specs-go
|
github.com/container-orchestrated-devices/container-device-interface/specs-go
|
||||||
# github.com/containerd/cgroups v1.0.3
|
# github.com/containerd/cgroups v1.0.3
|
||||||
github.com/containerd/cgroups/stats/v1
|
github.com/containerd/cgroups/stats/v1
|
||||||
# github.com/containerd/containerd v1.6.6
|
# github.com/containerd/containerd v1.6.8
|
||||||
github.com/containerd/containerd/errdefs
|
github.com/containerd/containerd/errdefs
|
||||||
github.com/containerd/containerd/log
|
github.com/containerd/containerd/log
|
||||||
github.com/containerd/containerd/pkg/userns
|
github.com/containerd/containerd/pkg/userns
|
||||||
|
@ -114,7 +114,7 @@ github.com/containers/buildah/pkg/rusage
|
||||||
github.com/containers/buildah/pkg/sshagent
|
github.com/containers/buildah/pkg/sshagent
|
||||||
github.com/containers/buildah/pkg/util
|
github.com/containers/buildah/pkg/util
|
||||||
github.com/containers/buildah/util
|
github.com/containers/buildah/util
|
||||||
# github.com/containers/common v0.49.2-0.20220817132854-f6679f170eca
|
# github.com/containers/common v0.49.2-0.20220823130605-72a7da3358ac
|
||||||
## explicit
|
## explicit
|
||||||
github.com/containers/common/libimage
|
github.com/containers/common/libimage
|
||||||
github.com/containers/common/libimage/define
|
github.com/containers/common/libimage/define
|
||||||
|
|
Loading…
Reference in New Issue