allow running dockerd
in an unprivileged user namespace (rootless mode)
Please refer to `docs/rootless.md`. TLDR: * Make sure `/etc/subuid` and `/etc/subgid` contain the entry for you * `dockerd-rootless.sh --experimental` * `docker -H unix://$XDG_RUNTIME_DIR/docker.sock run ...` Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
This commit is contained in:
parent
50e63adf30
commit
ec87479b7e
36 changed files with 638 additions and 68 deletions
|
@ -161,7 +161,12 @@ ENV INSTALL_BINARY_NAME=tini
|
|||
COPY hack/dockerfile/install/$INSTALL_BINARY_NAME.installer ./
|
||||
RUN PREFIX=/build ./install.sh $INSTALL_BINARY_NAME
|
||||
|
||||
|
||||
FROM base AS rootlesskit
|
||||
ENV INSTALL_BINARY_NAME=rootlesskit
|
||||
COPY hack/dockerfile/install/install.sh ./install.sh
|
||||
COPY hack/dockerfile/install/$INSTALL_BINARY_NAME.installer ./
|
||||
RUN PREFIX=/build/ ./install.sh $INSTALL_BINARY_NAME
|
||||
COPY ./contrib/dockerd-rootless.sh /build
|
||||
|
||||
# TODO: Some of this is only really needed for testing, it would be nice to split this up
|
||||
FROM runtime-dev AS dev
|
||||
|
@ -233,6 +238,7 @@ RUN cd /docker-py \
|
|||
&& pip install paramiko==2.4.2 \
|
||||
&& pip install yamllint==1.5.0 \
|
||||
&& pip install -r test-requirements.txt
|
||||
COPY --from=rootlesskit /build/ /usr/local/bin/
|
||||
|
||||
ENV PATH=/usr/local/cli:$PATH
|
||||
ENV DOCKER_BUILDTAGS apparmor seccomp selinux
|
||||
|
|
|
@ -13,6 +13,8 @@ var (
|
|||
)
|
||||
|
||||
// Dir returns the path to the configuration directory as specified by the DOCKER_CONFIG environment variable.
|
||||
// If DOCKER_CONFIG is unset, Dir returns ~/.docker .
|
||||
// Dir ignores XDG_CONFIG_HOME (same as the docker client).
|
||||
// TODO: this was copied from cli/config/configfile and should be removed once cmd/dockerd moves
|
||||
func Dir() string {
|
||||
return configDir
|
||||
|
|
|
@ -17,8 +17,20 @@ const (
|
|||
)
|
||||
|
||||
// installCommonConfigFlags adds flags to the pflag.FlagSet to configure the daemon
|
||||
func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
|
||||
func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) error {
|
||||
var maxConcurrentDownloads, maxConcurrentUploads int
|
||||
defaultPidFile, err := getDefaultPidFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defaultDataRoot, err := getDefaultDataRoot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defaultExecRoot, err := getDefaultExecRoot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
installRegistryServiceFlags(&conf.ServiceOptions, flags)
|
||||
|
||||
|
@ -80,6 +92,7 @@ func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
|
|||
|
||||
conf.MaxConcurrentDownloads = &maxConcurrentDownloads
|
||||
conf.MaxConcurrentUploads = &maxConcurrentUploads
|
||||
return nil
|
||||
}
|
||||
|
||||
func installRegistryServiceFlags(options *registry.ServiceOptions, flags *pflag.FlagSet) {
|
||||
|
|
|
@ -3,17 +3,48 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/daemon/config"
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/docker/docker/rootless"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultPidFile = "/var/run/docker.pid"
|
||||
defaultDataRoot = "/var/lib/docker"
|
||||
defaultExecRoot = "/var/run/docker"
|
||||
)
|
||||
func getDefaultPidFile() (string, error) {
|
||||
if !rootless.RunningWithNonRootUsername() {
|
||||
return "/var/run/docker.pid", nil
|
||||
}
|
||||
runtimeDir, err := homedir.GetRuntimeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(runtimeDir, "docker.pid"), nil
|
||||
}
|
||||
|
||||
func getDefaultDataRoot() (string, error) {
|
||||
if !rootless.RunningWithNonRootUsername() {
|
||||
return "/var/lib/docker", nil
|
||||
}
|
||||
dataHome, err := homedir.GetDataHome()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dataHome, "docker"), nil
|
||||
}
|
||||
|
||||
func getDefaultExecRoot() (string, error) {
|
||||
if !rootless.RunningWithNonRootUsername() {
|
||||
return "/var/run/docker", nil
|
||||
}
|
||||
runtimeDir, err := homedir.GetRuntimeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(runtimeDir, "docker"), nil
|
||||
}
|
||||
|
||||
// installUnixConfigFlags adds command-line options to the top-level flag parser for
|
||||
// the current process that are common across Unix platforms.
|
||||
|
|
|
@ -5,14 +5,17 @@ package main
|
|||
import (
|
||||
"github.com/docker/docker/daemon/config"
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/docker/docker/rootless"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// installConfigFlags adds flags to the pflag.FlagSet to configure the daemon
|
||||
func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
|
||||
func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) error {
|
||||
// First handle install flags which are consistent cross-platform
|
||||
installCommonConfigFlags(conf, flags)
|
||||
if err := installCommonConfigFlags(conf, flags); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Then install flags common to unix platforms
|
||||
installUnixConfigFlags(conf, flags)
|
||||
|
@ -46,5 +49,7 @@ func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
|
|||
flags.BoolVar(&conf.NoNewPrivileges, "no-new-privileges", false, "Set no-new-privileges by default for new containers")
|
||||
flags.StringVar(&conf.IpcMode, "default-ipc-mode", config.DefaultIpcMode, `Default mode for containers ipc ("shareable" | "private")`)
|
||||
flags.Var(&conf.NetworkConfig.DefaultAddressPools, "default-address-pool", "Default address pools for node specific local networks")
|
||||
|
||||
// Mostly users don't need to set this flag explicitly.
|
||||
flags.BoolVar(&conf.Rootless, "rootless", rootless.RunningWithNonRootUsername(), "Enable rootless mode (experimental)")
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -15,7 +15,8 @@ func TestDaemonParseShmSize(t *testing.T) {
|
|||
flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||
|
||||
conf := &config.Config{}
|
||||
installConfigFlags(conf, flags)
|
||||
err := installConfigFlags(conf, flags)
|
||||
assert.NilError(t, err)
|
||||
// By default `--default-shm-size=64M`
|
||||
assert.Check(t, is.Equal(int64(64*1024*1024), conf.ShmSize.Value()))
|
||||
assert.Check(t, flags.Set("default-shm-size", "128M"))
|
||||
|
|
|
@ -8,19 +8,28 @@ import (
|
|||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultPidFile string
|
||||
defaultDataRoot = filepath.Join(os.Getenv("programdata"), "docker")
|
||||
defaultExecRoot = filepath.Join(os.Getenv("programdata"), "docker", "exec-root")
|
||||
)
|
||||
func getDefaultPidFile() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func getDefaultDataRoot() (string, error) {
|
||||
return filepath.Join(os.Getenv("programdata"), "docker"), nil
|
||||
}
|
||||
|
||||
func getDefaultExecRoot() (string, error) {
|
||||
return filepath.Join(os.Getenv("programdata"), "docker", "exec-root"), nil
|
||||
}
|
||||
|
||||
// installConfigFlags adds flags to the pflag.FlagSet to configure the daemon
|
||||
func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
|
||||
func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) error {
|
||||
// First handle install flags which are consistent cross-platform
|
||||
installCommonConfigFlags(conf, flags)
|
||||
if err := installCommonConfigFlags(conf, flags); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Then platform-specific install flags.
|
||||
flags.StringVar(&conf.BridgeConfig.FixedCIDR, "fixed-cidr", "", "IPv4 subnet for fixed IPs")
|
||||
flags.StringVarP(&conf.BridgeConfig.Iface, "bridge", "b", "", "Attach containers to a virtual switch")
|
||||
flags.StringVarP(&conf.SocketGroup, "group", "G", "", "Users or groups that can access the named pipe")
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -40,12 +40,14 @@ import (
|
|||
"github.com/docker/docker/libcontainerd/supervisor"
|
||||
dopts "github.com/docker/docker/opts"
|
||||
"github.com/docker/docker/pkg/authorization"
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/pkg/pidfile"
|
||||
"github.com/docker/docker/pkg/plugingetter"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
"github.com/docker/docker/plugin"
|
||||
"github.com/docker/docker/rootless"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
swarmapi "github.com/docker/swarmkit/api"
|
||||
|
@ -97,6 +99,17 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
|
|||
|
||||
if cli.Config.Experimental {
|
||||
logrus.Warn("Running experimental build")
|
||||
if cli.Config.IsRootless() {
|
||||
logrus.Warn("Running in rootless mode. Cgroups, AppArmor, and CRIU are disabled.")
|
||||
}
|
||||
} else {
|
||||
if cli.Config.IsRootless() {
|
||||
return fmt.Errorf("rootless mode is supported only when running in experimental mode")
|
||||
}
|
||||
}
|
||||
// return human-friendly error before creating files
|
||||
if runtime.GOOS == "linux" && os.Geteuid() != 0 {
|
||||
return fmt.Errorf("dockerd needs to be started with root. To see how to run dockerd in rootless mode with unprivileged user, see the documentation")
|
||||
}
|
||||
|
||||
system.InitLCOW(cli.Config.Experimental)
|
||||
|
@ -115,11 +128,14 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
potentiallyUnderRuntimeDir := []string{cli.Config.ExecRoot}
|
||||
|
||||
if cli.Pidfile != "" {
|
||||
pf, err := pidfile.New(cli.Pidfile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to start daemon")
|
||||
}
|
||||
potentiallyUnderRuntimeDir = append(potentiallyUnderRuntimeDir, cli.Pidfile)
|
||||
defer func() {
|
||||
if err := pf.Remove(); err != nil {
|
||||
logrus.Error(err)
|
||||
|
@ -127,6 +143,12 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
|
|||
}()
|
||||
}
|
||||
|
||||
// Set sticky bit if XDG_RUNTIME_DIR is set && the file is actually under XDG_RUNTIME_DIR
|
||||
if _, err := homedir.StickRuntimeDirContents(potentiallyUnderRuntimeDir); err != nil {
|
||||
// StickRuntimeDirContents returns nil error if XDG_RUNTIME_DIR is just unset
|
||||
logrus.WithError(err).Warn("cannot set sticky bit on files under XDG_RUNTIME_DIR")
|
||||
}
|
||||
|
||||
serverConfig, err := newAPIServerConfig(cli)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create API server")
|
||||
|
@ -140,7 +162,11 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
|
|||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
if cli.Config.ContainerdAddr == "" && runtime.GOOS != "windows" {
|
||||
if !systemContainerdRunning() {
|
||||
systemContainerdAddr, ok, err := systemContainerdRunning(cli.Config.IsRootless())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not determine whether the system containerd is running")
|
||||
}
|
||||
if !ok {
|
||||
opts, err := cli.getContainerdDaemonOpts()
|
||||
if err != nil {
|
||||
cancel()
|
||||
|
@ -157,7 +183,7 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
|
|||
// Try to wait for containerd to shutdown
|
||||
defer r.WaitTimeout(10 * time.Second)
|
||||
} else {
|
||||
cli.Config.ContainerdAddr = containerddefaults.DefaultAddress
|
||||
cli.Config.ContainerdAddr = systemContainerdAddr
|
||||
}
|
||||
}
|
||||
defer cancel()
|
||||
|
@ -403,9 +429,11 @@ func loadDaemonCliConfig(opts *daemonOptions) (*config.Config, error) {
|
|||
}
|
||||
|
||||
if conf.TrustKeyPath == "" {
|
||||
conf.TrustKeyPath = filepath.Join(
|
||||
getDaemonConfDir(conf.Root),
|
||||
defaultTrustKeyFile)
|
||||
daemonConfDir, err := getDaemonConfDir(conf.Root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conf.TrustKeyPath = filepath.Join(daemonConfDir, defaultTrustKeyFile)
|
||||
}
|
||||
|
||||
if flags.Changed("graph") && flags.Changed("data-root") {
|
||||
|
@ -585,7 +613,7 @@ func loadListeners(cli *DaemonCli, serverConfig *apiserver.Config) ([]string, er
|
|||
var hosts []string
|
||||
for i := 0; i < len(cli.Config.Hosts); i++ {
|
||||
var err error
|
||||
if cli.Config.Hosts[i], err = dopts.ParseHost(cli.Config.TLS, cli.Config.Hosts[i]); err != nil {
|
||||
if cli.Config.Hosts[i], err = dopts.ParseHost(cli.Config.TLS, rootless.RunningWithNonRootUsername(), cli.Config.Hosts[i]); err != nil {
|
||||
return nil, errors.Wrapf(err, "error parsing -H %s", cli.Config.Hosts[i])
|
||||
}
|
||||
|
||||
|
@ -662,9 +690,17 @@ func validateAuthzPlugins(requestedPlugins []string, pg plugingetter.PluginGette
|
|||
return nil
|
||||
}
|
||||
|
||||
func systemContainerdRunning() bool {
|
||||
_, err := os.Lstat(containerddefaults.DefaultAddress)
|
||||
return err == nil
|
||||
func systemContainerdRunning(isRootless bool) (string, bool, error) {
|
||||
addr := containerddefaults.DefaultAddress
|
||||
if isRootless {
|
||||
runtimeDir, err := homedir.GetRuntimeDir()
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
addr = filepath.Join(runtimeDir, "containerd", "containerd.sock")
|
||||
}
|
||||
_, err := os.Lstat(addr)
|
||||
return addr, err == nil, nil
|
||||
}
|
||||
|
||||
// configureDaemonLogs sets the logrus logging level and formatting
|
||||
|
|
|
@ -11,18 +11,22 @@ import (
|
|||
"gotest.tools/fs"
|
||||
)
|
||||
|
||||
func defaultOptions(configFile string) *daemonOptions {
|
||||
func defaultOptions(t *testing.T, configFile string) *daemonOptions {
|
||||
opts := newDaemonOptions(&config.Config{})
|
||||
opts.flags = &pflag.FlagSet{}
|
||||
opts.InstallFlags(opts.flags)
|
||||
installConfigFlags(opts.daemonConfig, opts.flags)
|
||||
if err := installConfigFlags(opts.daemonConfig, opts.flags); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defaultDaemonConfigFile, err := getDefaultDaemonConfigFile()
|
||||
assert.NilError(t, err)
|
||||
opts.flags.StringVar(&opts.configFile, "config-file", defaultDaemonConfigFile, "")
|
||||
opts.configFile = configFile
|
||||
return opts
|
||||
}
|
||||
|
||||
func TestLoadDaemonCliConfigWithoutOverriding(t *testing.T) {
|
||||
opts := defaultOptions("")
|
||||
opts := defaultOptions(t, "")
|
||||
opts.Debug = true
|
||||
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
|
@ -34,7 +38,7 @@ func TestLoadDaemonCliConfigWithoutOverriding(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoadDaemonCliConfigWithTLS(t *testing.T) {
|
||||
opts := defaultOptions("")
|
||||
opts := defaultOptions(t, "")
|
||||
opts.TLSOptions.CAFile = "/tmp/ca.pem"
|
||||
opts.TLS = true
|
||||
|
||||
|
@ -49,7 +53,7 @@ func TestLoadDaemonCliConfigWithConflicts(t *testing.T) {
|
|||
defer tempFile.Remove()
|
||||
configFile := tempFile.Path()
|
||||
|
||||
opts := defaultOptions(configFile)
|
||||
opts := defaultOptions(t, configFile)
|
||||
flags := opts.flags
|
||||
|
||||
assert.Check(t, flags.Set("config-file", configFile))
|
||||
|
@ -65,7 +69,7 @@ func TestLoadDaemonCliWithConflictingNodeGenericResources(t *testing.T) {
|
|||
defer tempFile.Remove()
|
||||
configFile := tempFile.Path()
|
||||
|
||||
opts := defaultOptions(configFile)
|
||||
opts := defaultOptions(t, configFile)
|
||||
flags := opts.flags
|
||||
|
||||
assert.Check(t, flags.Set("config-file", configFile))
|
||||
|
@ -77,7 +81,7 @@ func TestLoadDaemonCliWithConflictingNodeGenericResources(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoadDaemonCliWithConflictingLabels(t *testing.T) {
|
||||
opts := defaultOptions("")
|
||||
opts := defaultOptions(t, "")
|
||||
flags := opts.flags
|
||||
|
||||
assert.Check(t, flags.Set("label", "foo=bar"))
|
||||
|
@ -88,7 +92,7 @@ func TestLoadDaemonCliWithConflictingLabels(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoadDaemonCliWithDuplicateLabels(t *testing.T) {
|
||||
opts := defaultOptions("")
|
||||
opts := defaultOptions(t, "")
|
||||
flags := opts.flags
|
||||
|
||||
assert.Check(t, flags.Set("label", "foo=the-same"))
|
||||
|
@ -102,7 +106,7 @@ func TestLoadDaemonCliConfigWithTLSVerify(t *testing.T) {
|
|||
tempFile := fs.NewFile(t, "config", fs.WithContent(`{"tlsverify": true}`))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
opts := defaultOptions(t, tempFile.Path())
|
||||
opts.TLSOptions.CAFile = "/tmp/ca.pem"
|
||||
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
|
@ -115,7 +119,7 @@ func TestLoadDaemonCliConfigWithExplicitTLSVerifyFalse(t *testing.T) {
|
|||
tempFile := fs.NewFile(t, "config", fs.WithContent(`{"tlsverify": false}`))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
opts := defaultOptions(t, tempFile.Path())
|
||||
opts.TLSOptions.CAFile = "/tmp/ca.pem"
|
||||
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
|
@ -128,7 +132,7 @@ func TestLoadDaemonCliConfigWithoutTLSVerify(t *testing.T) {
|
|||
tempFile := fs.NewFile(t, "config", fs.WithContent(`{}`))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
opts := defaultOptions(t, tempFile.Path())
|
||||
opts.TLSOptions.CAFile = "/tmp/ca.pem"
|
||||
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
|
@ -141,7 +145,7 @@ func TestLoadDaemonCliConfigWithLogLevel(t *testing.T) {
|
|||
tempFile := fs.NewFile(t, "config", fs.WithContent(`{"log-level": "warn"}`))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
opts := defaultOptions(t, tempFile.Path())
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, loadedConfig != nil)
|
||||
|
@ -153,7 +157,7 @@ func TestLoadDaemonConfigWithEmbeddedOptions(t *testing.T) {
|
|||
tempFile := fs.NewFile(t, "config", fs.WithContent(content))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
opts := defaultOptions(t, tempFile.Path())
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, loadedConfig != nil)
|
||||
|
@ -170,7 +174,7 @@ func TestLoadDaemonConfigWithRegistryOptions(t *testing.T) {
|
|||
tempFile := fs.NewFile(t, "config", fs.WithContent(content))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
opts := defaultOptions(t, tempFile.Path())
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, loadedConfig != nil)
|
||||
|
|
|
@ -15,11 +15,33 @@ import (
|
|||
"github.com/docker/docker/daemon"
|
||||
"github.com/docker/docker/daemon/config"
|
||||
"github.com/docker/docker/libcontainerd/supervisor"
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/docker/docker/rootless"
|
||||
"github.com/docker/libnetwork/portallocator"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const defaultDaemonConfigFile = "/etc/docker/daemon.json"
|
||||
func getDefaultDaemonConfigDir() (string, error) {
|
||||
if !rootless.RunningWithNonRootUsername() {
|
||||
return "/etc/docker", nil
|
||||
}
|
||||
// NOTE: CLI uses ~/.docker while the daemon uses ~/.config/docker, because
|
||||
// ~/.docker was not designed to store daemon configurations.
|
||||
// In future, the daemon directory may be renamed to ~/.config/moby-engine (?).
|
||||
configHome, err := homedir.GetConfigHome()
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
return filepath.Join(configHome, "docker"), nil
|
||||
}
|
||||
|
||||
func getDefaultDaemonConfigFile() (string, error) {
|
||||
dir, err := getDefaultDaemonConfigDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, "daemon.json"), nil
|
||||
}
|
||||
|
||||
// setDefaultUmask sets the umask to 0022 to avoid problems
|
||||
// caused by custom umask
|
||||
|
@ -33,8 +55,8 @@ func setDefaultUmask() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func getDaemonConfDir(_ string) string {
|
||||
return "/etc/docker"
|
||||
func getDaemonConfDir(_ string) (string, error) {
|
||||
return getDefaultDaemonConfigDir()
|
||||
}
|
||||
|
||||
func (cli *DaemonCli) getPlatformContainerdDaemonOpts() ([]supervisor.DaemonOpt, error) {
|
||||
|
|
|
@ -16,7 +16,7 @@ func TestLoadDaemonCliConfigWithDaemonFlags(t *testing.T) {
|
|||
tempFile := fs.NewFile(t, "config", fs.WithContent(content))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
opts := defaultOptions(t, tempFile.Path())
|
||||
opts.Debug = true
|
||||
opts.LogLevel = "info"
|
||||
assert.Check(t, opts.flags.Set("selinux-enabled", "true"))
|
||||
|
@ -37,7 +37,7 @@ func TestLoadDaemonConfigWithNetwork(t *testing.T) {
|
|||
tempFile := fs.NewFile(t, "config", fs.WithContent(content))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
opts := defaultOptions(t, tempFile.Path())
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, loadedConfig != nil)
|
||||
|
@ -54,7 +54,7 @@ func TestLoadDaemonConfigWithMapOptions(t *testing.T) {
|
|||
tempFile := fs.NewFile(t, "config", fs.WithContent(content))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
opts := defaultOptions(t, tempFile.Path())
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, loadedConfig != nil)
|
||||
|
@ -71,7 +71,7 @@ func TestLoadDaemonConfigWithTrueDefaultValues(t *testing.T) {
|
|||
tempFile := fs.NewFile(t, "config", fs.WithContent(content))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
opts := defaultOptions(t, tempFile.Path())
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, loadedConfig != nil)
|
||||
|
@ -90,7 +90,7 @@ func TestLoadDaemonConfigWithTrueDefaultValuesLeaveDefaults(t *testing.T) {
|
|||
tempFile := fs.NewFile(t, "config", fs.WithContent(`{}`))
|
||||
defer tempFile.Remove()
|
||||
|
||||
opts := defaultOptions(tempFile.Path())
|
||||
opts := defaultOptions(t, tempFile.Path())
|
||||
loadedConfig, err := loadDaemonCliConfig(opts)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, loadedConfig != nil)
|
||||
|
|
|
@ -12,15 +12,17 @@ import (
|
|||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var defaultDaemonConfigFile = ""
|
||||
func getDefaultDaemonConfigFile() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// setDefaultUmask doesn't do anything on windows
|
||||
func setDefaultUmask() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDaemonConfDir(root string) string {
|
||||
return filepath.Join(root, `\config`)
|
||||
func getDaemonConfDir(root string) (string, error) {
|
||||
return filepath.Join(root, `\config`), nil
|
||||
}
|
||||
|
||||
// preNotifySystem sends a message to the host when the API is active, but before the daemon is
|
||||
|
|
|
@ -16,7 +16,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newDaemonCommand() *cobra.Command {
|
||||
func newDaemonCommand() (*cobra.Command, error) {
|
||||
opts := newDaemonOptions(config.New())
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -36,12 +36,18 @@ func newDaemonCommand() *cobra.Command {
|
|||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolP("version", "v", false, "Print version information and quit")
|
||||
defaultDaemonConfigFile, err := getDefaultDaemonConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
flags.StringVar(&opts.configFile, "config-file", defaultDaemonConfigFile, "Daemon configuration file")
|
||||
opts.InstallFlags(flags)
|
||||
installConfigFlags(opts.daemonConfig, flags)
|
||||
if err := installConfigFlags(opts.daemonConfig, flags); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
installServiceFlags(flags)
|
||||
|
||||
return cmd
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -72,10 +78,17 @@ func main() {
|
|||
logrus.SetOutput(stderr)
|
||||
}
|
||||
|
||||
cmd := newDaemonCommand()
|
||||
cmd.SetOutput(stdout)
|
||||
if err := cmd.Execute(); err != nil {
|
||||
onError := func(err error) {
|
||||
fmt.Fprintf(stderr, "%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cmd, err := newDaemonCommand()
|
||||
if err != nil {
|
||||
onError(err)
|
||||
}
|
||||
cmd.SetOutput(stdout)
|
||||
if err := cmd.Execute(); err != nil {
|
||||
onError(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,8 @@ func newDaemonOptions(config *config.Config) *daemonOptions {
|
|||
// InstallFlags adds flags for the common options on the FlagSet
|
||||
func (o *daemonOptions) InstallFlags(flags *pflag.FlagSet) {
|
||||
if dockerCertPath == "" {
|
||||
// cliconfig.Dir returns $DOCKER_CONFIG or ~/.docker.
|
||||
// cliconfig.Dir does not look up $XDG_CONFIG_HOME
|
||||
dockerCertPath = cliconfig.Dir()
|
||||
}
|
||||
|
||||
|
|
77
contrib/dockerd-rootless.sh
Executable file
77
contrib/dockerd-rootless.sh
Executable file
|
@ -0,0 +1,77 @@
|
|||
#!/bin/sh
|
||||
# dockerd-rootless.sh executes dockerd in rootless mode.
|
||||
#
|
||||
# Usage: dockerd-rootless.sh --experimental [DOCKERD_OPTIONS]
|
||||
# Currently, specifying --experimental is mandatory.
|
||||
#
|
||||
# External dependencies:
|
||||
# * newuidmap and newgidmap needs to be installed.
|
||||
# * /etc/subuid and /etc/subgid needs to be configured for the current user.
|
||||
# * Either slirp4netns (v0.3+) or VPNKit needs to be installed.
|
||||
#
|
||||
# See the documentation for the further information.
|
||||
|
||||
set -e -x
|
||||
if ! [ -w $XDG_RUNTIME_DIR ]; then
|
||||
echo "XDG_RUNTIME_DIR needs to be set and writable"
|
||||
exit 1
|
||||
fi
|
||||
if ! [ -w $HOME ]; then
|
||||
echo "HOME needs to be set and writable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rootlesskit=""
|
||||
for f in docker-rootlesskit rootlesskit; do
|
||||
if which $f >/dev/null 2>&1; then
|
||||
rootlesskit=$f
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z $rootlesskit ]; then
|
||||
echo "rootlesskit needs to be installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
net=""
|
||||
mtu=""
|
||||
if which slirp4netns >/dev/null 2>&1; then
|
||||
if slirp4netns --help | grep -- --disable-host-loopback; then
|
||||
net=slirp4netns
|
||||
mtu=65520
|
||||
else
|
||||
echo "slirp4netns does not support --disable-host-loopback. Falling back to VPNKit."
|
||||
fi
|
||||
fi
|
||||
if [ -z $net ]; then
|
||||
if which vpnkit >/dev/null 2>&1; then
|
||||
net=vpnkit
|
||||
mtu=1500
|
||||
else
|
||||
echo "Either slirp4netns (v0.3+) or vpnkit needs to be installed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z $_DOCKERD_ROOTLESS_CHILD ]; then
|
||||
_DOCKERD_ROOTLESS_CHILD=1
|
||||
export _DOCKERD_ROOTLESS_CHILD
|
||||
# Re-exec the script via RootlessKit, so as to create unprivileged {user,mount,network} namespaces.
|
||||
#
|
||||
# --copy-up allows removing/creating files in the directories by creating tmpfs and symlinks
|
||||
# * /etc: copy-up is required so as to prevent `/etc/resolv.conf` in the
|
||||
# namespace from being unexpectedly unmounted when `/etc/resolv.conf` is recreated on the host
|
||||
# (by either systemd-networkd or NetworkManager)
|
||||
# * /run: copy-up is required so that we can create /run/docker (hardcoded for plugins) in our namespace
|
||||
$rootlesskit \
|
||||
--net=$net --mtu=$mtu --disable-host-loopback \
|
||||
--copy-up=/etc --copy-up=/run \
|
||||
$DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS \
|
||||
$0 $@
|
||||
else
|
||||
[ $_DOCKERD_ROOTLESS_CHILD = 1 ]
|
||||
# remove the symlinks for the existing files in the parent namespace if any,
|
||||
# so that we can create our own files in our mount namespace.
|
||||
rm -f /run/docker /run/xtables.lock
|
||||
dockerd $@
|
||||
fi
|
|
@ -39,6 +39,7 @@ type Config struct {
|
|||
IpcMode string `json:"default-ipc-mode,omitempty"`
|
||||
// ResolvConf is the path to the configuration of the host resolver
|
||||
ResolvConf string `json:"resolv-conf,omitempty"`
|
||||
Rootless bool `json:"rootless,omitempty"`
|
||||
}
|
||||
|
||||
// BridgeConfig stores all the bridge driver specific
|
||||
|
@ -87,3 +88,8 @@ func verifyDefaultIpcMode(mode string) error {
|
|||
func (conf *Config) ValidatePlatformConfig() error {
|
||||
return verifyDefaultIpcMode(conf.IpcMode)
|
||||
}
|
||||
|
||||
// IsRootless returns conf.Rootless
|
||||
func (conf *Config) IsRootless() bool {
|
||||
return conf.Rootless
|
||||
}
|
||||
|
|
|
@ -55,3 +55,8 @@ func (conf *Config) IsSwarmCompatible() error {
|
|||
func (conf *Config) ValidatePlatformConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRootless returns conf.Rootless on Unix but false on Windows
|
||||
func (conf *Config) IsRootless() bool {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -800,6 +800,7 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S
|
|||
logrus.Warnf("Failed to configure golang's threads limit: %v", err)
|
||||
}
|
||||
|
||||
// ensureDefaultAppArmorProfile does nothing if apparmor is disabled
|
||||
if err := ensureDefaultAppArmorProfile(); err != nil {
|
||||
logrus.Errorf(err.Error())
|
||||
}
|
||||
|
|
|
@ -745,9 +745,6 @@ func verifyDaemonSettings(conf *config.Config) error {
|
|||
|
||||
// checkSystem validates platform-specific requirements
|
||||
func checkSystem() error {
|
||||
if os.Geteuid() != 0 {
|
||||
return fmt.Errorf("The Docker daemon needs to be run as root")
|
||||
}
|
||||
return checkKernel()
|
||||
}
|
||||
|
||||
|
|
|
@ -175,6 +175,9 @@ func (daemon *Daemon) fillSecurityOptions(v *types.Info, sysInfo *sysinfo.SysInf
|
|||
if rootIDs := daemon.idMapping.RootPair(); rootIDs.UID != 0 || rootIDs.GID != 0 {
|
||||
securityOptions = append(securityOptions, "name=userns")
|
||||
}
|
||||
if daemon.configStoreRootless() {
|
||||
securityOptions = append(securityOptions, "name=rootless")
|
||||
}
|
||||
v.SecurityOptions = securityOptions
|
||||
}
|
||||
|
||||
|
|
|
@ -245,3 +245,7 @@ func parseRuncVersion(v string) (version string, commit string, err error) {
|
|||
}
|
||||
return version, commit, err
|
||||
}
|
||||
|
||||
func (daemon *Daemon) configStoreRootless() bool {
|
||||
return daemon.configStore.Rootless
|
||||
}
|
||||
|
|
|
@ -13,3 +13,7 @@ func (daemon *Daemon) fillPlatformVersion(v *types.Version) {}
|
|||
|
||||
func fillDriverWarnings(v *types.Info) {
|
||||
}
|
||||
|
||||
func (daemon *Daemon) configStoreRootless() bool {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"strconv"
|
||||
|
||||
"github.com/coreos/go-systemd/activation"
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/docker/go-connections/sockets"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
@ -45,6 +46,10 @@ func Init(proto, addr, socketGroup string, tlsConfig *tls.Config) ([]net.Listene
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("can't create unix socket %s: %v", addr, err)
|
||||
}
|
||||
if _, err := homedir.StickRuntimeDirContents([]string{addr}); err != nil {
|
||||
// StickRuntimeDirContents returns nil error if XDG_RUNTIME_DIR is just unset
|
||||
logrus.WithError(err).Warnf("cannot set sticky bit on socket %s under XDG_RUNTIME_DIR", addr)
|
||||
}
|
||||
ls = append(ls, l)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid protocol format: %q", proto)
|
||||
|
|
|
@ -17,10 +17,12 @@ import (
|
|||
"github.com/docker/docker/oci/caps"
|
||||
"github.com/docker/docker/pkg/idtools"
|
||||
"github.com/docker/docker/pkg/mount"
|
||||
"github.com/docker/docker/rootless/specconv"
|
||||
volumemounts "github.com/docker/docker/volume/mounts"
|
||||
"github.com/opencontainers/runc/libcontainer/apparmor"
|
||||
"github.com/opencontainers/runc/libcontainer/cgroups"
|
||||
"github.com/opencontainers/runc/libcontainer/devices"
|
||||
rsystem "github.com/opencontainers/runc/libcontainer/system"
|
||||
"github.com/opencontainers/runc/libcontainer/user"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -89,7 +91,7 @@ func setDevices(s *specs.Spec, c *container.Container) error {
|
|||
// Build lists of devices allowed and created within the container.
|
||||
var devs []specs.LinuxDevice
|
||||
devPermissions := s.Linux.Resources.Devices
|
||||
if c.HostConfig.Privileged {
|
||||
if c.HostConfig.Privileged && !rsystem.RunningInUserNS() {
|
||||
hostDevices, err := devices.HostDevices()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -867,6 +869,11 @@ func (daemon *Daemon) createSpec(c *container.Container) (retSpec *specs.Spec, e
|
|||
s.Linux.ReadonlyPaths = c.HostConfig.ReadonlyPaths
|
||||
}
|
||||
|
||||
if daemon.configStore.Rootless {
|
||||
if err := specconv.ToRootless(&s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
|
|
92
docs/rootless.md
Normal file
92
docs/rootless.md
Normal file
|
@ -0,0 +1,92 @@
|
|||
# Rootless mode (Experimental)
|
||||
|
||||
The rootless mode allows running `dockerd` as an unprivileged user, using `user_namespaces(7)`, `mount_namespaces(7)`, `network_namespaces(7)`.
|
||||
|
||||
No SETUID/SETCAP binary is required except `newuidmap` and `newgidmap`.
|
||||
|
||||
## Requirements
|
||||
* `newuidmap` and `newgidmap` need to be installed on the host. These commands are provided by the `uidmap` package on most distros.
|
||||
|
||||
* `/etc/subuid` and `/etc/subgid` should contain >= 65536 sub-IDs. e.g. `penguin:231072:65536`.
|
||||
|
||||
```console
|
||||
$ id -u
|
||||
1001
|
||||
$ whoami
|
||||
penguin
|
||||
$ grep ^$(whoami): /etc/subuid
|
||||
penguin:231072:65536
|
||||
$ grep ^$(whoami): /etc/subgid
|
||||
penguin:231072:65536
|
||||
```
|
||||
|
||||
* Either [slirp4netns](https://github.com/rootless-containers/slirp4netns) (v0.3+) or [VPNKit](https://github.com/moby/vpnkit) needs to be installed. slirp4netns is preferred for the best performance.
|
||||
|
||||
### Distribution-specific hint
|
||||
|
||||
#### Debian (excluding Ubuntu)
|
||||
* `sudo sh -c "echo 1 > /proc/sys/kernel/unprivileged_userns_clone"` is required
|
||||
|
||||
#### Arch Linux
|
||||
* `sudo sh -c "echo 1 > /proc/sys/kernel/unprivileged_userns_clone"` is required
|
||||
|
||||
#### openSUSE
|
||||
* `sudo modprobe ip_tables iptable_mangle iptable_nat iptable_filter` is required. (This is likely to be required on other distros as well)
|
||||
|
||||
#### RHEL/CentOS 7
|
||||
* `sudo sh -c "echo 28633 > /proc/sys/user/max_user_namespaces"` is required
|
||||
* [COPR package `vbatts/shadow-utils-newxidmap`](https://copr.fedorainfracloud.org/coprs/vbatts/shadow-utils-newxidmap/) needs to be installed
|
||||
|
||||
## Restrictions
|
||||
|
||||
* Only `vfs` graphdriver is supported. However, on [Ubuntu](http://kernel.ubuntu.com/git/ubuntu/ubuntu-artful.git/commit/fs/overlayfs?h=Ubuntu-4.13.0-25.29&id=0a414bdc3d01f3b61ed86cfe3ce8b63a9240eba7) and a few distros, `overlay2` and `overlay` are also supported.
|
||||
* Following features are not supported:
|
||||
* Cgroups (including `docker top`, which depends on the cgroups device controller)
|
||||
* Apparmor
|
||||
* Checkpoint
|
||||
* Overlay network
|
||||
|
||||
## Usage
|
||||
|
||||
### Daemon
|
||||
|
||||
You need to run `dockerd-rootless.sh` instead of `dockerd`.
|
||||
|
||||
```console
|
||||
$ dockerd-rootless.sh --experimental"
|
||||
```
|
||||
As Rootless mode is experimental per se, currently you always need to run `dockerd-rootless.sh` with `--experimental`.
|
||||
|
||||
Remarks:
|
||||
* The socket path is set to `$XDG_RUNTIME_DIR/docker.sock` by default. `$XDG_RUNTIME_DIR` is typically set to `/run/user/$UID`.
|
||||
* The data dir is set to `~/.local/share/docker` by default.
|
||||
* The exec dir is set to `$XDG_RUNTIME_DIR/docker` by default.
|
||||
* The daemon config dir is set to `~/.config/docker` (not `~/.docker`, which is used by the client) by default.
|
||||
* The `dockerd-rootless.sh` script executes `dockerd` in its own user, mount, and network namespaces. You can enter the namespaces by running `nsenter -U --preserve-credentials -n -m -t $(cat $XDG_RUNTIME_DIR/docker.pid)`.
|
||||
|
||||
### Client
|
||||
|
||||
You can just use the upstream Docker client but you need to set the socket path explicitly.
|
||||
|
||||
```console
|
||||
$ docker -H unix://$XDG_RUNTIME_DIR/docker.sock run -d nginx
|
||||
```
|
||||
|
||||
### Exposing ports
|
||||
|
||||
In addition to exposing container ports to the `dockerd` network namespace, you also need to expose the ports in the `dockerd` network namespace to the host network namespace.
|
||||
|
||||
```console
|
||||
$ docker -H unix://$XDG_RUNTIME_DIR/docker.sock run -d -p 80:80 nginx
|
||||
$ socat -t -- TCP-LISTEN:8080,reuseaddr,fork EXEC:"nsenter -U -n -t $(cat $XDG_RUNTIME_DIR/docker.pid) socat -t -- STDIN TCP4\:127.0.0.1\:80"
|
||||
```
|
||||
|
||||
In future, `dockerd` will be able to expose the ports automatically.
|
||||
|
||||
### Routing ping packets
|
||||
|
||||
To route ping packets, you need to set up `net.ipv4.ping_group_range` properly as the root.
|
||||
|
||||
```console
|
||||
$ sudo sh -c "echo 0 2147483647 > /proc/sys/net/ipv4/ping_group_range"
|
||||
```
|
34
hack/dockerfile/install/rootlesskit.installer
Executable file
34
hack/dockerfile/install/rootlesskit.installer
Executable file
|
@ -0,0 +1,34 @@
|
|||
#!/bin/sh
|
||||
|
||||
# v0.3.0-alpha.0
|
||||
ROOTLESSKIT_COMMIT=3c4582e950e3a67795c2832179c125b258b78124
|
||||
|
||||
install_rootlesskit() {
|
||||
case "$1" in
|
||||
"dynamic")
|
||||
install_rootlesskit_dynamic
|
||||
return
|
||||
;;
|
||||
"")
|
||||
export CGO_ENABLED=0
|
||||
_install_rootlesskit
|
||||
;;
|
||||
*)
|
||||
echo 'Usage: $0 [dynamic]'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_rootlesskit_dynamic() {
|
||||
export ROOTLESSKIT_LDFLAGS="-linkmode=external" install_rootlesskit
|
||||
export BUILD_MODE="-buildmode=pie"
|
||||
_install_rootlesskit
|
||||
}
|
||||
|
||||
_install_rootlesskit() {
|
||||
echo "Install rootlesskit version $ROOTLESSKIT_COMMIT"
|
||||
git clone https://github.com/rootless-containers/rootlesskit.git "$GOPATH/src/github.com/rootless-containers/rootlesskit"
|
||||
cd "$GOPATH/src/github.com/rootless-containers/rootlesskit"
|
||||
git checkout -q "$ROOTLESSKIT_COMMIT"
|
||||
go build $BUILD_MODE -ldflags="$ROOTLESSKIT_LDFLAGS" -o "${PREFIX}/rootlesskit" github.com/rootless-containers/rootlesskit/cmd/rootlesskit
|
||||
}
|
|
@ -7,3 +7,5 @@ DOCKER_CONTAINERD_CTR_BINARY_NAME='ctr'
|
|||
DOCKER_CONTAINERD_SHIM_BINARY_NAME='containerd-shim'
|
||||
DOCKER_PROXY_BINARY_NAME='docker-proxy'
|
||||
DOCKER_INIT_BINARY_NAME='docker-init'
|
||||
DOCKER_ROOTLESSKIT_BINARY_NAME='rootlesskit'
|
||||
DOCKER_DAEMON_ROOTLESS_SH_BINARY_NAME='dockerd-rootless.sh'
|
||||
|
|
|
@ -14,7 +14,7 @@ copy_binaries() {
|
|||
return
|
||||
fi
|
||||
echo "Copying nested executables into $dir"
|
||||
for file in containerd containerd-shim ctr runc docker-init docker-proxy; do
|
||||
for file in containerd containerd-shim ctr runc docker-init docker-proxy rootlesskit dockerd-rootless.sh; do
|
||||
cp -f `which "$file"` "$dir/"
|
||||
if [ "$hash" == "hash" ]; then
|
||||
hash_files "$dir/$file"
|
||||
|
|
|
@ -26,4 +26,6 @@ install_binary() {
|
|||
install_binary "${DEST}/${DOCKER_CONTAINERD_SHIM_BINARY_NAME}"
|
||||
install_binary "${DEST}/${DOCKER_PROXY_BINARY_NAME}"
|
||||
install_binary "${DEST}/${DOCKER_INIT_BINARY_NAME}"
|
||||
install_binary "${DEST}/${DOCKER_ROOTLESSKIT_BINARY_NAME}"
|
||||
install_binary "${DEST}/${DOCKER_DAEMON_ROOTLESS_SH_BINARY_NAME}"
|
||||
)
|
||||
|
|
|
@ -4,8 +4,11 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -41,12 +44,20 @@ func ValidateHost(val string) (string, error) {
|
|||
return val, nil
|
||||
}
|
||||
|
||||
// ParseHost and set defaults for a Daemon host string
|
||||
func ParseHost(defaultToTLS bool, val string) (string, error) {
|
||||
// ParseHost and set defaults for a Daemon host string.
|
||||
// defaultToTLS is preferred over defaultToUnixRootless.
|
||||
func ParseHost(defaultToTLS, defaultToUnixRootless bool, val string) (string, error) {
|
||||
host := strings.TrimSpace(val)
|
||||
if host == "" {
|
||||
if defaultToTLS {
|
||||
host = DefaultTLSHost
|
||||
} else if defaultToUnixRootless {
|
||||
runtimeDir, err := homedir.GetRuntimeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
socket := filepath.Join(runtimeDir, "docker.sock")
|
||||
host = "unix://" + socket
|
||||
} else {
|
||||
host = DefaultHost
|
||||
}
|
||||
|
|
|
@ -38,13 +38,13 @@ func TestParseHost(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, value := range invalid {
|
||||
if _, err := ParseHost(false, value); err == nil {
|
||||
if _, err := ParseHost(false, false, value); err == nil {
|
||||
t.Errorf("Expected an error for %v, got [nil]", value)
|
||||
}
|
||||
}
|
||||
|
||||
for value, expected := range valid {
|
||||
if actual, err := ParseHost(false, value); err != nil || actual != expected {
|
||||
if actual, err := ParseHost(false, false, value); err != nil || actual != expected {
|
||||
t.Errorf("Expected for %v [%v], got [%v, %v]", value, expected, actual, err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package homedir // import "github.com/docker/docker/pkg/homedir"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/pkg/idtools"
|
||||
)
|
||||
|
@ -19,3 +22,88 @@ func GetStatic() (string, error) {
|
|||
}
|
||||
return usr.Home, nil
|
||||
}
|
||||
|
||||
// GetRuntimeDir returns XDG_RUNTIME_DIR.
|
||||
// XDG_RUNTIME_DIR is typically configured via pam_systemd.
|
||||
// GetRuntimeDir returns non-nil error if XDG_RUNTIME_DIR is not set.
|
||||
//
|
||||
// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
|
||||
func GetRuntimeDir() (string, error) {
|
||||
if xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); xdgRuntimeDir != "" {
|
||||
return xdgRuntimeDir, nil
|
||||
}
|
||||
return "", errors.New("could not get XDG_RUNTIME_DIR")
|
||||
}
|
||||
|
||||
// StickRuntimeDirContents sets the sticky bit on files that are under
|
||||
// XDG_RUNTIME_DIR, so that the files won't be periodically removed by the system.
|
||||
//
|
||||
// StickyRuntimeDir returns slice of sticked files.
|
||||
// StickyRuntimeDir returns nil error if XDG_RUNTIME_DIR is not set.
|
||||
//
|
||||
// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
|
||||
func StickRuntimeDirContents(files []string) ([]string, error) {
|
||||
runtimeDir, err := GetRuntimeDir()
|
||||
if err != nil {
|
||||
// ignore error if runtimeDir is empty
|
||||
return nil, nil
|
||||
}
|
||||
runtimeDir, err = filepath.Abs(runtimeDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sticked []string
|
||||
for _, f := range files {
|
||||
f, err = filepath.Abs(f)
|
||||
if err != nil {
|
||||
return sticked, err
|
||||
}
|
||||
if strings.HasPrefix(f, runtimeDir+"/") {
|
||||
if err = stick(f); err != nil {
|
||||
return sticked, err
|
||||
}
|
||||
sticked = append(sticked, f)
|
||||
}
|
||||
}
|
||||
return sticked, nil
|
||||
}
|
||||
|
||||
func stick(f string) error {
|
||||
st, err := os.Stat(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m := st.Mode()
|
||||
m |= os.ModeSticky
|
||||
return os.Chmod(f, m)
|
||||
}
|
||||
|
||||
// GetDataHome returns XDG_DATA_HOME.
|
||||
// GetDataHome returns $HOME/.local/share and nil error if XDG_DATA_HOME is not set.
|
||||
//
|
||||
// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
|
||||
func GetDataHome() (string, error) {
|
||||
if xdgDataHome := os.Getenv("XDG_DATA_HOME"); xdgDataHome != "" {
|
||||
return xdgDataHome, nil
|
||||
}
|
||||
home := os.Getenv("HOME")
|
||||
if home == "" {
|
||||
return "", errors.New("could not get either XDG_DATA_HOME or HOME")
|
||||
}
|
||||
return filepath.Join(home, ".local", "share"), nil
|
||||
}
|
||||
|
||||
// GetConfigHome returns XDG_CONFIG_HOME.
|
||||
// GetConfigHome returns $HOME/.config and nil error if XDG_CONFIG_HOME is not set.
|
||||
//
|
||||
// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
|
||||
func GetConfigHome() (string, error) {
|
||||
if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
|
||||
return xdgConfigHome, nil
|
||||
}
|
||||
home := os.Getenv("HOME")
|
||||
if home == "" {
|
||||
return "", errors.New("could not get either XDG_CONFIG_HOME or HOME")
|
||||
}
|
||||
return filepath.Join(home, ".config"), nil
|
||||
}
|
||||
|
|
|
@ -11,3 +11,23 @@ import (
|
|||
func GetStatic() (string, error) {
|
||||
return "", errors.New("homedir.GetStatic() is not supported on this system")
|
||||
}
|
||||
|
||||
// GetRuntimeDir is unsupported on non-linux system.
|
||||
func GetRuntimeDir() (string, error) {
|
||||
return "", errors.New("homedir.GetRuntimeDir() is not supported on this system")
|
||||
}
|
||||
|
||||
// StickRuntimeDirContents is unsupported on non-linux system.
|
||||
func StickRuntimeDirContents(files []string) ([]string, error) {
|
||||
return nil, errors.New("homedir.StickRuntimeDirContents() is not supported on this system")
|
||||
}
|
||||
|
||||
// GetDataHome is unsupported on non-linux system.
|
||||
func GetDataHome() (string, error) {
|
||||
return "", errors.New("homedir.GetDataHome() is not supported on this system")
|
||||
}
|
||||
|
||||
// GetConfigHome is unsupported on non-linux system.
|
||||
func GetConfigHome() (string, error) {
|
||||
return "", errors.New("homedir.GetConfigHome() is not supported on this system")
|
||||
}
|
||||
|
|
|
@ -51,7 +51,9 @@ func New(quiet bool) *SysInfo {
|
|||
|
||||
// Check if AppArmor is supported.
|
||||
if _, err := os.Stat("/sys/kernel/security/apparmor"); !os.IsNotExist(err) {
|
||||
sysInfo.AppArmor = true
|
||||
if _, err := ioutil.ReadFile("/sys/kernel/security/apparmor/profiles"); err == nil {
|
||||
sysInfo.AppArmor = true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if Seccomp is supported, via CONFIG_SECCOMP.
|
||||
|
|
26
rootless/rootless.go
Normal file
26
rootless/rootless.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package rootless
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
runningWithNonRootUsername bool
|
||||
runningWithNonRootUsernameOnce sync.Once
|
||||
)
|
||||
|
||||
// RunningWithNonRootUsername returns true if we $USER is set to a non-root value,
|
||||
// regardless to the UID/EUID value.
|
||||
//
|
||||
// The value of this variable is mostly used for configuring default paths.
|
||||
// If the value is true, $HOME and $XDG_RUNTIME_DIR should be honored for setting up the default paths.
|
||||
// If false (not only EUID==0 but also $USER==root), $HOME and $XDG_RUNTIME_DIR should be ignored
|
||||
// even if we are in a user namespace.
|
||||
func RunningWithNonRootUsername() bool {
|
||||
runningWithNonRootUsernameOnce.Do(func() {
|
||||
u := os.Getenv("USER")
|
||||
runningWithNonRootUsername = u != "" && u != "root"
|
||||
})
|
||||
return runningWithNonRootUsername
|
||||
}
|
38
rootless/specconv/specconv_linux.go
Normal file
38
rootless/specconv/specconv_linux.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package specconv
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
// ToRootless converts spec to be compatible with "rootless" runc.
|
||||
// * Remove cgroups (will be supported in separate PR when delegation permission is configured)
|
||||
// * Fix up OOMScoreAdj
|
||||
func ToRootless(spec *specs.Spec) error {
|
||||
return toRootless(spec, getCurrentOOMScoreAdj())
|
||||
}
|
||||
|
||||
func getCurrentOOMScoreAdj() int {
|
||||
b, err := ioutil.ReadFile("/proc/self/oom_score_adj")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
i, err := strconv.Atoi(string(b))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func toRootless(spec *specs.Spec, currentOOMScoreAdj int) error {
|
||||
// Remove cgroup settings.
|
||||
spec.Linux.Resources = nil
|
||||
spec.Linux.CgroupsPath = ""
|
||||
|
||||
if spec.Process.OOMScoreAdj != nil && *spec.Process.OOMScoreAdj < currentOOMScoreAdj {
|
||||
*spec.Process.OOMScoreAdj = currentOOMScoreAdj
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
Reference in a new issue