Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ structure_test
structure-test
structure_tests.test
out/
**/bazel-*
**/bazel-*
.idea
container-structure-test
37 changes: 25 additions & 12 deletions cmd/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func Parse(fp string) (types.StructureTest, error) {
var unmarshal types.Unmarshaller
var strictUnmarshal types.Unmarshaller
var versionHolder types.SchemaVersion
var driverOptionsHolder types.DriverOptions

switch {
case strings.HasSuffix(fp, ".json"):
Expand All @@ -124,12 +125,21 @@ func Parse(fp string) (types.StructureTest, error) {
if err := unmarshal(testContents, &versionHolder); err != nil {
return nil, err
}
if err := unmarshal(testContents, &driverOptionsHolder); err != nil {
return nil, err
}

version := versionHolder.SchemaVersion
if version == "" {
return nil, errors.New("Please provide JSON schema version")
}

// We create the drivers.NewDriverFunc for the specified driver
driverImplFunc := NewDriverImplFunc(driverOptionsHolder.Driver)

// args is a global variable, need to pass in the containerRunOpts
args.ContainerRunOpts = driverOptionsHolder.ContainerRunOpts

var st types.StructureTest
if schemaVersion, ok := types.SchemaVersions[version]; ok {
st = schemaVersion()
Expand All @@ -140,19 +150,31 @@ func Parse(fp string) (types.StructureTest, error) {
strictUnmarshal(testContents, st)

tests, _ := st.(types.StructureTest) //type assertion
tests.SetDriverImpl(driverImpl, *args)
tests.SetDriverImpl(driverImplFunc, *args)
return tests, nil
}

func NewDriverImplFunc(driver string) drivers.NewDriverFunc {
// let's default to docker in the case of no driver
if driver == "" {
driver = "docker"

}
driverImpl = drivers.InitDriverImpl(driver)
if driverImpl == nil {
logrus.Fatalf("Unsupported driver type: %s", driver)
}
logrus.Infof("Using driver %s\n", driver)
return driverImpl
}

func Run() []*unversioned.FullResult {
args = &drivers.DriverConfig{
Image: imagePath,
Save: save,
Metadata: metadata,
}

var err error

if pull {
if driver != drivers.Docker {
logrus.Fatal("Image pull not supported when not using Docker driver")
Expand All @@ -178,21 +200,12 @@ func Run() []*unversioned.FullResult {
os.Exit(1)
}

driverImpl = drivers.InitDriverImpl(driver)
if driverImpl == nil {
logrus.Fatalf("Unsupported driver type: %s", driver)
}
if err != nil {
logrus.Fatal(err.Error())
}
logrus.Infof("Using driver %s\n", driver)
return RunTests()
}

func init() {
RootCmd.AddCommand(TestCmd)
TestCmd.Flags().StringVar(&imagePath, "image", "", "path to test image")
TestCmd.Flags().StringVar(&driver, "driver", "docker", "driver to use when running tests")
TestCmd.Flags().StringVar(&metadata, "metadata", "", "path to image metadata file")
TestCmd.Flags().BoolVar(&pull, "pull", false, "force a pull of the image before running tests")
TestCmd.Flags().BoolVar(&save, "save", false, "preserve created containers after test run")
Expand Down
1 change: 1 addition & 0 deletions pkg/drivers/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ go_library(
"docker_driver.go",
"driver.go",
"host_driver.go",
"options.go",
"tar_driver.go",
],
importpath = "github.com/GoogleCloudPlatform/container-structure-test/pkg/drivers",
Expand Down
48 changes: 32 additions & 16 deletions pkg/drivers/docker_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,42 @@ import (
"bufio"
"bytes"
"fmt"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"io"
"os"
"path"
"path/filepath"
"strings"

"github.com/pkg/errors"
"github.com/sirupsen/logrus"

"github.com/GoogleCloudPlatform/container-structure-test/pkg/types/unversioned"
"github.com/GoogleCloudPlatform/container-structure-test/pkg/utils"
docker "github.com/fsouza/go-dockerclient"
)

type DockerDriver struct {
originalImage string
currentImage string
cli docker.Client
env map[string]string
save bool
originalImage string
currentImage string
cli docker.Client
env map[string]string
save bool
containerRunOpts ContainerRunOpts
}

func NewDockerDriver(args DriverConfig) (Driver, error) {
newCli, err := docker.NewClientFromEnv()
if err != nil {
return nil, err
}

return &DockerDriver{
originalImage: args.Image,
currentImage: args.Image,
cli: *newCli,
env: nil,
save: args.Save,
originalImage: args.Image,
currentImage: args.Image,
cli: *newCli,
env: nil,
save: args.Save,
containerRunOpts: args.ContainerRunOpts,
}, nil
}

Expand Down Expand Up @@ -251,6 +255,11 @@ func (d *DockerDriver) ReadDir(target string) ([]os.FileInfo, error) {
// 3) commits the container with its changes to a new image,
// and sets that image as the new "current image"
func (d *DockerDriver) runAndCommit(env []string, command []string) (string, error) {
hostConfig := &docker.HostConfig{
Binds: d.containerRunOpts.BindMounts,
CapAdd: d.containerRunOpts.Capabilities,
Privileged: d.containerRunOpts.Privileged,
}
container, err := d.cli.CreateContainer(docker.CreateContainerOptions{
Config: &docker.Config{
Image: d.currentImage,
Expand All @@ -260,14 +269,15 @@ func (d *DockerDriver) runAndCommit(env []string, command []string) (string, err
AttachStdout: true,
AttachStderr: true,
},
HostConfig: nil,
HostConfig: hostConfig,
NetworkingConfig: nil,
})
if err != nil {
return "", errors.Wrap(err, "Error creating container")
}

if err = d.cli.StartContainer(container.ID, nil); err != nil {
err = d.cli.StartContainer(container.ID, hostConfig)
if err != nil {
return "", errors.Wrap(err, "Error creating container")
}

Expand Down Expand Up @@ -296,6 +306,12 @@ func (d *DockerDriver) runAndCommit(env []string, command []string) (string, err
}

func (d *DockerDriver) exec(env []string, command []string) (string, string, int, error) {
hostConfig := &docker.HostConfig{
Binds: d.containerRunOpts.BindMounts,
CapAdd: d.containerRunOpts.Capabilities,
Privileged: d.containerRunOpts.Privileged,
}

// first, start container from the current image
container, err := d.cli.CreateContainer(docker.CreateContainerOptions{
Config: &docker.Config{
Expand All @@ -306,7 +322,7 @@ func (d *DockerDriver) exec(env []string, command []string) (string, string, int
AttachStdout: true,
AttachStderr: true,
},
HostConfig: nil,
HostConfig: hostConfig,
NetworkingConfig: nil,
})
if err != nil {
Expand All @@ -317,7 +333,7 @@ func (d *DockerDriver) exec(env []string, command []string) (string, string, int
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)

if err = d.cli.StartContainer(container.ID, nil); err != nil {
if err = d.cli.StartContainer(container.ID, hostConfig); err != nil {
return "", "", -1, errors.Wrap(err, "Error creating container")
}

Expand Down
11 changes: 7 additions & 4 deletions pkg/drivers/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ const (
)

type DriverConfig struct {
Image string // used by Docker/Tar drivers
Save bool // used by Docker/Tar drivers
Metadata string // used by Host driver
Image string // used by Docker/Tar drivers
Save bool // used by Docker/Tar drivers
Metadata string // used by Host driver
ContainerRunOpts ContainerRunOpts // used by the Docker driver
}

type Driver interface {
Expand All @@ -56,7 +57,9 @@ type Driver interface {
Destroy()
}

func InitDriverImpl(driver string) func(DriverConfig) (Driver, error) {
type NewDriverFunc func(DriverConfig) (Driver, error)

func InitDriverImpl(driver string) NewDriverFunc {
switch driver {
// future drivers will be added here
case Docker:
Expand Down
7 changes: 7 additions & 0 deletions pkg/drivers/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package drivers

type ContainerRunOpts struct {
BindMounts []string `yaml:"bindMounts"`
Privileged bool `yaml:"privileged"`
Capabilities []string `yaml:"capabilities"`
}
5 changes: 5 additions & 0 deletions pkg/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,9 @@ type SchemaVersion struct {
SchemaVersion string `yaml:"schemaVersion"`
}

type DriverOptions struct {
Driver string `yaml:"driver"`
ContainerRunOpts drivers.ContainerRunOpts `yaml:"containerRunOpts"`
}

type Unmarshaller func([]byte, interface{}) error
18 changes: 10 additions & 8 deletions pkg/types/v2/structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ import (
)

type StructureTest struct {
DriverImpl func(drivers.DriverConfig) (drivers.Driver, error)
DriverArgs drivers.DriverConfig
GlobalEnvVars []types.EnvVar `yaml:"globalEnvVars"`
CommandTests []CommandTest `yaml:"commandTests"`
FileExistenceTests []FileExistenceTest `yaml:"fileExistenceTests"`
FileContentTests []FileContentTest `yaml:"fileContentTests"`
MetadataTest MetadataTest `yaml:"metadataTest"`
LicenseTests []LicenseTest `yaml:"licenseTests"`
DriverImpl func(drivers.DriverConfig) (drivers.Driver, error)
DriverArgs drivers.DriverConfig
// Used to pass mount/privilege arguments to the container runtime
ContainerRunOpts drivers.ContainerRunOpts `yaml:"containerRunOpts"`
GlobalEnvVars []types.EnvVar `yaml:"globalEnvVars"`
CommandTests []CommandTest `yaml:"commandTests"`
FileExistenceTests []FileExistenceTest `yaml:"fileExistenceTests"`
FileContentTests []FileContentTest `yaml:"fileContentTests"`
MetadataTest MetadataTest `yaml:"metadataTest"`
LicenseTests []LicenseTest `yaml:"licenseTests"`
}

func (st *StructureTest) NewDriver() (drivers.Driver, error) {
Expand Down
24 changes: 24 additions & 0 deletions tests/debian_test.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
schemaVersion: '2.0.0' # Make sure to test the latest schema version

driver: "docker"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added this, but i'm not sure how we should handle this in code.

It feels to me like this should be the default, and overridable by the command line flag. Let me know your thoughts on how to proceed with this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, the driver gets passed in as a CLI flag and then instantiated here: https://github.com/GoogleCloudPlatform/container-structure-test/blob/master/structure_test.go#L111

Instead, that will have to happen in the Parse() method, similar to how the schemaVersion gets parsed out of the config file: https://github.com/GoogleCloudPlatform/container-structure-test/blob/master/structure_test.go#L80

You should still be able to reuse the logic for InitDriverImpl here.


containerRunOpts:
bindMounts:
- "/tmp/test:/tmp/test"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've actually been thinking about this overnight...

I'm wondering whether we should support environment variable interpolation here. Because Docker requires absolute paths. So being able to interpolate a ~ or $PWD would be really useful.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, yeah I see what you mean. In order to do that we'd need to first parse the config, then somehow retrieve the env from the image, and then substitute that into a new config before running the tests. This is probably doable, but might get tricky especially if we're not using docker. Feel free and give it a shot though, I'd like to see what it looks like

privileged: true
capabilities:
- "sys_admin"

commandTests:
- name: 'apt-get'
command: 'apt-get'
Expand All @@ -13,6 +23,18 @@ commandTests:
command: 'sh'
args: ['-c', 'echo $PATH']
expectedOutput: ['/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin']
- name: "Test Capabilities ContainerRunOptions"
command: "capsh"
args: ["--print"]
expectedOutput:
- ".*cap_sys_admin.*"
- name: "Test bindMounts containerRunOptions"
command: "test"
args:
- "-d"
- "/tmp/test"
exitCode: 0

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to test this using the command module, since it looks like the fileExistenceTests only work on the image content. Hopefully this is all fine.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I totally understand, can you explain a little more what you're actually testing here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had tested this using the fileExistenceTests, but the test was failing. I think that's because the fileExistenceTests don't run inside the running container, but on the underlying image data? (maybe i've misunderstood).

So instead, I use the command module and run test -d /tmp/test which i've added as a bindMount. This tests that a file exists at path /tmp/test and that it's of type directory.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, so the whole idea of this framework is that nothing happens inside the "running container": a new container is created from the base image on each test, as a way to isolate the testing environment. When you make changes through Setup command for example, those get made on a running container and then committed to a new image which is used as the base image for all subsequent tests.

The idea here is that you'll want to treat your runOpts as a sort of setup, that gets run before any of the tests get run, and then committed to a new image. Then you should be able to use the fileExistence tests to test this as normal. Does that make sense?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think i understand what you mean. I'm not sure this can work with docker though.

Mounting volumes is something that happens at container creation time, and you can't commit their contents along with an image layer commit. The same goes for the privileged boolean and capabilities.

The idea here is if i wanted to bootstrap an airflow test (for instance), I might want to mount in a test configuration in order to run the command tests. This was one example of a use case of ours.

What shall we do about this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, I see what you mean.

In that case, I think we'll need to just pass the runOpts anytime we create a container in the docker driver (e.g. here). We'll need to just be careful to pass it everywhere since we can't commit this as the "base image".

One idea would be to make the runOpts a subset of the Config struct that gets passed around in the client library we're using: then you could just have a method that creates a Config with the values in the runOpts, and use that where ever we're called CreateContainer().


fileContentTests:
- name: 'Debian Sources'
excludedContents: ['.*gce_debian_mirror.*']
Expand All @@ -21,6 +43,7 @@ fileContentTests:
- name: 'Retry Policy'
expectedContents: ['Acquire::Retries 3;']
path: '/etc/apt/apt.conf.d/apt-retry'

fileExistenceTests:
- name: 'Root'
isDirectory: true
Expand All @@ -34,6 +57,7 @@ fileExistenceTests:
isDirectory: false
path: '/etc/machine-id'
shouldExist: true

licenseTests:
- debian: true
files: