123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- package daemon
- import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "testing"
- is "gotest.tools/v3/assert/cmp"
- "gotest.tools/v3/fs"
- containertypes "github.com/docker/docker/api/types/container"
- "github.com/docker/docker/container"
- swarmagent "github.com/moby/swarmkit/v2/agent"
- swarmapi "github.com/moby/swarmkit/v2/api"
- specs "github.com/opencontainers/runtime-spec/specs-go"
- "golang.org/x/sys/windows/registry"
- "gotest.tools/v3/assert"
- )
- func TestSetWindowsCredentialSpecInSpec(t *testing.T) {
- // we need a temp directory to act as the daemon's root
- tmpDaemonRoot := fs.NewDir(t, t.Name()).Path()
- defer func() {
- assert.NilError(t, os.RemoveAll(tmpDaemonRoot))
- }()
- daemon := &Daemon{
- root: tmpDaemonRoot,
- }
- t.Run("it does nothing if there are no security options", func(t *testing.T) {
- spec := &specs.Spec{}
- err := daemon.setWindowsCredentialSpec(&container.Container{}, spec)
- assert.NilError(t, err)
- assert.Check(t, spec.Windows == nil)
- err = daemon.setWindowsCredentialSpec(&container.Container{HostConfig: &containertypes.HostConfig{}}, spec)
- assert.NilError(t, err)
- assert.Check(t, spec.Windows == nil)
- err = daemon.setWindowsCredentialSpec(&container.Container{HostConfig: &containertypes.HostConfig{SecurityOpt: []string{}}}, spec)
- assert.NilError(t, err)
- assert.Check(t, spec.Windows == nil)
- })
- dummyContainerID := "dummy-container-ID"
- containerFactory := func(secOpt string) *container.Container {
- if !strings.Contains(secOpt, "=") {
- secOpt = "credentialspec=" + secOpt
- }
- return &container.Container{
- ID: dummyContainerID,
- HostConfig: &containertypes.HostConfig{
- SecurityOpt: []string{secOpt},
- },
- }
- }
- credSpecsDir := filepath.Join(tmpDaemonRoot, credentialSpecFileLocation)
- dummyCredFileContents := `{"We don't need no": "education"}`
- t.Run("happy path with a 'file://' option", func(t *testing.T) {
- spec := &specs.Spec{}
- // let's render a dummy cred file
- err := os.Mkdir(credSpecsDir, os.ModePerm)
- assert.NilError(t, err)
- dummyCredFileName := "dummy-cred-spec.json"
- dummyCredFilePath := filepath.Join(credSpecsDir, dummyCredFileName)
- err = os.WriteFile(dummyCredFilePath, []byte(dummyCredFileContents), 0o644)
- defer func() {
- assert.NilError(t, os.Remove(dummyCredFilePath))
- }()
- assert.NilError(t, err)
- err = daemon.setWindowsCredentialSpec(containerFactory("file://"+dummyCredFileName), spec)
- assert.NilError(t, err)
- if assert.Check(t, spec.Windows != nil) {
- assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
- }
- })
- t.Run("it's not allowed to use a 'file://' option with an absolute path", func(t *testing.T) {
- spec := &specs.Spec{}
- err := daemon.setWindowsCredentialSpec(containerFactory(`file://C:\path\to\my\credspec.json`), spec)
- assert.ErrorContains(t, err, "invalid credential spec: file:// path cannot be absolute")
- assert.Check(t, spec.Windows == nil)
- })
- t.Run("it's not allowed to use a 'file://' option breaking out of the cred specs' directory", func(t *testing.T) {
- spec := &specs.Spec{}
- err := daemon.setWindowsCredentialSpec(containerFactory(`file://..\credspec.json`), spec)
- assert.ErrorContains(t, err, fmt.Sprintf("invalid credential spec: file:// path must be under %s", credSpecsDir))
- assert.Check(t, spec.Windows == nil)
- })
- t.Run("when using a 'file://' option pointing to a file that doesn't exist, it fails gracefully", func(t *testing.T) {
- spec := &specs.Spec{}
- err := daemon.setWindowsCredentialSpec(containerFactory("file://i-dont-exist.json"), spec)
- assert.Check(t, is.ErrorContains(err, fmt.Sprintf("failed to load credential spec for container %s", dummyContainerID)))
- assert.Check(t, is.ErrorIs(err, os.ErrNotExist))
- assert.Check(t, spec.Windows == nil)
- })
- t.Run("happy path with a 'registry://' option", func(t *testing.T) {
- valueName := "my-cred-spec"
- key := &dummyRegistryKey{
- getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
- assert.Equal(t, valueName, name)
- return dummyCredFileContents, 0, nil
- },
- }
- defer setRegistryOpenKeyFunc(t, key)()
- spec := &specs.Spec{}
- assert.NilError(t, daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec))
- if assert.Check(t, spec.Windows != nil) {
- assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
- }
- assert.Check(t, key.closed)
- })
- t.Run("when using a 'registry://' option and opening the registry key fails, it fails gracefully", func(t *testing.T) {
- dummyError := fmt.Errorf("dummy error")
- defer setRegistryOpenKeyFunc(t, &dummyRegistryKey{}, dummyError)()
- spec := &specs.Spec{}
- err := daemon.setWindowsCredentialSpec(containerFactory("registry://my-cred-spec"), spec)
- assert.ErrorContains(t, err, fmt.Sprintf("registry key %s could not be opened: %v", credentialSpecRegistryLocation, dummyError))
- assert.Check(t, spec.Windows == nil)
- })
- t.Run("when using a 'registry://' option pointing to a value that doesn't exist, it fails gracefully", func(t *testing.T) {
- valueName := "my-cred-spec"
- key := &dummyRegistryKey{
- getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
- assert.Equal(t, valueName, name)
- return "", 0, registry.ErrNotExist
- },
- }
- defer setRegistryOpenKeyFunc(t, key)()
- spec := &specs.Spec{}
- err := daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec)
- assert.ErrorContains(t, err, fmt.Sprintf("registry credential spec %q for container %s was not found", valueName, dummyContainerID))
- assert.Check(t, key.closed)
- })
- t.Run("when using a 'registry://' option and reading the registry value fails, it fails gracefully", func(t *testing.T) {
- dummyError := fmt.Errorf("dummy error")
- valueName := "my-cred-spec"
- key := &dummyRegistryKey{
- getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
- assert.Equal(t, valueName, name)
- return "", 0, dummyError
- },
- }
- defer setRegistryOpenKeyFunc(t, key)()
- spec := &specs.Spec{}
- err := daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec)
- assert.ErrorContains(t, err, fmt.Sprintf("error reading credential spec %q from registry for container %s: %v", valueName, dummyContainerID, dummyError))
- assert.Check(t, key.closed)
- })
- t.Run("happy path with a 'config://' option", func(t *testing.T) {
- configID := "my-cred-spec"
- dependencyManager := swarmagent.NewDependencyManager(nil)
- dependencyManager.Configs().Add(swarmapi.Config{
- ID: configID,
- Spec: swarmapi.ConfigSpec{
- Data: []byte(dummyCredFileContents),
- },
- })
- task := &swarmapi.Task{
- Spec: swarmapi.TaskSpec{
- Runtime: &swarmapi.TaskSpec_Container{
- Container: &swarmapi.ContainerSpec{
- Configs: []*swarmapi.ConfigReference{
- {
- ConfigID: configID,
- },
- },
- },
- },
- },
- }
- cntr := containerFactory("config://" + configID)
- cntr.DependencyStore = swarmagent.Restrict(dependencyManager, task)
- spec := &specs.Spec{}
- err := daemon.setWindowsCredentialSpec(cntr, spec)
- assert.NilError(t, err)
- if assert.Check(t, spec.Windows != nil) {
- assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
- }
- })
- 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) {
- spec := &specs.Spec{}
- err := daemon.setWindowsCredentialSpec(containerFactory("config://whatever"), spec)
- assert.Equal(t, errInvalidCredentialSpecSecOpt, err)
- assert.Check(t, spec.Windows == nil)
- })
- t.Run("happy path with a 'raw://' option", func(t *testing.T) {
- spec := &specs.Spec{}
- err := daemon.setWindowsCredentialSpec(containerFactory("raw://"+dummyCredFileContents), spec)
- assert.NilError(t, err)
- if assert.Check(t, spec.Windows != nil) {
- assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
- }
- })
- t.Run("it's not case sensitive in the option names", func(t *testing.T) {
- spec := &specs.Spec{}
- err := daemon.setWindowsCredentialSpec(containerFactory("CreDENtiaLSPeC=rAw://"+dummyCredFileContents), spec)
- assert.NilError(t, err)
- if assert.Check(t, spec.Windows != nil) {
- assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
- }
- })
- t.Run("it rejects unknown options", func(t *testing.T) {
- spec := &specs.Spec{}
- err := daemon.setWindowsCredentialSpec(containerFactory("credentialspe=config://whatever"), spec)
- assert.ErrorContains(t, err, "security option not supported: credentialspe")
- assert.Check(t, spec.Windows == nil)
- })
- t.Run("it rejects unsupported credentialspec options", func(t *testing.T) {
- spec := &specs.Spec{}
- err := daemon.setWindowsCredentialSpec(containerFactory("idontexist://whatever"), spec)
- assert.Equal(t, errInvalidCredentialSpecSecOpt, err)
- assert.Check(t, spec.Windows == nil)
- })
- for _, option := range []string{"file", "registry", "config", "raw"} {
- t.Run(fmt.Sprintf("it rejects empty values for %s", option), func(t *testing.T) {
- spec := &specs.Spec{}
- err := daemon.setWindowsCredentialSpec(containerFactory(option+"://"), spec)
- assert.Equal(t, errInvalidCredentialSpecSecOpt, err)
- assert.Check(t, spec.Windows == nil)
- })
- }
- }
- /* Helpers below */
- type dummyRegistryKey struct {
- getStringValueFunc func(name string) (val string, valtype uint32, err error)
- closed bool
- }
- func (k *dummyRegistryKey) GetStringValue(name string) (val string, valtype uint32, err error) {
- return k.getStringValueFunc(name)
- }
- func (k *dummyRegistryKey) Close() error {
- k.closed = true
- return nil
- }
- // setRegistryOpenKeyFunc replaces the registryOpenKeyFunc package variable, and returns a function
- // to be called to revert the change when done with testing.
- func setRegistryOpenKeyFunc(t *testing.T, key *dummyRegistryKey, err ...error) func() {
- previousRegistryOpenKeyFunc := registryOpenKeyFunc
- registryOpenKeyFunc = func(baseKey registry.Key, path string, access uint32) (registryKey, error) {
- // this should always be called with exactly the same arguments
- assert.Equal(t, registry.LOCAL_MACHINE, baseKey)
- assert.Equal(t, credentialSpecRegistryLocation, path)
- assert.Equal(t, uint32(registry.QUERY_VALUE), access)
- if len(err) > 0 {
- return nil, err[0]
- }
- return key, nil
- }
- return func() {
- registryOpenKeyFunc = previousRegistryOpenKeyFunc
- }
- }
- func TestSetupWindowsDevices(t *testing.T) {
- t.Run("it does nothing if there are no devices", func(t *testing.T) {
- devices, err := setupWindowsDevices(nil)
- assert.NilError(t, err)
- assert.Equal(t, len(devices), 0)
- })
- t.Run("it fails if any devices are blank", func(t *testing.T) {
- devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: ""}})
- assert.ErrorContains(t, err, "invalid device assignment path")
- assert.ErrorContains(t, err, "''")
- assert.Equal(t, len(devices), 0)
- })
- t.Run("it fails if all devices do not contain '/' or '://'", func(t *testing.T) {
- devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "anything"}, {PathOnHost: "goes"}})
- assert.ErrorContains(t, err, "invalid device assignment path")
- assert.ErrorContains(t, err, "'anything'")
- assert.Equal(t, len(devices), 0)
- })
- t.Run("it fails if any devices do not contain '/' or '://'", func(t *testing.T) {
- devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: "goes"}})
- assert.ErrorContains(t, err, "invalid device assignment path")
- assert.ErrorContains(t, err, "'goes'")
- assert.Equal(t, len(devices), 0)
- })
- t.Run("it fails if all '/'-separated devices do not have IDType 'class'", func(t *testing.T) {
- devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "klass/anything"}, {PathOnHost: "klass/goes"}})
- assert.ErrorContains(t, err, "invalid device assignment path")
- assert.ErrorContains(t, err, "'klass/anything'")
- assert.Equal(t, len(devices), 0)
- })
- t.Run("it fails if any '/'-separated devices do not have IDType 'class'", func(t *testing.T) {
- devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: "klass/goes"}})
- assert.ErrorContains(t, err, "invalid device assignment path")
- assert.ErrorContains(t, err, "'klass/goes'")
- assert.Equal(t, len(devices), 0)
- })
- t.Run("it fails if any '://'-separated devices have IDType ''", func(t *testing.T) {
- devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: "://goes"}})
- assert.ErrorContains(t, err, "invalid device assignment path")
- assert.ErrorContains(t, err, "'://goes'")
- assert.Equal(t, len(devices), 0)
- })
- t.Run("it creates devices if all '/'-separated devices have IDType 'class'", func(t *testing.T) {
- devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: "class/goes"}})
- expectedDevices := []specs.WindowsDevice{{IDType: "class", ID: "anything"}, {IDType: "class", ID: "goes"}}
- assert.NilError(t, err)
- assert.Equal(t, len(devices), len(expectedDevices))
- for i := range expectedDevices {
- assert.Equal(t, devices[i], expectedDevices[i])
- }
- })
- t.Run("it creates devices if all '://'-separated devices have non-blank IDType", func(t *testing.T) {
- devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class://anything"}, {PathOnHost: "klass://goes"}})
- expectedDevices := []specs.WindowsDevice{{IDType: "class", ID: "anything"}, {IDType: "klass", ID: "goes"}}
- assert.NilError(t, err)
- assert.Equal(t, len(devices), len(expectedDevices))
- for i := range expectedDevices {
- assert.Equal(t, devices[i], expectedDevices[i])
- }
- })
- t.Run("it creates devices when given a mix of '/'-separated and '://'-separated devices", func(t *testing.T) {
- devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: "klass://goes"}})
- expectedDevices := []specs.WindowsDevice{{IDType: "class", ID: "anything"}, {IDType: "klass", ID: "goes"}}
- assert.NilError(t, err)
- assert.Equal(t, len(devices), len(expectedDevices))
- for i := range expectedDevices {
- assert.Equal(t, devices[i], expectedDevices[i])
- }
- })
- }
|