Skip to content

Commit 18434d7

Browse files
authored
feat: Add VolumesFrom, Tmpfs and UTSMode option (runfinch#231)
Signed-off-by: Arjun Raja Yogidas <[email protected]>
1 parent 8dc97f8 commit 18434d7

File tree

4 files changed

+281
-5
lines changed

4 files changed

+281
-5
lines changed

api/handlers/container/create.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,16 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) {
166166
CpuQuota = req.HostConfig.CPUQuota
167167
}
168168

169+
volumesFrom := []string{}
170+
if req.HostConfig.VolumesFrom != nil {
171+
volumesFrom = req.HostConfig.VolumesFrom
172+
}
173+
174+
tmpfs := []string{}
175+
if req.HostConfig.Tmpfs != nil {
176+
tmpfs = translateTmpfs(req.HostConfig.Tmpfs)
177+
}
178+
169179
globalOpt := ncTypes.GlobalCommandOptions(*h.Config)
170180
createOpt := ncTypes.ContainerCreateOptions{
171181
Stdout: nil,
@@ -229,7 +239,9 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) {
229239
// #endregion
230240

231241
// #region for volume flags
232-
Volume: volumes,
242+
Volume: volumes,
243+
VolumesFrom: volumesFrom,
244+
Tmpfs: tmpfs,
233245
// #endregion
234246

235247
// #region for env flags
@@ -287,6 +299,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) {
287299
PortMappings: portMappings,
288300
AddHost: req.HostConfig.ExtraHosts, // Extra hosts.
289301
MACAddress: req.MacAddress,
302+
UTSNamespace: req.HostConfig.UTSMode,
290303
}
291304

292305
ctx := namespaces.WithNamespace(r.Context(), h.Config.Namespace)
@@ -310,6 +323,21 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) {
310323
response.JSON(w, http.StatusCreated, containerCreateResponse{cid})
311324
}
312325

326+
// translateTmpfs converts a map of tmpfs mounts to a slice of strings in the format "DEST:OPTIONS".
327+
// Tmpfs are passed in as a map of strings,
328+
// but nerdctl expects an array of strings with format [TMPFS1:VALUE1, TMPFS2:VALUE2, ...].
329+
func translateTmpfs(tmpfs map[string]string) []string {
330+
var result []string
331+
for dest, options := range tmpfs {
332+
if options == "" {
333+
result = append(result, dest)
334+
} else {
335+
result = append(result, fmt.Sprintf("%s:%s", dest, options))
336+
}
337+
}
338+
return result
339+
}
340+
313341
// translate docker port mappings to go-cni port mappings.
314342
func translatePortMappings(portMappings nat.PortMap) ([]gocni.PortMapping, error) {
315343
ports := []gocni.PortMapping{}

api/handlers/container/create_test.go

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,48 @@ var _ = Describe("Container Create API ", func() {
775775
Expect(rr.Body).Should(MatchJSON(jsonResponse))
776776
})
777777

778+
It("should set VolumesFrom option", func() {
779+
body := []byte(`{
780+
"Image": "test-image",
781+
"HostConfig": {
782+
"VolumesFrom": [ "parent", "other:ro"]
783+
}
784+
}`)
785+
req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body))
786+
787+
createOpt.VolumesFrom = []string{"parent", "other:ro"}
788+
789+
service.EXPECT().Create(gomock.Any(), "test-image", nil, equalTo(createOpt), equalTo(netOpt)).Return(
790+
cid, nil)
791+
792+
h.create(rr, req)
793+
Expect(rr).Should(HaveHTTPStatus(http.StatusCreated))
794+
Expect(rr.Body).Should(MatchJSON(jsonResponse))
795+
})
796+
797+
It("should set Tmpfs and UTSMode option", func() {
798+
body := []byte(`{
799+
"Image": "test-image",
800+
"HostConfig": {
801+
"Tmpfs": { "/run": "rw,noexec,nosuid,size=65536k" },
802+
"UTSMode": "host"
803+
}
804+
}`)
805+
req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body))
806+
807+
// expected create options
808+
createOpt.Tmpfs = []string{"/run:rw,noexec,nosuid,size=65536k"}
809+
netOpt.UTSNamespace = "host"
810+
811+
service.EXPECT().Create(gomock.Any(), "test-image", nil, equalTo(createOpt), equalTo(netOpt)).Return(
812+
cid, nil)
813+
814+
// handler should return success message with 201 status code.
815+
h.create(rr, req)
816+
Expect(rr).Should(HaveHTTPStatus(http.StatusCreated))
817+
Expect(rr.Body).Should(MatchJSON(jsonResponse))
818+
})
819+
778820
Context("translate port mappings", func() {
779821
It("should return empty if port mappings is nil", func() {
780822
Expect(translatePortMappings(nil)).Should(BeEmpty())
@@ -843,6 +885,46 @@ var _ = Describe("Container Create API ", func() {
843885
Expect(cniPortMappings).Should(ContainElements(expected))
844886
})
845887
})
888+
889+
Context("translate tmpfs", func() {
890+
It("should return nil for nil input", func() {
891+
Expect(translateTmpfs(nil)).Should(BeNil())
892+
})
893+
894+
It("should return empty slice for empty map", func() {
895+
Expect(translateTmpfs(map[string]string{})).Should(BeEmpty())
896+
})
897+
898+
It("should handle single tmpfs mount with options", func() {
899+
input := map[string]string{
900+
"/run": "rw,noexec,nosuid,size=65536k",
901+
}
902+
expected := []string{"/run:rw,noexec,nosuid,size=65536k"}
903+
Expect(translateTmpfs(input)).Should(Equal(expected))
904+
})
905+
906+
It("should handle multiple tmpfs mounts with different options", func() {
907+
input := map[string]string{
908+
"/run": "rw,noexec,nosuid,size=65536k",
909+
"/tmp": "rw,exec,size=32768k",
910+
"/var": "",
911+
}
912+
result := translateTmpfs(input)
913+
Expect(result).Should(ConsistOf(
914+
"/run:rw,noexec,nosuid,size=65536k",
915+
"/tmp:rw,exec,size=32768k",
916+
"/var",
917+
))
918+
})
919+
920+
It("should handle tmpfs mount without options", func() {
921+
input := map[string]string{
922+
"/run": "",
923+
}
924+
expected := []string{"/run"}
925+
Expect(translateTmpfs(input)).Should(Equal(expected))
926+
})
927+
})
846928
})
847929
})
848930

@@ -917,7 +999,9 @@ func getDefaultCreateOpt(conf config.Config) types.ContainerCreateOptions {
917999
// #endregion
9181000

9191001
// #region for volume flags
920-
Volume: nil,
1002+
Volume: nil,
1003+
VolumesFrom: []string{},
1004+
Tmpfs: []string{},
9211005
// #endregion
9221006

9231007
// #region for env flags

api/types/container_types.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ type ContainerHostConfig struct {
6666
PortBindings nat.PortMap // Port mapping between the exposed port (container) and the host
6767
RestartPolicy RestartPolicy // Restart policy to be used for the container
6868
AutoRemove bool // Automatically remove container when it exits
69+
VolumesFrom []string // List of volumes to take from other container
6970
// TODO: VolumeDriver string // Name of the volume driver used to mount volumes
70-
// TODO: VolumesFrom []string // List of volumes to take from other container
7171
// TODO: ConsoleSize [2]uint // Initial console size (height,width)
7272
// TODO: Annotations map[string]string `json:",omitempty"` // Arbitrary non-identifying metadata attached to container and provided to the runtime
7373

@@ -89,8 +89,8 @@ type ContainerHostConfig struct {
8989
Privileged bool // Is the container in privileged mode
9090
// TODO: ReadonlyRootfs bool // Is the container root filesystem in read-only
9191
// TODO: SecurityOpt []string // List of string values to customize labels for MLS systems, such as SELinux. (["key=value"])
92-
// TODO: Tmpfs map[string]string `json:",omitempty"` // List of tmpfs (mounts) used for the container
93-
// TODO: UTSMode string // UTS namespace to use for the container
92+
Tmpfs map[string]string `json:",omitempty"` // List of tmpfs (mounts) used for the container
93+
UTSMode string // UTS namespace to use for the container
9494
// TODO: ShmSize int64 // Size of /dev/shm in bytes. The size must be greater than 0.
9595
// TODO: Sysctls map[string]string `json:",omitempty"` // List of Namespaced sysctls used for the container
9696
// TODO: Runtime string `json:",omitempty"` // Runtime to use with this container

e2e/tests/container_create.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,170 @@ func ContainerCreate(opt *option.Option) {
962962
Expect(responseReadIopsDevices[0].String()).Should(Equal(readIopsDevices[0].String()))
963963
Expect(responseWriteIopsDevices[0].String()).Should(Equal(writeIopsDevices[0].String()))
964964
})
965+
966+
It("should create container with volumes from another container", func() {
967+
tID := testContainerName
968+
969+
// Create temporary directories
970+
rwDir, err := os.MkdirTemp("", "rw")
971+
Expect(err).Should(BeNil())
972+
roDir, err := os.MkdirTemp("", "ro")
973+
Expect(err).Should(BeNil())
974+
defer os.RemoveAll(rwDir)
975+
defer os.RemoveAll(roDir)
976+
977+
// Create named volumes
978+
rwVolName := tID + "-rw"
979+
roVolName := tID + "-ro"
980+
command.Run(opt, "volume", "create", rwVolName)
981+
command.Run(opt, "volume", "create", roVolName)
982+
defer command.Run(opt, "volume", "rm", "-f", rwVolName)
983+
defer command.Run(opt, "volume", "rm", "-f", roVolName)
984+
985+
// Create source container with multiple volume types
986+
fromContainerName := tID + "-from"
987+
sourceOptions := types.ContainerCreateRequest{}
988+
sourceOptions.Image = defaultImage
989+
sourceOptions.Cmd = []string{"top"}
990+
sourceOptions.HostConfig.Binds = []string{
991+
fmt.Sprintf("%s:%s", rwDir, "/mnt1"),
992+
fmt.Sprintf("%s:%s:ro", roDir, "/mnt2"),
993+
fmt.Sprintf("%s:%s", rwVolName, "/mnt3"),
994+
fmt.Sprintf("%s:%s:ro", roVolName, "/mnt4"),
995+
}
996+
997+
// Create and start source container
998+
statusCode, _ := createContainer(uClient, url, fromContainerName, sourceOptions)
999+
Expect(statusCode).Should(Equal(http.StatusCreated))
1000+
command.Run(opt, "start", fromContainerName)
1001+
defer command.Run(opt, "rm", "-f", fromContainerName)
1002+
1003+
// Create target container with volumes-from
1004+
toContainerName := tID + "-to"
1005+
targetOptions := types.ContainerCreateRequest{}
1006+
targetOptions.Image = defaultImage
1007+
targetOptions.Cmd = []string{"top"}
1008+
targetOptions.HostConfig.VolumesFrom = []string{fromContainerName}
1009+
1010+
// Create and start target container
1011+
statusCode, _ = createContainer(uClient, url, toContainerName, targetOptions)
1012+
Expect(statusCode).Should(Equal(http.StatusCreated))
1013+
command.Run(opt, "start", toContainerName)
1014+
defer command.Run(opt, "rm", "-f", toContainerName)
1015+
1016+
// Test write permissions
1017+
command.Run(opt, "exec", toContainerName, "sh", "-exc", "echo -n str1 > /mnt1/file1")
1018+
command.RunWithoutSuccessfulExit(opt, "exec", toContainerName, "sh", "-exc", "echo -n str2 > /mnt2/file2")
1019+
command.Run(opt, "exec", toContainerName, "sh", "-exc", "echo -n str3 > /mnt3/file3")
1020+
command.RunWithoutSuccessfulExit(opt, "exec", toContainerName, "sh", "-exc", "echo -n str4 > /mnt4/file4")
1021+
1022+
// Remove target container
1023+
command.Run(opt, "rm", "-f", toContainerName)
1024+
1025+
// Create a new container to verify data persistence
1026+
verifyOptions := types.ContainerCreateRequest{}
1027+
verifyOptions.Image = defaultImage
1028+
verifyOptions.Cmd = []string{"sh", "-c", "cat /mnt1/file1 /mnt3/file3"}
1029+
verifyOptions.HostConfig.VolumesFrom = []string{fromContainerName}
1030+
1031+
statusCode, _ = createContainer(uClient, url, "verify-container", verifyOptions)
1032+
Expect(statusCode).Should(Equal(http.StatusCreated))
1033+
out := command.StdoutStr(opt, "start", "-a", "verify-container")
1034+
Expect(out).Should(Equal("str1str3"))
1035+
defer command.Run(opt, "rm", "-f", "verify-container")
1036+
})
1037+
1038+
It("should create a container with tmpfs mounts", func() {
1039+
// Define options
1040+
options.Cmd = []string{"sleep", "Infinity"}
1041+
options.HostConfig.Tmpfs = map[string]string{
1042+
"/tmpfs1": "rw,noexec,nosuid,size=65536k",
1043+
"/tmpfs2": "", // no options
1044+
}
1045+
1046+
// Create container
1047+
statusCode, ctr := createContainer(uClient, url, testContainerName, options)
1048+
Expect(statusCode).Should(Equal(http.StatusCreated))
1049+
Expect(ctr.ID).ShouldNot(BeEmpty())
1050+
1051+
// Start container
1052+
command.Run(opt, "start", testContainerName)
1053+
1054+
// Verify tmpfs mounts using native inspect
1055+
nativeResp := command.Stdout(opt, "inspect", "--mode=native", testContainerName)
1056+
var nativeInspect []map[string]interface{}
1057+
err := json.Unmarshal(nativeResp, &nativeInspect)
1058+
Expect(err).Should(BeNil())
1059+
Expect(nativeInspect).Should(HaveLen(1))
1060+
1061+
// Navigate to the mounts section
1062+
spec, ok := nativeInspect[0]["Spec"].(map[string]interface{})
1063+
Expect(ok).Should(BeTrue())
1064+
mounts, ok := spec["mounts"].([]interface{})
1065+
Expect(ok).Should(BeTrue())
1066+
1067+
// Verify tmpfs mounts
1068+
foundMounts := make(map[string]bool)
1069+
for _, mount := range mounts {
1070+
m := mount.(map[string]interface{})
1071+
if m["type"] == "tmpfs" {
1072+
foundMounts[m["destination"].(string)] = true
1073+
if m["destination"] == "/tmpfs1" {
1074+
options := m["options"].([]interface{})
1075+
optionsStr := make([]string, len(options))
1076+
for i, opt := range options {
1077+
optionsStr[i] = opt.(string)
1078+
}
1079+
Expect(optionsStr).Should(ContainElements(
1080+
"rw",
1081+
"noexec",
1082+
"nosuid",
1083+
"size=65536k",
1084+
))
1085+
}
1086+
}
1087+
}
1088+
})
1089+
1090+
It("should create a container with UTSMode set to host", func() {
1091+
// Define options
1092+
options.Cmd = []string{"sleep", "Infinity"}
1093+
options.HostConfig.UTSMode = "host"
1094+
1095+
// Create container
1096+
statusCode, ctr := createContainer(uClient, url, testContainerName, options)
1097+
Expect(statusCode).Should(Equal(http.StatusCreated))
1098+
Expect(ctr.ID).ShouldNot(BeEmpty())
1099+
1100+
// Start container
1101+
command.Run(opt, "start", testContainerName)
1102+
1103+
// Inspect using native format to verify UTS namespace configuration
1104+
nativeResp := command.Stdout(opt, "inspect", "--mode=native", testContainerName)
1105+
var nativeInspect []map[string]interface{}
1106+
err := json.Unmarshal(nativeResp, &nativeInspect)
1107+
Expect(err).Should(BeNil())
1108+
Expect(nativeInspect).Should(HaveLen(1))
1109+
1110+
// Navigate to the namespaces section
1111+
spec, ok := nativeInspect[0]["Spec"].(map[string]interface{})
1112+
Expect(ok).Should(BeTrue())
1113+
linux, ok := spec["linux"].(map[string]interface{})
1114+
Expect(ok).Should(BeTrue())
1115+
namespaces, ok := linux["namespaces"].([]interface{})
1116+
Expect(ok).Should(BeTrue())
1117+
1118+
// Verify UTS namespace is not present (indicating host namespace is used)
1119+
foundUTSNamespace := false
1120+
for _, ns := range namespaces {
1121+
namespace := ns.(map[string]interface{})
1122+
if namespace["type"] == "uts" {
1123+
foundUTSNamespace = true
1124+
break
1125+
}
1126+
}
1127+
Expect(foundUTSNamespace).Should(BeFalse())
1128+
})
9651129
})
9661130
}
9671131

0 commit comments

Comments
 (0)