local_linux_test.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. //go:build linux
  2. package local // import "github.com/docker/docker/volume/local"
  3. import (
  4. "net"
  5. "os"
  6. "path/filepath"
  7. "strconv"
  8. "testing"
  9. "github.com/docker/docker/errdefs"
  10. "github.com/docker/docker/pkg/idtools"
  11. "github.com/docker/docker/quota"
  12. "gotest.tools/v3/assert"
  13. is "gotest.tools/v3/assert/cmp"
  14. )
  15. const (
  16. quotaSize = 1024 * 1024
  17. quotaSizeLiteral = "1M"
  18. )
  19. func TestQuota(t *testing.T) {
  20. if msg, ok := quota.CanTestQuota(); !ok {
  21. t.Skip(msg)
  22. }
  23. // get sparse xfs test image
  24. imageFileName, err := quota.PrepareQuotaTestImage(t)
  25. if err != nil {
  26. t.Fatal(err)
  27. }
  28. defer os.Remove(imageFileName)
  29. t.Run("testVolWithQuota", quota.WrapMountTest(imageFileName, true, testVolWithQuota))
  30. t.Run("testVolQuotaUnsupported", quota.WrapMountTest(imageFileName, false, testVolQuotaUnsupported))
  31. }
  32. func testVolWithQuota(t *testing.T, mountPoint, backingFsDev, testDir string) {
  33. r, err := New(testDir, idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()})
  34. if err != nil {
  35. t.Fatal(err)
  36. }
  37. assert.Assert(t, r.quotaCtl != nil)
  38. vol, err := r.Create("testing", map[string]string{"size": quotaSizeLiteral})
  39. if err != nil {
  40. t.Fatal(err)
  41. }
  42. dir, err := vol.Mount("1234")
  43. if err != nil {
  44. t.Fatal(err)
  45. }
  46. defer func() {
  47. if err := vol.Unmount("1234"); err != nil {
  48. t.Fatal(err)
  49. }
  50. }()
  51. testfile := filepath.Join(dir, "testfile")
  52. // test writing file smaller than quota
  53. assert.NilError(t, os.WriteFile(testfile, make([]byte, quotaSize/2), 0o644))
  54. assert.NilError(t, os.Remove(testfile))
  55. // test writing fiel larger than quota
  56. err = os.WriteFile(testfile, make([]byte, quotaSize+1), 0o644)
  57. assert.ErrorContains(t, err, "")
  58. if _, err := os.Stat(testfile); err == nil {
  59. assert.NilError(t, os.Remove(testfile))
  60. }
  61. }
  62. func testVolQuotaUnsupported(t *testing.T, mountPoint, backingFsDev, testDir string) {
  63. r, err := New(testDir, idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()})
  64. if err != nil {
  65. t.Fatal(err)
  66. }
  67. assert.Assert(t, is.Nil(r.quotaCtl))
  68. _, err = r.Create("testing", map[string]string{"size": quotaSizeLiteral})
  69. assert.ErrorContains(t, err, "no quota support")
  70. vol, err := r.Create("testing", nil)
  71. if err != nil {
  72. t.Fatal(err)
  73. }
  74. // this could happen if someone moves volumes from storage with
  75. // quota support to some place without
  76. lv, ok := vol.(*localVolume)
  77. assert.Assert(t, ok)
  78. lv.opts = &optsConfig{
  79. Quota: quota.Quota{Size: quotaSize},
  80. }
  81. _, err = vol.Mount("1234")
  82. assert.ErrorContains(t, err, "no quota support")
  83. }
  84. func TestVolCreateValidation(t *testing.T) {
  85. r, err := New(t.TempDir(), idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()})
  86. if err != nil {
  87. t.Fatal(err)
  88. }
  89. mandatoryOpts = map[string][]string{
  90. "device": {"type"},
  91. "type": {"device"},
  92. "o": {"device", "type"},
  93. }
  94. tests := []struct {
  95. doc string
  96. name string
  97. opts map[string]string
  98. expectedErr string
  99. }{
  100. {
  101. doc: "invalid: name too short",
  102. name: "a",
  103. opts: map[string]string{
  104. "type": "foo",
  105. "device": "foo",
  106. },
  107. expectedErr: `volume name is too short, names should be at least two alphanumeric characters`,
  108. },
  109. {
  110. doc: "invalid: name invalid characters",
  111. name: "hello world",
  112. opts: map[string]string{
  113. "type": "foo",
  114. "device": "foo",
  115. },
  116. expectedErr: `"hello world" includes invalid characters for a local volume name, only "[a-zA-Z0-9][a-zA-Z0-9_.-]" are allowed. If you intended to pass a host directory, use absolute path`,
  117. },
  118. {
  119. doc: "invalid: unknown option",
  120. opts: map[string]string{"hello": "world"},
  121. expectedErr: `invalid option: "hello"`,
  122. },
  123. {
  124. doc: "invalid: invalid size",
  125. opts: map[string]string{"size": "hello"},
  126. expectedErr: `invalid size: 'hello'`,
  127. },
  128. {
  129. doc: "invalid: size, but no quotactl",
  130. opts: map[string]string{"size": "1234"},
  131. expectedErr: `quota size requested but no quota support`,
  132. },
  133. {
  134. doc: "invalid: device without type",
  135. opts: map[string]string{
  136. "device": "foo",
  137. },
  138. expectedErr: `missing required option: "type"`,
  139. },
  140. {
  141. doc: "invalid: type without device",
  142. opts: map[string]string{
  143. "type": "foo",
  144. },
  145. expectedErr: `missing required option: "device"`,
  146. },
  147. {
  148. doc: "invalid: o without device",
  149. opts: map[string]string{
  150. "o": "foo",
  151. "type": "foo",
  152. },
  153. expectedErr: `missing required option: "device"`,
  154. },
  155. {
  156. doc: "invalid: o without type",
  157. opts: map[string]string{
  158. "o": "foo",
  159. "device": "foo",
  160. },
  161. expectedErr: `missing required option: "type"`,
  162. },
  163. {
  164. doc: "valid: short name, no options",
  165. name: "ab",
  166. },
  167. {
  168. doc: "valid: device and type",
  169. opts: map[string]string{
  170. "type": "foo",
  171. "device": "foo",
  172. },
  173. },
  174. {
  175. doc: "valid: device, type, and o",
  176. opts: map[string]string{
  177. "type": "foo",
  178. "device": "foo",
  179. "o": "foo",
  180. },
  181. },
  182. {
  183. doc: "cifs",
  184. opts: map[string]string{
  185. "type": "cifs",
  186. "device": "//some.example.com/thepath",
  187. "o": "foo",
  188. },
  189. },
  190. {
  191. doc: "cifs with port in url",
  192. opts: map[string]string{
  193. "type": "cifs",
  194. "device": "//some.example.com:2345/thepath",
  195. "o": "foo",
  196. },
  197. expectedErr: "port not allowed in CIFS device URL, include 'port' in 'o='",
  198. },
  199. {
  200. doc: "cifs with bad url",
  201. opts: map[string]string{
  202. "type": "cifs",
  203. "device": ":::",
  204. "o": "foo",
  205. },
  206. expectedErr: `error parsing mount device url: parse ":::": missing protocol scheme`,
  207. },
  208. }
  209. for i, tc := range tests {
  210. tc := tc
  211. t.Run(tc.doc, func(t *testing.T) {
  212. if tc.name == "" {
  213. tc.name = "vol-" + strconv.Itoa(i)
  214. }
  215. v, err := r.Create(tc.name, tc.opts)
  216. if v != nil {
  217. defer assert.Check(t, r.Remove(v))
  218. }
  219. if tc.expectedErr == "" {
  220. assert.NilError(t, err)
  221. } else {
  222. assert.Check(t, errdefs.IsInvalidParameter(err), "got: %T", err)
  223. assert.ErrorContains(t, err, tc.expectedErr)
  224. }
  225. })
  226. }
  227. }
  228. func TestVolMountOpts(t *testing.T) {
  229. tests := []struct {
  230. name string
  231. opts optsConfig
  232. expectedErr string
  233. expectedDevice, expectedOpts string
  234. }{
  235. {
  236. name: "cifs url with space",
  237. opts: optsConfig{
  238. MountType: "cifs",
  239. MountDevice: "//1.2.3.4/Program Files",
  240. },
  241. expectedDevice: "//1.2.3.4/Program Files",
  242. expectedOpts: "",
  243. },
  244. {
  245. name: "cifs resolve addr",
  246. opts: optsConfig{
  247. MountType: "cifs",
  248. MountDevice: "//example.com/Program Files",
  249. MountOpts: "addr=example.com",
  250. },
  251. expectedDevice: "//example.com/Program Files",
  252. expectedOpts: "addr=1.2.3.4",
  253. },
  254. {
  255. name: "cifs resolve device",
  256. opts: optsConfig{
  257. MountType: "cifs",
  258. MountDevice: "//example.com/Program Files",
  259. },
  260. expectedDevice: "//1.2.3.4/Program Files",
  261. },
  262. {
  263. name: "nfs dont resolve device",
  264. opts: optsConfig{
  265. MountType: "nfs",
  266. MountDevice: "//example.com/Program Files",
  267. },
  268. expectedDevice: "//example.com/Program Files",
  269. },
  270. {
  271. name: "nfs resolve addr",
  272. opts: optsConfig{
  273. MountType: "nfs",
  274. MountDevice: "//example.com/Program Files",
  275. MountOpts: "addr=example.com",
  276. },
  277. expectedDevice: "//example.com/Program Files",
  278. expectedOpts: "addr=1.2.3.4",
  279. },
  280. }
  281. ip1234 := net.ParseIP("1.2.3.4")
  282. resolveIP := func(network, addr string) (*net.IPAddr, error) {
  283. switch addr {
  284. case "example.com":
  285. return &net.IPAddr{IP: ip1234}, nil
  286. }
  287. return nil, &net.DNSError{Err: "no such host", Name: addr, IsNotFound: true}
  288. }
  289. for _, tc := range tests {
  290. tc := tc
  291. t.Run(tc.name, func(t *testing.T) {
  292. dev, opts, err := getMountOptions(&tc.opts, resolveIP)
  293. if tc.expectedErr != "" {
  294. assert.Check(t, is.ErrorContains(err, tc.expectedErr))
  295. } else {
  296. assert.Check(t, err)
  297. }
  298. assert.Check(t, is.Equal(dev, tc.expectedDevice))
  299. assert.Check(t, is.Equal(opts, tc.expectedOpts))
  300. })
  301. }
  302. }