mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Merge pull request #15099 from ewindisch/apparmor-restore-en
Restore AppArmor generation + fixes
This commit is contained in:
commit
d7661cb48b
6 changed files with 343 additions and 57 deletions
|
@ -1,25 +0,0 @@
|
||||||
#include <tunables/global>
|
|
||||||
|
|
||||||
profile docker-default flags=(attach_disconnected,mediate_deleted) {
|
|
||||||
#include <abstractions/base>
|
|
||||||
|
|
||||||
network,
|
|
||||||
capability,
|
|
||||||
file,
|
|
||||||
umount,
|
|
||||||
|
|
||||||
deny @{PROC}/sys/fs/** wklx,
|
|
||||||
deny @{PROC}/sysrq-trigger rwklx,
|
|
||||||
deny @{PROC}/sys/kernel/[^s][^h][^m]* wklx,
|
|
||||||
deny @{PROC}/sys/kernel/*/** wklx,
|
|
||||||
|
|
||||||
deny mount,
|
|
||||||
|
|
||||||
deny /sys/[^f]*/** wklx,
|
|
||||||
deny /sys/f[^s]*/** wklx,
|
|
||||||
deny /sys/fs/[^c]*/** wklx,
|
|
||||||
deny /sys/fs/c[^g]*/** wklx,
|
|
||||||
deny /sys/fs/cg[^r]*/** wklx,
|
|
||||||
deny /sys/firmware/efi/efivars/** rwklx,
|
|
||||||
deny /sys/kernel/security/** rwklx,
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
@{DOCKER_GRAPH_PATH}=/var/lib/docker
|
@{DOCKER_GRAPH_PATH}=/var/lib/docker
|
||||||
|
|
||||||
profile /usr/bin/docker (attach_disconnected) {
|
profile /usr/bin/docker (attach_disconnected, complain) {
|
||||||
# Prevent following links to these files during container setup.
|
# Prevent following links to these files during container setup.
|
||||||
deny /etc/** mkl,
|
deny /etc/** mkl,
|
||||||
deny /dev/** kl,
|
deny /dev/** kl,
|
||||||
|
@ -21,51 +21,131 @@ profile /usr/bin/docker (attach_disconnected) {
|
||||||
ipc rw,
|
ipc rw,
|
||||||
network,
|
network,
|
||||||
capability,
|
capability,
|
||||||
file,
|
owner /** rw,
|
||||||
|
/var/lib/docker/** rwl,
|
||||||
|
|
||||||
|
# For non-root client use:
|
||||||
|
/dev/urandom r,
|
||||||
|
/run/docker.sock rw,
|
||||||
|
/proc/** r,
|
||||||
|
/sys/kernel/mm/hugepages/ r,
|
||||||
|
/etc/localtime r,
|
||||||
|
|
||||||
ptrace peer=@{profile_name},
|
ptrace peer=@{profile_name},
|
||||||
|
ptrace (read) peer=docker-default,
|
||||||
|
deny ptrace (trace) peer=docker-default,
|
||||||
|
deny ptrace peer=/usr/bin/docker///bin/ps,
|
||||||
|
|
||||||
/usr/bin/docker pix,
|
/usr/bin/docker pix,
|
||||||
/sbin/xtables-multi rCix,
|
/sbin/xtables-multi rCx,
|
||||||
/sbin/iptables rCx,
|
/sbin/iptables rCx,
|
||||||
/sbin/modprobe rCx,
|
/sbin/modprobe rCx,
|
||||||
/sbin/auplink rCx,
|
/sbin/auplink rCx,
|
||||||
|
/bin/kmod rCx,
|
||||||
/usr/bin/xz rCx,
|
/usr/bin/xz rCx,
|
||||||
|
/bin/ps rCx,
|
||||||
|
/bin/cat rCx,
|
||||||
|
/sbin/zfs rCx,
|
||||||
|
|
||||||
# Transitions
|
# Transitions
|
||||||
change_profile -> docker-*,
|
change_profile -> docker-*,
|
||||||
change_profile -> unconfined,
|
change_profile -> unconfined,
|
||||||
|
|
||||||
profile /sbin/iptables {
|
profile /bin/cat (complain) {
|
||||||
signal (receive) peer=/usr/bin/docker,
|
/etc/ld.so.cache r,
|
||||||
capability net_admin,
|
/lib/** r,
|
||||||
}
|
/dev/null rw,
|
||||||
profile /sbin/auplink flags=(attach_disconnected) {
|
/proc r,
|
||||||
signal (receive) peer=/usr/bin/docker,
|
/bin/cat mr,
|
||||||
capability sys_admin,
|
|
||||||
capability dac_override,
|
|
||||||
|
|
||||||
@{DOCKER_GRAPH_PATH}/aufs/** rw,
|
# For reading in 'docker stats':
|
||||||
# For user namespaces:
|
/proc/[0-9]*/net/dev r,
|
||||||
@{DOCKER_GRAPH_PATH}/[0-9]*.[0-9]*/** rw,
|
|
||||||
|
|
||||||
# The following may be removed via delegates
|
|
||||||
/sys/fs/aufs/** r,
|
|
||||||
/lib/** r,
|
|
||||||
/apparmor/.null r,
|
|
||||||
/dev/null rw,
|
|
||||||
/etc/ld.so.cache r,
|
|
||||||
/sbin/auplink rm,
|
|
||||||
/proc/fs/aufs/** rw,
|
|
||||||
/proc/[0-9]*/mounts rw,
|
|
||||||
}
|
}
|
||||||
profile /sbin/modprobe {
|
profile /bin/ps (complain) {
|
||||||
signal (receive) peer=/usr/bin/docker,
|
/etc/ld.so.cache r,
|
||||||
capability sys_module,
|
/etc/localtime r,
|
||||||
file,
|
/etc/passwd r,
|
||||||
|
/etc/nsswitch.conf r,
|
||||||
|
/lib/** r,
|
||||||
|
/proc/[0-9]*/** r,
|
||||||
|
/dev/null rw,
|
||||||
|
/bin/ps mr,
|
||||||
|
|
||||||
|
# We don't need ptrace so we'll deny and ignore the error.
|
||||||
|
deny ptrace (read, trace),
|
||||||
|
|
||||||
|
# Quiet dac_override denials
|
||||||
|
deny capability dac_override,
|
||||||
|
deny capability dac_read_search,
|
||||||
|
deny capability sys_ptrace,
|
||||||
|
|
||||||
|
/dev/tty r,
|
||||||
|
/proc/stat r,
|
||||||
|
/proc/cpuinfo r,
|
||||||
|
/proc/meminfo r,
|
||||||
|
/proc/uptime r,
|
||||||
|
/sys/devices/system/cpu/online r,
|
||||||
|
/proc/sys/kernel/pid_max r,
|
||||||
|
/proc/ r,
|
||||||
|
/proc/tty/drivers r,
|
||||||
|
}
|
||||||
|
profile /sbin/iptables (complain) {
|
||||||
|
signal (receive) peer=/usr/bin/docker,
|
||||||
|
capability net_admin,
|
||||||
|
}
|
||||||
|
profile /sbin/auplink flags=(attach_disconnected, complain) {
|
||||||
|
signal (receive) peer=/usr/bin/docker,
|
||||||
|
capability sys_admin,
|
||||||
|
capability dac_override,
|
||||||
|
|
||||||
|
@{DOCKER_GRAPH_PATH}/aufs/** rw,
|
||||||
|
@{DOCKER_GRAPH_PATH}/tmp/** rw,
|
||||||
|
# For user namespaces:
|
||||||
|
@{DOCKER_GRAPH_PATH}/[0-9]*.[0-9]*/** rw,
|
||||||
|
|
||||||
|
/sys/fs/aufs/** r,
|
||||||
|
/lib/** r,
|
||||||
|
/apparmor/.null r,
|
||||||
|
/dev/null rw,
|
||||||
|
/etc/ld.so.cache r,
|
||||||
|
/sbin/auplink rm,
|
||||||
|
/proc/fs/aufs/** rw,
|
||||||
|
/proc/[0-9]*/mounts rw,
|
||||||
|
}
|
||||||
|
profile /sbin/modprobe /bin/kmod (complain) {
|
||||||
|
signal (receive) peer=/usr/bin/docker,
|
||||||
|
capability sys_module,
|
||||||
|
/etc/ld.so.cache r,
|
||||||
|
/lib/** r,
|
||||||
|
/dev/null rw,
|
||||||
|
/apparmor/.null rw,
|
||||||
|
/sbin/modprobe rm,
|
||||||
|
/bin/kmod rm,
|
||||||
|
/proc/cmdline r,
|
||||||
|
/sys/module/** r,
|
||||||
|
/etc/modprobe.d{/,/**} r,
|
||||||
}
|
}
|
||||||
# xz works via pipes, so we do not need access to the filesystem.
|
# xz works via pipes, so we do not need access to the filesystem.
|
||||||
profile /usr/bin/xz {
|
profile /usr/bin/xz (complain) {
|
||||||
signal (receive) peer=/usr/bin/docker,
|
signal (receive) peer=/usr/bin/docker,
|
||||||
|
/etc/ld.so.cache r,
|
||||||
|
/lib/** r,
|
||||||
|
/usr/bin/xz rm,
|
||||||
|
deny /proc/** rw,
|
||||||
|
deny /sys/** rw,
|
||||||
|
}
|
||||||
|
profile /sbin/xtables-multi (attach_disconnected, complain) {
|
||||||
|
/etc/ld.so.cache r,
|
||||||
|
/lib/** r,
|
||||||
|
/sbin/xtables-multi rm,
|
||||||
|
/apparmor/.null w,
|
||||||
|
/dev/null rw,
|
||||||
|
capability net_raw,
|
||||||
|
capability net_admin,
|
||||||
|
network raw,
|
||||||
|
}
|
||||||
|
profile /sbin/zfs (attach_disconnected, complain) {
|
||||||
|
file,
|
||||||
|
capability,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
146
daemon/execdriver/native/apparmor.go
Normal file
146
daemon/execdriver/native/apparmor.go
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package native
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/opencontainers/runc/libcontainer/apparmor"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
apparmorProfilePath = "/etc/apparmor.d/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
type data struct {
|
||||||
|
Name string
|
||||||
|
Imports []string
|
||||||
|
InnerImports []string
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseTemplate = `
|
||||||
|
{{range $value := .Imports}}
|
||||||
|
{{$value}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
profile {{.Name}} flags=(attach_disconnected,mediate_deleted) {
|
||||||
|
{{range $value := .InnerImports}}
|
||||||
|
{{$value}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
network,
|
||||||
|
capability,
|
||||||
|
file,
|
||||||
|
umount,
|
||||||
|
|
||||||
|
deny @{PROC}/sys/fs/** wklx,
|
||||||
|
deny @{PROC}/fs/** wklx,
|
||||||
|
deny @{PROC}/sysrq-trigger rwklx,
|
||||||
|
deny @{PROC}/mem rwklx,
|
||||||
|
deny @{PROC}/kmem rwklx,
|
||||||
|
deny @{PROC}/kore rwklx,
|
||||||
|
deny @{PROC}/sys/kernel/[^s][^h][^m]* wklx,
|
||||||
|
deny @{PROC}/sys/kernel/*/** wklx,
|
||||||
|
|
||||||
|
deny mount,
|
||||||
|
deny ptrace,
|
||||||
|
|
||||||
|
deny /sys/[^f]*/** wklx,
|
||||||
|
deny /sys/f[^s]*/** wklx,
|
||||||
|
deny /sys/fs/[^c]*/** wklx,
|
||||||
|
deny /sys/fs/c[^g]*/** wklx,
|
||||||
|
deny /sys/fs/cg[^r]*/** wklx,
|
||||||
|
deny /sys/firmware/efi/efivars/** rwklx,
|
||||||
|
deny /sys/kernel/security/** rwklx,
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func generateProfile(out io.Writer) error {
|
||||||
|
compiled, err := template.New("apparmor_profile").Parse(baseTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data := &data{
|
||||||
|
Name: "docker-default",
|
||||||
|
}
|
||||||
|
if tunablesExists() {
|
||||||
|
data.Imports = append(data.Imports, "#include <tunables/global>")
|
||||||
|
} else {
|
||||||
|
data.Imports = append(data.Imports, "@{PROC}=/proc/")
|
||||||
|
}
|
||||||
|
if abstractionsExists() {
|
||||||
|
data.InnerImports = append(data.InnerImports, "#include <abstractions/base>")
|
||||||
|
}
|
||||||
|
if err := compiled.Execute(out, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the tunables/global exist
|
||||||
|
func tunablesExists() bool {
|
||||||
|
_, err := os.Stat("/etc/apparmor.d/tunables/global")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if abstractions/base exist
|
||||||
|
func abstractionsExists() bool {
|
||||||
|
_, err := os.Stat("/etc/apparmor.d/abstractions/base")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func installAppArmorProfile() error {
|
||||||
|
if !apparmor.IsEnabled() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure /etc/apparmor.d exists
|
||||||
|
if err := os.MkdirAll(path.Dir(apparmorProfilePath), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.OpenFile(apparmorProfilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := generateProfile(f); err != nil {
|
||||||
|
f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
cmd := exec.Command("/sbin/apparmor_parser", "-r", "-W", "docker")
|
||||||
|
// to use the parser directly we have to make sure we are in the correct
|
||||||
|
// dir with the profile
|
||||||
|
cmd.Dir = "/etc/apparmor.d"
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error loading docker apparmor profile: %s (%s)", err, output)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasAppArmorProfileLoaded(profile string) error {
|
||||||
|
file, err := os.Open("/sys/kernel/security/apparmor/profiles")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r := bufio.NewReader(file)
|
||||||
|
for {
|
||||||
|
p, err := r.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(p, profile+" ") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ import (
|
||||||
sysinfo "github.com/docker/docker/pkg/system"
|
sysinfo "github.com/docker/docker/pkg/system"
|
||||||
"github.com/docker/docker/pkg/term"
|
"github.com/docker/docker/pkg/term"
|
||||||
"github.com/opencontainers/runc/libcontainer"
|
"github.com/opencontainers/runc/libcontainer"
|
||||||
|
"github.com/opencontainers/runc/libcontainer/apparmor"
|
||||||
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
|
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
|
||||||
"github.com/opencontainers/runc/libcontainer/configs"
|
"github.com/opencontainers/runc/libcontainer/configs"
|
||||||
"github.com/opencontainers/runc/libcontainer/system"
|
"github.com/opencontainers/runc/libcontainer/system"
|
||||||
|
@ -55,6 +56,20 @@ func NewDriver(root, initPath string, options []string) (*Driver, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if apparmor.IsEnabled() {
|
||||||
|
if err := installAppArmorProfile(); err != nil {
|
||||||
|
apparmor_profiles := []string{"docker-default", "docker-unconfined"}
|
||||||
|
|
||||||
|
// Allow daemon to run if loading failed, but are active
|
||||||
|
// (possibly through another run, manually, or via system startup)
|
||||||
|
for _, policy := range apparmor_profiles {
|
||||||
|
if err := hasAppArmorProfileLoaded(policy); err != nil {
|
||||||
|
return nil, fmt.Errorf("AppArmor enabled on system but the %s profile could not be loaded.", policy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// choose cgroup manager
|
// choose cgroup manager
|
||||||
// this makes sure there are no breaking changes to people
|
// this makes sure there are no breaking changes to people
|
||||||
// who upgrade from versions without native.cgroupdriver opt
|
// who upgrade from versions without native.cgroupdriver opt
|
||||||
|
|
45
docs/security/apparmor.md
Normal file
45
docs/security/apparmor.md
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
AppArmor security profiles for Docker
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
AppArmor (Application Armor) is a security module that allows a system
|
||||||
|
administrator to associate a security profile with each program. Docker
|
||||||
|
expects to find an AppArmor policy loaded and enforced.
|
||||||
|
|
||||||
|
Container profiles are loaded automatically by Docker. A profile
|
||||||
|
for the Docker Engine itself also exists and is installed
|
||||||
|
with the official *.deb* packages. Advanced users and package
|
||||||
|
managers may find the profile for */usr/bin/docker* underneath
|
||||||
|
[contrib/apparmor](https://github.com/docker/docker/tree/master/contrib/apparmor)
|
||||||
|
in the Docker Engine source repository.
|
||||||
|
|
||||||
|
|
||||||
|
Understand the policies
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
The `docker-default` profile the default for running
|
||||||
|
containers. It is moderately protective while
|
||||||
|
providing wide application compatability.
|
||||||
|
|
||||||
|
The `docker-unconfined` profile is intended for
|
||||||
|
privileged applications and is the default when runing
|
||||||
|
a container with the *--privileged* flag.
|
||||||
|
|
||||||
|
The system's standard `unconfined` profile inherits all
|
||||||
|
system-wide policies, applying path-based policies
|
||||||
|
intended for the host system inside of containers.
|
||||||
|
This was the default for privileged containers
|
||||||
|
prior to Docker 1.8.
|
||||||
|
|
||||||
|
|
||||||
|
Overriding the profile for a container
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
Users may override the AppArmor profile using the
|
||||||
|
`security-opt` option (per-container).
|
||||||
|
|
||||||
|
For example, the following explicitly specifies the default policy:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker run --rm -it --security-opt apparmor:docker-default hello-world
|
||||||
|
```
|
||||||
|
|
|
@ -2397,7 +2397,10 @@ func (s *DockerSuite) TestRunWriteToProcAsound(c *check.C) {
|
||||||
func (s *DockerSuite) TestRunReadProcTimer(c *check.C) {
|
func (s *DockerSuite) TestRunReadProcTimer(c *check.C) {
|
||||||
testRequires(c, NativeExecDriver)
|
testRequires(c, NativeExecDriver)
|
||||||
out, code, err := dockerCmdWithError("run", "busybox", "cat", "/proc/timer_stats")
|
out, code, err := dockerCmdWithError("run", "busybox", "cat", "/proc/timer_stats")
|
||||||
if err != nil || code != 0 {
|
if code != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
c.Fatal(err)
|
c.Fatal(err)
|
||||||
}
|
}
|
||||||
if strings.Trim(out, "\n ") != "" {
|
if strings.Trim(out, "\n ") != "" {
|
||||||
|
@ -2414,7 +2417,10 @@ func (s *DockerSuite) TestRunReadProcLatency(c *check.C) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
out, code, err := dockerCmdWithError("run", "busybox", "cat", "/proc/latency_stats")
|
out, code, err := dockerCmdWithError("run", "busybox", "cat", "/proc/latency_stats")
|
||||||
if err != nil || code != 0 {
|
if code != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
c.Fatal(err)
|
c.Fatal(err)
|
||||||
}
|
}
|
||||||
if strings.Trim(out, "\n ") != "" {
|
if strings.Trim(out, "\n ") != "" {
|
||||||
|
@ -2422,6 +2428,24 @@ func (s *DockerSuite) TestRunReadProcLatency(c *check.C) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DockerSuite) TestRunReadFilteredProc(c *check.C) {
|
||||||
|
testRequires(c, Apparmor)
|
||||||
|
|
||||||
|
testReadPaths := []string{
|
||||||
|
"/proc/latency_stats",
|
||||||
|
"/proc/timer_stats",
|
||||||
|
"/proc/kcore",
|
||||||
|
}
|
||||||
|
for i, filePath := range testReadPaths {
|
||||||
|
name := fmt.Sprintf("procsieve-%d", i)
|
||||||
|
shellCmd := fmt.Sprintf("exec 3<%s", filePath)
|
||||||
|
|
||||||
|
if out, exitCode, err := dockerCmdWithError("run", "--privileged", "--security-opt", "apparmor:docker-default", "--name", name, "busybox", "sh", "-c", shellCmd); err == nil || exitCode == 0 {
|
||||||
|
c.Fatalf("Open FD for read should have failed with permission denied, got: %s, %v", out, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DockerSuite) TestMountIntoProc(c *check.C) {
|
func (s *DockerSuite) TestMountIntoProc(c *check.C) {
|
||||||
testRequires(c, NativeExecDriver)
|
testRequires(c, NativeExecDriver)
|
||||||
_, code, err := dockerCmdWithError("run", "-v", "/proc//sys", "busybox", "true")
|
_, code, err := dockerCmdWithError("run", "-v", "/proc//sys", "busybox", "true")
|
||||||
|
@ -2515,6 +2539,7 @@ func (s *DockerSuite) TestRunWriteFilteredProc(c *check.C) {
|
||||||
"/proc/sys/kernel/modprobe",
|
"/proc/sys/kernel/modprobe",
|
||||||
"/proc/sys/kernel/core_pattern",
|
"/proc/sys/kernel/core_pattern",
|
||||||
"/proc/sysrq-trigger",
|
"/proc/sysrq-trigger",
|
||||||
|
"/proc/kcore",
|
||||||
}
|
}
|
||||||
for i, filePath := range testWritePaths {
|
for i, filePath := range testWritePaths {
|
||||||
name := fmt.Sprintf("writeprocsieve-%d", i)
|
name := fmt.Sprintf("writeprocsieve-%d", i)
|
||||||
|
|
Loading…
Add table
Reference in a new issue