Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add --volumes-from support in run command #2241

Merged
merged 1 commit into from
Jul 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
49 changes: 49 additions & 0 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 @@ -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
Loading