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