github.com/rumpl/bof@v23.0.0-rc.2+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 "gotest.tools/v3/fs" 11 12 containertypes "github.com/docker/docker/api/types/container" 13 "github.com/docker/docker/container" 14 swarmagent "github.com/moby/swarmkit/v2/agent" 15 swarmapi "github.com/moby/swarmkit/v2/api" 16 specs "github.com/opencontainers/runtime-spec/specs-go" 17 "golang.org/x/sys/windows/registry" 18 "gotest.tools/v3/assert" 19 ) 20 21 func TestSetWindowsCredentialSpecInSpec(t *testing.T) { 22 // we need a temp directory to act as the daemon's root 23 tmpDaemonRoot := fs.NewDir(t, t.Name()).Path() 24 defer func() { 25 assert.NilError(t, os.RemoveAll(tmpDaemonRoot)) 26 }() 27 28 daemon := &Daemon{ 29 root: tmpDaemonRoot, 30 } 31 32 t.Run("it does nothing if there are no security options", func(t *testing.T) { 33 spec := &specs.Spec{} 34 35 err := daemon.setWindowsCredentialSpec(&container.Container{}, spec) 36 assert.NilError(t, err) 37 assert.Check(t, spec.Windows == nil) 38 39 err = daemon.setWindowsCredentialSpec(&container.Container{HostConfig: &containertypes.HostConfig{}}, spec) 40 assert.NilError(t, err) 41 assert.Check(t, spec.Windows == nil) 42 43 err = daemon.setWindowsCredentialSpec(&container.Container{HostConfig: &containertypes.HostConfig{SecurityOpt: []string{}}}, spec) 44 assert.NilError(t, err) 45 assert.Check(t, spec.Windows == nil) 46 }) 47 48 dummyContainerID := "dummy-container-ID" 49 containerFactory := func(secOpt string) *container.Container { 50 if !strings.Contains(secOpt, "=") { 51 secOpt = "credentialspec=" + secOpt 52 } 53 return &container.Container{ 54 ID: dummyContainerID, 55 HostConfig: &containertypes.HostConfig{ 56 SecurityOpt: []string{secOpt}, 57 }, 58 } 59 } 60 61 credSpecsDir := filepath.Join(tmpDaemonRoot, credentialSpecFileLocation) 62 dummyCredFileContents := `{"We don't need no": "education"}` 63 64 t.Run("happy path with a 'file://' option", func(t *testing.T) { 65 spec := &specs.Spec{} 66 67 // let's render a dummy cred file 68 err := os.Mkdir(credSpecsDir, os.ModePerm) 69 assert.NilError(t, err) 70 dummyCredFileName := "dummy-cred-spec.json" 71 dummyCredFilePath := filepath.Join(credSpecsDir, dummyCredFileName) 72 err = os.WriteFile(dummyCredFilePath, []byte(dummyCredFileContents), 0644) 73 defer func() { 74 assert.NilError(t, os.Remove(dummyCredFilePath)) 75 }() 76 assert.NilError(t, err) 77 78 err = daemon.setWindowsCredentialSpec(containerFactory("file://"+dummyCredFileName), spec) 79 assert.NilError(t, err) 80 81 if assert.Check(t, spec.Windows != nil) { 82 assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec) 83 } 84 }) 85 86 t.Run("it's not allowed to use a 'file://' option with an absolute path", func(t *testing.T) { 87 spec := &specs.Spec{} 88 89 err := daemon.setWindowsCredentialSpec(containerFactory(`file://C:\path\to\my\credspec.json`), spec) 90 assert.ErrorContains(t, err, "invalid credential spec - file:// path cannot be absolute") 91 92 assert.Check(t, spec.Windows == nil) 93 }) 94 95 t.Run("it's not allowed to use a 'file://' option breaking out of the cred specs' directory", func(t *testing.T) { 96 spec := &specs.Spec{} 97 98 err := daemon.setWindowsCredentialSpec(containerFactory(`file://..\credspec.json`), spec) 99 assert.ErrorContains(t, err, fmt.Sprintf("invalid credential spec - file:// path must be under %s", credSpecsDir)) 100 101 assert.Check(t, spec.Windows == nil) 102 }) 103 104 t.Run("when using a 'file://' option pointing to a file that doesn't exist, it fails gracefully", func(t *testing.T) { 105 spec := &specs.Spec{} 106 107 err := daemon.setWindowsCredentialSpec(containerFactory("file://i-dont-exist.json"), spec) 108 assert.ErrorContains(t, err, fmt.Sprintf("credential spec for container %s could not be read from file", dummyContainerID)) 109 assert.ErrorContains(t, err, "The system cannot find") 110 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 }