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  }