github.com/toplink-cn/moby@v0.0.0-20240305205811-460b4aebdf81/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), 0o644)
    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  }