2019-03-06 20:44:36 -05:00
package daemon
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
2020-02-07 08:39:24 -05:00
"gotest.tools/v3/fs"
2019-03-06 20:44:36 -05:00
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/container"
2022-04-21 17:33:07 -04:00
swarmagent "github.com/moby/swarmkit/v2/agent"
swarmapi "github.com/moby/swarmkit/v2/api"
2019-08-05 10:37:47 -04:00
specs "github.com/opencontainers/runtime-spec/specs-go"
2019-03-06 20:44:36 -05:00
"golang.org/x/sys/windows/registry"
2020-02-07 08:39:24 -05:00
"gotest.tools/v3/assert"
2019-03-06 20:44:36 -05:00
)
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 )
2021-08-24 06:10:50 -04:00
err = os . WriteFile ( dummyCredFilePath , [ ] byte ( dummyCredFileContents ) , 0644 )
2019-03-06 20:44:36 -05:00
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 . ErrorContains ( t , err , fmt . Sprintf ( "credential spec for container %s could not be read from file" , dummyContainerID ) )
assert . ErrorContains ( t , err , "The system cannot find" )
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"
2022-03-10 16:07:02 -05:00
dependencyManager := swarmagent . NewDependencyManager ( nil )
2019-03-06 20:44:36 -05:00
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
}
}
2022-03-12 05:05:55 -05:00
func TestSetupWindowsDevices ( t * testing . T ) {
2022-03-12 09:32:18 -05:00
t . Run ( "it does nothing if there are no devices" , func ( t * testing . T ) {
devices , err := setupWindowsDevices ( nil )
2022-03-12 05:05:55 -05:00
assert . NilError ( t , err )
assert . Equal ( t , len ( devices ) , 0 )
} )
2022-03-12 09:32:18 -05:00
t . Run ( "it fails if any devices are blank" , func ( t * testing . T ) {
devices , err := setupWindowsDevices ( [ ] containertypes . DeviceMapping { { PathOnHost : "class/anything" } , { PathOnHost : "" } } )
2022-03-12 05:05:55 -05:00
assert . ErrorContains ( t , err , "invalid device assignment path" )
2022-03-13 01:01:28 -05:00
assert . ErrorContains ( t , err , "''" )
2022-03-12 05:05:55 -05:00
assert . Equal ( t , len ( devices ) , 0 )
} )
2022-03-13 01:01:28 -05:00
t . Run ( "it fails if all devices do not contain '/' or '://'" , func ( t * testing . T ) {
2022-03-12 09:32:18 -05:00
devices , err := setupWindowsDevices ( [ ] containertypes . DeviceMapping { { PathOnHost : "anything" } , { PathOnHost : "goes" } } )
2022-03-12 05:05:55 -05:00
assert . ErrorContains ( t , err , "invalid device assignment path" )
2022-03-13 01:01:28 -05:00
assert . ErrorContains ( t , err , "'anything'" )
2022-03-12 05:05:55 -05:00
assert . Equal ( t , len ( devices ) , 0 )
} )
2022-03-13 01:01:28 -05:00
t . Run ( "it fails if any devices do not contain '/' or '://'" , func ( t * testing . T ) {
2022-03-12 09:32:18 -05:00
devices , err := setupWindowsDevices ( [ ] containertypes . DeviceMapping { { PathOnHost : "class/anything" } , { PathOnHost : "goes" } } )
2022-03-12 05:05:55 -05:00
assert . ErrorContains ( t , err , "invalid device assignment path" )
2022-03-13 01:01:28 -05:00
assert . ErrorContains ( t , err , "'goes'" )
2022-03-12 05:05:55 -05:00
assert . Equal ( t , len ( devices ) , 0 )
} )
2022-03-13 01:01:28 -05:00
t . Run ( "it fails if all '/'-separated devices do not have IDType 'class'" , func ( t * testing . T ) {
2022-03-12 09:32:18 -05:00
devices , err := setupWindowsDevices ( [ ] containertypes . DeviceMapping { { PathOnHost : "klass/anything" } , { PathOnHost : "klass/goes" } } )
2022-03-13 01:01:28 -05:00
assert . ErrorContains ( t , err , "invalid device assignment path" )
assert . ErrorContains ( t , err , "'klass/anything'" )
2022-03-12 05:05:55 -05:00
assert . Equal ( t , len ( devices ) , 0 )
} )
2022-03-13 01:01:28 -05:00
t . Run ( "it fails if any '/'-separated devices do not have IDType 'class'" , func ( t * testing . T ) {
2022-03-12 09:32:18 -05:00
devices , err := setupWindowsDevices ( [ ] containertypes . DeviceMapping { { PathOnHost : "class/anything" } , { PathOnHost : "klass/goes" } } )
2022-03-13 01:01:28 -05:00
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'" )
2022-03-12 05:05:55 -05:00
assert . Equal ( t , len ( devices ) , 0 )
} )
2022-03-13 01:01:28 -05:00
t . Run ( "it creates devices if all '/'-separated devices have IDType 'class'" , func ( t * testing . T ) {
2022-03-12 09:32:18 -05:00
devices , err := setupWindowsDevices ( [ ] containertypes . DeviceMapping { { PathOnHost : "class/anything" } , { PathOnHost : "class/goes" } } )
2022-03-12 05:05:55 -05:00
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 ] )
}
} )
2022-03-13 01:01:28 -05:00
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 ] )
}
} )
2022-03-12 05:05:55 -05:00
}