oci_windows_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. package daemon
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. "strings"
  7. "testing"
  8. is "gotest.tools/v3/assert/cmp"
  9. "gotest.tools/v3/fs"
  10. containertypes "github.com/docker/docker/api/types/container"
  11. "github.com/docker/docker/container"
  12. swarmagent "github.com/moby/swarmkit/v2/agent"
  13. swarmapi "github.com/moby/swarmkit/v2/api"
  14. specs "github.com/opencontainers/runtime-spec/specs-go"
  15. "golang.org/x/sys/windows/registry"
  16. "gotest.tools/v3/assert"
  17. )
  18. func TestSetWindowsCredentialSpecInSpec(t *testing.T) {
  19. // we need a temp directory to act as the daemon's root
  20. tmpDaemonRoot := fs.NewDir(t, t.Name()).Path()
  21. defer func() {
  22. assert.NilError(t, os.RemoveAll(tmpDaemonRoot))
  23. }()
  24. daemon := &Daemon{
  25. root: tmpDaemonRoot,
  26. }
  27. t.Run("it does nothing if there are no security options", func(t *testing.T) {
  28. spec := &specs.Spec{}
  29. err := daemon.setWindowsCredentialSpec(&container.Container{}, spec)
  30. assert.NilError(t, err)
  31. assert.Check(t, spec.Windows == nil)
  32. err = daemon.setWindowsCredentialSpec(&container.Container{HostConfig: &containertypes.HostConfig{}}, spec)
  33. assert.NilError(t, err)
  34. assert.Check(t, spec.Windows == nil)
  35. err = daemon.setWindowsCredentialSpec(&container.Container{HostConfig: &containertypes.HostConfig{SecurityOpt: []string{}}}, spec)
  36. assert.NilError(t, err)
  37. assert.Check(t, spec.Windows == nil)
  38. })
  39. dummyContainerID := "dummy-container-ID"
  40. containerFactory := func(secOpt string) *container.Container {
  41. if !strings.Contains(secOpt, "=") {
  42. secOpt = "credentialspec=" + secOpt
  43. }
  44. return &container.Container{
  45. ID: dummyContainerID,
  46. HostConfig: &containertypes.HostConfig{
  47. SecurityOpt: []string{secOpt},
  48. },
  49. }
  50. }
  51. credSpecsDir := filepath.Join(tmpDaemonRoot, credentialSpecFileLocation)
  52. dummyCredFileContents := `{"We don't need no": "education"}`
  53. t.Run("happy path with a 'file://' option", func(t *testing.T) {
  54. spec := &specs.Spec{}
  55. // let's render a dummy cred file
  56. err := os.Mkdir(credSpecsDir, os.ModePerm)
  57. assert.NilError(t, err)
  58. dummyCredFileName := "dummy-cred-spec.json"
  59. dummyCredFilePath := filepath.Join(credSpecsDir, dummyCredFileName)
  60. err = os.WriteFile(dummyCredFilePath, []byte(dummyCredFileContents), 0o644)
  61. defer func() {
  62. assert.NilError(t, os.Remove(dummyCredFilePath))
  63. }()
  64. assert.NilError(t, err)
  65. err = daemon.setWindowsCredentialSpec(containerFactory("file://"+dummyCredFileName), spec)
  66. assert.NilError(t, err)
  67. if assert.Check(t, spec.Windows != nil) {
  68. assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
  69. }
  70. })
  71. t.Run("it's not allowed to use a 'file://' option with an absolute path", func(t *testing.T) {
  72. spec := &specs.Spec{}
  73. err := daemon.setWindowsCredentialSpec(containerFactory(`file://C:\path\to\my\credspec.json`), spec)
  74. assert.ErrorContains(t, err, "invalid credential spec: file:// path cannot be absolute")
  75. assert.Check(t, spec.Windows == nil)
  76. })
  77. t.Run("it's not allowed to use a 'file://' option breaking out of the cred specs' directory", func(t *testing.T) {
  78. spec := &specs.Spec{}
  79. err := daemon.setWindowsCredentialSpec(containerFactory(`file://..\credspec.json`), spec)
  80. assert.ErrorContains(t, err, fmt.Sprintf("invalid credential spec: file:// path must be under %s", credSpecsDir))
  81. assert.Check(t, spec.Windows == nil)
  82. })
  83. t.Run("when using a 'file://' option pointing to a file that doesn't exist, it fails gracefully", func(t *testing.T) {
  84. spec := &specs.Spec{}
  85. err := daemon.setWindowsCredentialSpec(containerFactory("file://i-dont-exist.json"), spec)
  86. assert.Check(t, is.ErrorContains(err, fmt.Sprintf("failed to load credential spec for container %s", dummyContainerID)))
  87. assert.Check(t, is.ErrorIs(err, os.ErrNotExist))
  88. assert.Check(t, spec.Windows == nil)
  89. })
  90. t.Run("happy path with a 'registry://' option", func(t *testing.T) {
  91. valueName := "my-cred-spec"
  92. key := &dummyRegistryKey{
  93. getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
  94. assert.Equal(t, valueName, name)
  95. return dummyCredFileContents, 0, nil
  96. },
  97. }
  98. defer setRegistryOpenKeyFunc(t, key)()
  99. spec := &specs.Spec{}
  100. assert.NilError(t, daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec))
  101. if assert.Check(t, spec.Windows != nil) {
  102. assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
  103. }
  104. assert.Check(t, key.closed)
  105. })
  106. t.Run("when using a 'registry://' option and opening the registry key fails, it fails gracefully", func(t *testing.T) {
  107. dummyError := fmt.Errorf("dummy error")
  108. defer setRegistryOpenKeyFunc(t, &dummyRegistryKey{}, dummyError)()
  109. spec := &specs.Spec{}
  110. err := daemon.setWindowsCredentialSpec(containerFactory("registry://my-cred-spec"), spec)
  111. assert.ErrorContains(t, err, fmt.Sprintf("registry key %s could not be opened: %v", credentialSpecRegistryLocation, dummyError))
  112. assert.Check(t, spec.Windows == nil)
  113. })
  114. t.Run("when using a 'registry://' option pointing to a value that doesn't exist, it fails gracefully", func(t *testing.T) {
  115. valueName := "my-cred-spec"
  116. key := &dummyRegistryKey{
  117. getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
  118. assert.Equal(t, valueName, name)
  119. return "", 0, registry.ErrNotExist
  120. },
  121. }
  122. defer setRegistryOpenKeyFunc(t, key)()
  123. spec := &specs.Spec{}
  124. err := daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec)
  125. assert.ErrorContains(t, err, fmt.Sprintf("registry credential spec %q for container %s was not found", valueName, dummyContainerID))
  126. assert.Check(t, key.closed)
  127. })
  128. t.Run("when using a 'registry://' option and reading the registry value fails, it fails gracefully", func(t *testing.T) {
  129. dummyError := fmt.Errorf("dummy error")
  130. valueName := "my-cred-spec"
  131. key := &dummyRegistryKey{
  132. getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
  133. assert.Equal(t, valueName, name)
  134. return "", 0, dummyError
  135. },
  136. }
  137. defer setRegistryOpenKeyFunc(t, key)()
  138. spec := &specs.Spec{}
  139. err := daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec)
  140. assert.ErrorContains(t, err, fmt.Sprintf("error reading credential spec %q from registry for container %s: %v", valueName, dummyContainerID, dummyError))
  141. assert.Check(t, key.closed)
  142. })
  143. t.Run("happy path with a 'config://' option", func(t *testing.T) {
  144. configID := "my-cred-spec"
  145. dependencyManager := swarmagent.NewDependencyManager(nil)
  146. dependencyManager.Configs().Add(swarmapi.Config{
  147. ID: configID,
  148. Spec: swarmapi.ConfigSpec{
  149. Data: []byte(dummyCredFileContents),
  150. },
  151. })
  152. task := &swarmapi.Task{
  153. Spec: swarmapi.TaskSpec{
  154. Runtime: &swarmapi.TaskSpec_Container{
  155. Container: &swarmapi.ContainerSpec{
  156. Configs: []*swarmapi.ConfigReference{
  157. {
  158. ConfigID: configID,
  159. },
  160. },
  161. },
  162. },
  163. },
  164. }
  165. cntr := containerFactory("config://" + configID)
  166. cntr.DependencyStore = swarmagent.Restrict(dependencyManager, task)
  167. spec := &specs.Spec{}
  168. err := daemon.setWindowsCredentialSpec(cntr, spec)
  169. assert.NilError(t, err)
  170. if assert.Check(t, spec.Windows != nil) {
  171. assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
  172. }
  173. })
  174. t.Run("using a 'config://' option on a container not managed by swarmkit is not allowed, and results in a generic error message to hide that purely internal API", func(t *testing.T) {
  175. spec := &specs.Spec{}
  176. err := daemon.setWindowsCredentialSpec(containerFactory("config://whatever"), spec)
  177. assert.Equal(t, errInvalidCredentialSpecSecOpt, err)
  178. assert.Check(t, spec.Windows == nil)
  179. })
  180. t.Run("happy path with a 'raw://' option", func(t *testing.T) {
  181. spec := &specs.Spec{}
  182. err := daemon.setWindowsCredentialSpec(containerFactory("raw://"+dummyCredFileContents), spec)
  183. assert.NilError(t, err)
  184. if assert.Check(t, spec.Windows != nil) {
  185. assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
  186. }
  187. })
  188. t.Run("it's not case sensitive in the option names", func(t *testing.T) {
  189. spec := &specs.Spec{}
  190. err := daemon.setWindowsCredentialSpec(containerFactory("CreDENtiaLSPeC=rAw://"+dummyCredFileContents), spec)
  191. assert.NilError(t, err)
  192. if assert.Check(t, spec.Windows != nil) {
  193. assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
  194. }
  195. })
  196. t.Run("it rejects unknown options", func(t *testing.T) {
  197. spec := &specs.Spec{}
  198. err := daemon.setWindowsCredentialSpec(containerFactory("credentialspe=config://whatever"), spec)
  199. assert.ErrorContains(t, err, "security option not supported: credentialspe")
  200. assert.Check(t, spec.Windows == nil)
  201. })
  202. t.Run("it rejects unsupported credentialspec options", func(t *testing.T) {
  203. spec := &specs.Spec{}
  204. err := daemon.setWindowsCredentialSpec(containerFactory("idontexist://whatever"), spec)
  205. assert.Equal(t, errInvalidCredentialSpecSecOpt, err)
  206. assert.Check(t, spec.Windows == nil)
  207. })
  208. for _, option := range []string{"file", "registry", "config", "raw"} {
  209. t.Run(fmt.Sprintf("it rejects empty values for %s", option), func(t *testing.T) {
  210. spec := &specs.Spec{}
  211. err := daemon.setWindowsCredentialSpec(containerFactory(option+"://"), spec)
  212. assert.Equal(t, errInvalidCredentialSpecSecOpt, err)
  213. assert.Check(t, spec.Windows == nil)
  214. })
  215. }
  216. }
  217. /* Helpers below */
  218. type dummyRegistryKey struct {
  219. getStringValueFunc func(name string) (val string, valtype uint32, err error)
  220. closed bool
  221. }
  222. func (k *dummyRegistryKey) GetStringValue(name string) (val string, valtype uint32, err error) {
  223. return k.getStringValueFunc(name)
  224. }
  225. func (k *dummyRegistryKey) Close() error {
  226. k.closed = true
  227. return nil
  228. }
  229. // setRegistryOpenKeyFunc replaces the registryOpenKeyFunc package variable, and returns a function
  230. // to be called to revert the change when done with testing.
  231. func setRegistryOpenKeyFunc(t *testing.T, key *dummyRegistryKey, err ...error) func() {
  232. previousRegistryOpenKeyFunc := registryOpenKeyFunc
  233. registryOpenKeyFunc = func(baseKey registry.Key, path string, access uint32) (registryKey, error) {
  234. // this should always be called with exactly the same arguments
  235. assert.Equal(t, registry.LOCAL_MACHINE, baseKey)
  236. assert.Equal(t, credentialSpecRegistryLocation, path)
  237. assert.Equal(t, uint32(registry.QUERY_VALUE), access)
  238. if len(err) > 0 {
  239. return nil, err[0]
  240. }
  241. return key, nil
  242. }
  243. return func() {
  244. registryOpenKeyFunc = previousRegistryOpenKeyFunc
  245. }
  246. }
  247. func TestSetupWindowsDevices(t *testing.T) {
  248. t.Run("it does nothing if there are no devices", func(t *testing.T) {
  249. devices, err := setupWindowsDevices(nil)
  250. assert.NilError(t, err)
  251. assert.Equal(t, len(devices), 0)
  252. })
  253. t.Run("it fails if any devices are blank", func(t *testing.T) {
  254. devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: ""}})
  255. assert.ErrorContains(t, err, "invalid device assignment path")
  256. assert.ErrorContains(t, err, "''")
  257. assert.Equal(t, len(devices), 0)
  258. })
  259. t.Run("it fails if all devices do not contain '/' or '://'", func(t *testing.T) {
  260. devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "anything"}, {PathOnHost: "goes"}})
  261. assert.ErrorContains(t, err, "invalid device assignment path")
  262. assert.ErrorContains(t, err, "'anything'")
  263. assert.Equal(t, len(devices), 0)
  264. })
  265. t.Run("it fails if any devices do not contain '/' or '://'", func(t *testing.T) {
  266. devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: "goes"}})
  267. assert.ErrorContains(t, err, "invalid device assignment path")
  268. assert.ErrorContains(t, err, "'goes'")
  269. assert.Equal(t, len(devices), 0)
  270. })
  271. t.Run("it fails if all '/'-separated devices do not have IDType 'class'", func(t *testing.T) {
  272. devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "klass/anything"}, {PathOnHost: "klass/goes"}})
  273. assert.ErrorContains(t, err, "invalid device assignment path")
  274. assert.ErrorContains(t, err, "'klass/anything'")
  275. assert.Equal(t, len(devices), 0)
  276. })
  277. t.Run("it fails if any '/'-separated devices do not have IDType 'class'", func(t *testing.T) {
  278. devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: "klass/goes"}})
  279. assert.ErrorContains(t, err, "invalid device assignment path")
  280. assert.ErrorContains(t, err, "'klass/goes'")
  281. assert.Equal(t, len(devices), 0)
  282. })
  283. t.Run("it fails if any '://'-separated devices have IDType ''", func(t *testing.T) {
  284. devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: "://goes"}})
  285. assert.ErrorContains(t, err, "invalid device assignment path")
  286. assert.ErrorContains(t, err, "'://goes'")
  287. assert.Equal(t, len(devices), 0)
  288. })
  289. t.Run("it creates devices if all '/'-separated devices have IDType 'class'", func(t *testing.T) {
  290. devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: "class/goes"}})
  291. expectedDevices := []specs.WindowsDevice{{IDType: "class", ID: "anything"}, {IDType: "class", ID: "goes"}}
  292. assert.NilError(t, err)
  293. assert.Equal(t, len(devices), len(expectedDevices))
  294. for i := range expectedDevices {
  295. assert.Equal(t, devices[i], expectedDevices[i])
  296. }
  297. })
  298. t.Run("it creates devices if all '://'-separated devices have non-blank IDType", func(t *testing.T) {
  299. devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class://anything"}, {PathOnHost: "klass://goes"}})
  300. expectedDevices := []specs.WindowsDevice{{IDType: "class", ID: "anything"}, {IDType: "klass", ID: "goes"}}
  301. assert.NilError(t, err)
  302. assert.Equal(t, len(devices), len(expectedDevices))
  303. for i := range expectedDevices {
  304. assert.Equal(t, devices[i], expectedDevices[i])
  305. }
  306. })
  307. t.Run("it creates devices when given a mix of '/'-separated and '://'-separated devices", func(t *testing.T) {
  308. devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: "klass://goes"}})
  309. expectedDevices := []specs.WindowsDevice{{IDType: "class", ID: "anything"}, {IDType: "klass", ID: "goes"}}
  310. assert.NilError(t, err)
  311. assert.Equal(t, len(devices), len(expectedDevices))
  312. for i := range expectedDevices {
  313. assert.Equal(t, devices[i], expectedDevices[i])
  314. }
  315. })
  316. }