Skip to content

Commit

Permalink
feat: add --volumes-from support in run command
Browse files Browse the repository at this point in the history
Signed-off-by: Ziwen Ning <ningziwe@amazon.com>
  • Loading branch information
ningziwen committed Jun 30, 2023
1 parent ba23d5d commit 55558dd
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 48 deletions.
4 changes: 4 additions & 0 deletions cmd/nerdctl/container_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,10 @@ func processContainerCreateOptions(cmd *cobra.Command) (opt types.ContainerCreat
if err != nil {
return
}
opt.VolumesFrom, err = cmd.Flags().GetStringArray("volumes-from")
if err != nil {
return
}
// #endregion

// #region for rootfs flags
Expand Down
2 changes: 2 additions & 0 deletions cmd/nerdctl/container_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ func setCreateFlags(cmd *cobra.Command) {
// tmpfs needs to be StringArray, not StringSlice, to prevent "/foo:size=64m,exec" from being split to {"/foo:size=64m", "exec"}
cmd.Flags().StringArray("tmpfs", nil, "Mount a tmpfs directory")
cmd.Flags().StringArray("mount", nil, "Attach a filesystem mount to the container")
// volumes-from needs to be StringArray, not StringSlice, to prevent "id1,id2" from being split to {"id1", "id2"} (compatible with Docker)
cmd.Flags().StringArray("volumes-from", nil, "Mount volumes from the specified container(s)")
// #endregion

// rootfs flags
Expand Down
53 changes: 53 additions & 0 deletions cmd/nerdctl/container_run_mount_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -631,3 +631,56 @@ func isRootfsShareableMount() bool {

return false
}

func TestRunVolumesFrom(t *testing.T) {
t.Parallel()
base := testutil.NewBase(t)
tID := testutil.Identifier(t)
rwDir, err := os.MkdirTemp(t.TempDir(), "rw")
if err != nil {
t.Fatal(err)
}
roDir, err := os.MkdirTemp(t.TempDir(), "ro")
if err != nil {
t.Fatal(err)
}
rwVolName := tID + "-rw"
roVolName := tID + "-ro"
for _, v := range []string{rwVolName, roVolName} {
defer base.Cmd("volume", "rm", "-f", v).Run()
base.Cmd("volume", "create", v).AssertOK()
}

fromContainerName := tID + "-from"
toContainerName := tID + "-to"
defer base.Cmd("rm", "-f", fromContainerName).AssertOK()
defer base.Cmd("rm", "-f", toContainerName).AssertOK()
base.Cmd("run",
"-d",
"--name", fromContainerName,
"-v", fmt.Sprintf("%s:/mnt1", rwDir),
"-v", fmt.Sprintf("%s:/mnt2:ro", roDir),
"-v", fmt.Sprintf("%s:/mnt3", rwVolName),
"-v", fmt.Sprintf("%s:/mnt4:ro", roVolName),
testutil.AlpineImage,
"top",
).AssertOK()
base.Cmd("run",
"-d",
"--name", toContainerName,
"--volumes-from", fromContainerName,
testutil.AlpineImage,
"top",
).AssertOK()
base.Cmd("exec", toContainerName, "sh", "-exc", "echo -n str1 > /mnt1/file1").AssertOK()
base.Cmd("exec", toContainerName, "sh", "-exc", "echo -n str2 > /mnt2/file2").AssertFail()
base.Cmd("exec", toContainerName, "sh", "-exc", "echo -n str3 > /mnt3/file3").AssertOK()
base.Cmd("exec", toContainerName, "sh", "-exc", "echo -n str4 > /mnt4/file4").AssertFail()
base.Cmd("rm", "-f", toContainerName).AssertOK()
base.Cmd("run",
"--rm",
"--volumes-from", fromContainerName,
testutil.AlpineImage,
"cat", "/mnt1/file1", "/mnt3/file3",
).AssertOutExactly("str1str3")
}
3 changes: 2 additions & 1 deletion docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ Volume flags:
Defaults to `1777` or world-writable.
- Options specific to `volume`:
- unimplemented options: `volume-nocopy`, `volume-label`, `volume-driver`, `volume-opt`
- :whale: `--volumes-from`: Mount volumes from the specified container(s), e.g. "--volumes-from my-container".

Rootfs flags:

Expand Down Expand Up @@ -374,7 +375,7 @@ Unimplemented `docker run` flags:
`--attach`, `--blkio-weight-device`, `--cpu-rt-*`, `--device-*`,
`--disable-content-trust`, `--domainname`, `--expose`, `--health-*`, `--ip6`, `--isolation`, `--no-healthcheck`,
`--link*`, `--mac-address`, `--publish-all`, `--sig-proxy`, `--storage-opt`,
`--userns`, `--volume-driver`, `--volumes-from`
`--userns`, `--volume-driver`

### :whale: :blue_square: nerdctl exec

Expand Down
2 changes: 2 additions & 0 deletions pkg/api/types/container_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ type ContainerCreateOptions struct {
Tmpfs []string
// Mount specifies a list of mounts to mount
Mount []string
// VolumesFrom specifies a list of specified containers to mount from
VolumesFrom []string
// #endregion

// #region for rootfs flags
Expand Down
17 changes: 17 additions & 0 deletions pkg/cmd/container/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,23 @@ func dockercompatMounts(mountPoints []*mountutil.Processed) []dockercompat.Mount
return result
}

func processeds(mountPoints []dockercompat.MountPoint) []*mountutil.Processed {
result := make([]*mountutil.Processed, len(mountPoints))
for i := range mountPoints {
mp := mountPoints[i]
result[i] = &mountutil.Processed{
Type: mp.Type,
Name: mp.Name,
Mount: specs.Mount{
Source: mp.Source,
Destination: mp.Destination,
},
Mode: mp.Mode,
}
}
return result
}

func propagateContainerdLabelsToOCIAnnotations() oci.SpecOpts {
return func(ctx context.Context, oc oci.Client, c *containers.Container, s *oci.Spec) error {
return oci.WithAnnotations(c.Labels)(ctx, oc, c, s)
Expand Down
41 changes: 0 additions & 41 deletions pkg/cmd/container/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,47 +166,6 @@ func getContainerName(containerLabels map[string]string) string {
return ""
}

type containerVolume struct {
Type string
Name string
Source string
Destination string
Mode string
RW bool
Propagation string
}

func getContainerVolumes(containerLabels map[string]string) []*containerVolume {
var vols []*containerVolume
volLabels := []string{labels.AnonymousVolumes, labels.Mounts}
for _, volLabel := range volLabels {
names, ok := containerLabels[volLabel]
if !ok {
continue
}
var (
volumes []*containerVolume
err error
)
if volLabel == labels.Mounts {
err = json.Unmarshal([]byte(names), &volumes)
}
if volLabel == labels.AnonymousVolumes {
var anonymous []string
err = json.Unmarshal([]byte(names), &anonymous)
for _, anony := range anonymous {
volumes = append(volumes, &containerVolume{Name: anony})
}

}
if err != nil {
logrus.Warn(err)
}
vols = append(vols, volumes...)
}
return vols
}

func getContainerNetworks(containerLables map[string]string) []string {
var networks []string
if names, ok := containerLables[labels.Networks]; ok {
Expand Down
7 changes: 4 additions & 3 deletions pkg/cmd/container/list_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

"github.com/containerd/containerd"
"github.com/containerd/containerd/containers"
"github.com/containerd/nerdctl/pkg/containerutil"
"github.com/sirupsen/logrus"
)

Expand All @@ -44,7 +45,7 @@ type containerFilterContext struct {
sinceFilterFuncs []func(t time.Time) bool
statusFilterFuncs []func(containerd.ProcessStatus) bool
labelFilterFuncs []func(map[string]string) bool
volumeFilterFuncs []func([]*containerVolume) bool
volumeFilterFuncs []func([]*containerutil.ContainerVolume) bool
networkFilterFuncs []func([]string) bool
}

Expand Down Expand Up @@ -187,7 +188,7 @@ func (cl *containerFilterContext) foldLabelFilter(_ context.Context, filter, val
}

func (cl *containerFilterContext) foldVolumeFilter(_ context.Context, filter, value string) error {
cl.volumeFilterFuncs = append(cl.volumeFilterFuncs, func(vols []*containerVolume) bool {
cl.volumeFilterFuncs = append(cl.volumeFilterFuncs, func(vols []*containerutil.ContainerVolume) bool {
for _, vol := range vols {
if (vol.Source != "" && vol.Source == value) ||
(vol.Destination != "" && vol.Destination == value) ||
Expand Down Expand Up @@ -337,7 +338,7 @@ func (cl *containerFilterContext) matchesVolumeFilter(info containers.Container)
if len(cl.volumeFilterFuncs) == 0 {
return true
}
vols := getContainerVolumes(info.Labels)
vols := containerutil.GetContainerVolumes(info.Labels)
for _, volumeFilterFunc := range cl.volumeFilterFuncs {
if !volumeFilterFunc(vols) {
continue
Expand Down
55 changes: 52 additions & 3 deletions pkg/cmd/container/run_mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package container

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
Expand All @@ -36,6 +37,8 @@ import (
"github.com/containerd/nerdctl/pkg/cmd/volume"
"github.com/containerd/nerdctl/pkg/idgen"
"github.com/containerd/nerdctl/pkg/imgutil"
"github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
"github.com/containerd/nerdctl/pkg/labels"
"github.com/containerd/nerdctl/pkg/mountutil"
"github.com/containerd/nerdctl/pkg/mountutil/volumestore"
"github.com/containerd/nerdctl/pkg/strutil"
Expand Down Expand Up @@ -84,8 +87,8 @@ func withMounts(mounts []specs.Mount) oci.SpecOpts {
}
}

// parseMountFlags parses --volume, --mount and --tmpfs.
func parseMountFlags(volStore volumestore.VolumeStore, options types.ContainerCreateOptions) ([]*mountutil.Processed, error) {
// parseMountFlags parses --volume, --mount, --tmpfs and --volumes-from.
func parseMountFlags(volStore volumestore.VolumeStore, options types.ContainerCreateOptions, ctx context.Context, client *containerd.Client) ([]*mountutil.Processed, error) {
var parsed []*mountutil.Processed //nolint:prealloc
for _, v := range strutil.DedupeStrSlice(options.Volume) {
x, err := mountutil.ProcessFlagV(v, volStore)
Expand Down Expand Up @@ -215,7 +218,7 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm
}
}

if parsed, err := parseMountFlags(volStore, options); err != nil {
if parsed, err := parseMountFlags(volStore, options, ctx, client); err != nil {
return nil, nil, nil, err
} else if len(parsed) > 0 {
ociMounts := make([]specs.Mount, len(parsed))
Expand Down Expand Up @@ -292,6 +295,52 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm
}

opts = append(opts, withMounts(userMounts))

containers, err := client.Containers(ctx)
if err != nil {
return nil, nil, nil, err
}

vfSet := strutil.SliceToSet(options.VolumesFrom)
var vfMountPoints []dockercompat.MountPoint
var vfAnonVolumes []string

for _, c := range containers {
ls, err := c.Labels(ctx)
if err != nil {
return nil, nil, nil, err
}
_, idMatch := vfSet[c.ID()]
nameMatch := false
if name, found := ls[labels.Name]; found {
_, nameMatch = vfSet[name]
}

if idMatch || nameMatch {
if av, found := ls[labels.AnonymousVolumes]; found {
err = json.Unmarshal([]byte(av), &vfAnonVolumes)
if err != nil {
return nil, nil, nil, err
}
}
if m, found := ls[labels.Mounts]; found {
err = json.Unmarshal([]byte(m), &vfMountPoints)
if err != nil {
return nil, nil, nil, err
}
}

ps := processeds(vfMountPoints)
s, err := c.Spec(ctx)
if err != nil {
return nil, nil, nil, err
}
opts = append(opts, withMounts(s.Mounts))
anonVolumes = append(anonVolumes, vfAnonVolumes...)
mountPoints = append(mountPoints, ps...)
}
}

return opts, anonVolumes, mountPoints, nil
}

Expand Down
49 changes: 49 additions & 0 deletions pkg/containerutil/containerutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package containerutil

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -498,3 +499,51 @@ func ContainerStateDirPath(ns, dataStore, id string) (string, error) {
}
return filepath.Join(dataStore, "containers", ns, id), nil
}

// ContainerVolume is a struct representing a volume in a container.
type ContainerVolume struct {
Type string
Name string
Source string
Destination string
Mode string
RW bool
Propagation string
}

// GetContainerVolumes is a function that returns a slice of containerVolume pointers.
// It accepts a map of container labels as input, where key is the label name and value is its associated value.
// The function iterates over the predefined volume labels (AnonymousVolumes and Mounts)
// and for each, it checks if the labels exists in the provided container labels.
// If yes, it decodes the label value from JSON format and appends the volumes to the result.
// In case of error during decoding, it logs the error and continues to the next label.
func GetContainerVolumes(containerLabels map[string]string) []*ContainerVolume {
var vols []*ContainerVolume
volLabels := []string{labels.AnonymousVolumes, labels.Mounts}
for _, volLabel := range volLabels {
names, ok := containerLabels[volLabel]
if !ok {
continue
}
var (
volumes []*ContainerVolume
err error
)
if volLabel == labels.Mounts {
err = json.Unmarshal([]byte(names), &volumes)
}
if volLabel == labels.AnonymousVolumes {
var anonymous []string
err = json.Unmarshal([]byte(names), &anonymous)
for _, anony := range anonymous {
volumes = append(volumes, &ContainerVolume{Name: anony})
}

}
if err != nil {
logrus.Warn(err)
}
vols = append(vols, volumes...)
}
return vols
}
13 changes: 13 additions & 0 deletions pkg/strutil/strutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,19 @@ func DedupeStrSlice(in []string) []string {
return res
}

// SliceToSet converts a slice of strings into a set.
// In Go, a set is often represented as a map with keys as the set elements and values as boolean.
// This function iterates over the slice, adding each string as a key in the map.
// The corresponding map value is set to true, serving as a placeholder.
// The resulting map can be used to quickly check the presence of an element in the set.
func SliceToSet(in []string) map[string]bool {
set := make(map[string]bool)
for _, s := range in {
set[s] = true
}
return set
}

// ParseCSVMap parses a string like "foo=x,bar=y" into a map
func ParseCSVMap(s string) (map[string]string, error) {
csvR := csv.NewReader(strings.NewReader(s))
Expand Down
11 changes: 11 additions & 0 deletions pkg/strutil/strutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ func TestDedupeStrSlice(t *testing.T) {

}

func TestSliceToSet(t *testing.T) {
assert.DeepEqual(t,
map[string]bool{"apple": true, "banana": true, "chocolate": true},
SliceToSet([]string{"apple", "banana", "apple", "chocolate"}))

assert.DeepEqual(t,
map[string]bool{"apple": true, "banana": true, "chocolate": true},
SliceToSet([]string{"apple", "apple", "banana", "chocolate", "apple"}))

}

func TestTrimStrSliceRight(t *testing.T) {
assert.DeepEqual(t,
[]string{"foo", "bar", "baz"},
Expand Down

0 comments on commit 55558dd

Please sign in to comment.