From 55d8e002e4b48eef7f6319c3bb8698b7498a5377 Mon Sep 17 00:00:00 2001 From: Julian Friedman Date: Mon, 7 Sep 2015 16:17:40 +0100 Subject: [PATCH] Create depot directories for containers Signed-off-by: Gareth Smith --- README.md | 6 + cmd/guardian/main.go | 89 +++++++++++ gardener/fakes/fake_containerizer.go | 53 +++++++ gardener/gardener.go | 140 +++++++++++++++++ gardener/gardener_suite_test.go | 13 ++ gardener/gardener_test.go | 37 +++++ gqt/create_test.go | 42 ++++++ gqt/gqt_suite_test.go | 13 ++ gqt/runner/runner.go | 217 +++++++++++++++++++++++++++ rundmc/README.md | 21 +++ rundmc/dirdepot.go | 17 +++ rundmc/dirdepot_test.go | 38 +++++ rundmc/fakes/fake_depot.go | 53 +++++++ rundmc/rundmc.go | 17 +++ rundmc/rundmc_suite_test.go | 13 ++ rundmc/rundmc_test.go | 26 ++++ 16 files changed, 795 insertions(+) create mode 100644 cmd/guardian/main.go create mode 100644 gardener/fakes/fake_containerizer.go create mode 100644 gardener/gardener.go create mode 100644 gardener/gardener_suite_test.go create mode 100644 gardener/gardener_test.go create mode 100644 gqt/create_test.go create mode 100644 gqt/gqt_suite_test.go create mode 100644 gqt/runner/runner.go create mode 100644 rundmc/README.md create mode 100644 rundmc/dirdepot.go create mode 100644 rundmc/dirdepot_test.go create mode 100644 rundmc/fakes/fake_depot.go create mode 100644 rundmc/rundmc.go create mode 100644 rundmc/rundmc_suite_test.go create mode 100644 rundmc/rundmc_test.go diff --git a/README.md b/README.md index ae7256b9d..23a9c2dfc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ # Guardian A simple single-host OCI container manager. + +## Components + + - **Gardeners Question Time (GQT):** A venerable British radio programme. And also a test suite. + - **Gardener:** Orchestrates the other components. Implements the Cloud Foundry Garden API. + - **RunDMC:** A tiny wrappper around RunC to manage a collection of RunC containers. diff --git a/cmd/guardian/main.go b/cmd/guardian/main.go new file mode 100644 index 000000000..be8f8ec49 --- /dev/null +++ b/cmd/guardian/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "flag" + "os" + "os/signal" + "syscall" + + "github.com/cloudfoundry-incubator/cf-debug-server" + "github.com/cloudfoundry-incubator/cf-lager" + "github.com/cloudfoundry-incubator/garden/server" + "github.com/cloudfoundry-incubator/guardian/gardener" + "github.com/cloudfoundry-incubator/guardian/rundmc" + "github.com/pivotal-golang/lager" +) + +var listenNetwork = flag.String( + "listenNetwork", + "unix", + "how to listen on the address (unix, tcp, etc.)", +) + +var listenAddr = flag.String( + "listenAddr", + "/tmp/garden.sock", + "address to listen on", +) + +var depotPath = flag.String( + "depot", + "", + "directory in which to store containers", +) + +var graceTime = flag.Duration( + "containerGraceTime", + 0, + "time after which to destroy idle containers", +) + +func main() { + cf_debug_server.AddFlags(flag.CommandLine) + cf_lager.AddFlags(flag.CommandLine) + flag.Parse() + + logger, _ := cf_lager.New("guardian") + + if *depotPath == "" { + missing("-depot") + } + + backend := &gardener.Gardener{ + Containerizer: &rundmc.Containerizer{ + Depot: &rundmc.DirectoryDepot{ + Dir: *depotPath, + }, + }, + } + + gardenServer := server.New(*listenNetwork, *listenAddr, *graceTime, backend, logger) + + err := gardenServer.Start() + if err != nil { + logger.Fatal("failed-to-start-server", err) + } + + signals := make(chan os.Signal, 1) + + go func() { + <-signals + gardenServer.Stop() + os.Exit(0) + }() + + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + + logger.Info("started", lager.Data{ + "network": *listenNetwork, + "addr": *listenAddr, + }) + + select {} +} + +func missing(flagName string) { + println("missing " + flagName) + println() + flag.Usage() +} diff --git a/gardener/fakes/fake_containerizer.go b/gardener/fakes/fake_containerizer.go new file mode 100644 index 000000000..39ccf1fe7 --- /dev/null +++ b/gardener/fakes/fake_containerizer.go @@ -0,0 +1,53 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + "github.com/cloudfoundry-incubator/guardian/gardener" +) + +type FakeContainerizer struct { + CreateStub func(spec gardener.DesiredContainerSpec) error + createMutex sync.RWMutex + createArgsForCall []struct { + spec gardener.DesiredContainerSpec + } + createReturns struct { + result1 error + } +} + +func (fake *FakeContainerizer) Create(spec gardener.DesiredContainerSpec) error { + fake.createMutex.Lock() + fake.createArgsForCall = append(fake.createArgsForCall, struct { + spec gardener.DesiredContainerSpec + }{spec}) + fake.createMutex.Unlock() + if fake.CreateStub != nil { + return fake.CreateStub(spec) + } else { + return fake.createReturns.result1 + } +} + +func (fake *FakeContainerizer) CreateCallCount() int { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return len(fake.createArgsForCall) +} + +func (fake *FakeContainerizer) CreateArgsForCall(i int) gardener.DesiredContainerSpec { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return fake.createArgsForCall[i].spec +} + +func (fake *FakeContainerizer) CreateReturns(result1 error) { + fake.CreateStub = nil + fake.createReturns = struct { + result1 error + }{result1} +} + +var _ gardener.Containerizer = new(FakeContainerizer) diff --git a/gardener/gardener.go b/gardener/gardener.go new file mode 100644 index 000000000..24fa5fa5c --- /dev/null +++ b/gardener/gardener.go @@ -0,0 +1,140 @@ +package gardener + +import ( + "io" + "time" + + "github.com/cloudfoundry-incubator/garden" +) + +//go:generate counterfeiter . Containerizer +type Containerizer interface { + Create(spec DesiredContainerSpec) error +} + +type DesiredContainerSpec struct { + Handle string +} + +type Gardener struct { + Containerizer Containerizer +} + +func (g *Gardener) Create(spec garden.ContainerSpec) (garden.Container, error) { + g.Containerizer.Create(DesiredContainerSpec{ + Handle: spec.Handle, + }) + + return &container{}, nil +} + +func (g *Gardener) Start() error { return nil } +func (g *Gardener) Stop() {} +func (g *Gardener) GraceTime(garden.Container) time.Duration { return 0 } +func (g *Gardener) Ping() error { return nil } +func (g *Gardener) Capacity() (garden.Capacity, error) { return garden.Capacity{}, nil } +func (g *Gardener) Destroy(handle string) error { return nil } +func (g *Gardener) Containers(garden.Properties) ([]garden.Container, error) { return nil, nil } + +func (g *Gardener) BulkInfo(handles []string) (map[string]garden.ContainerInfoEntry, error) { + return nil, nil +} + +func (g *Gardener) BulkMetrics(handles []string) (map[string]garden.ContainerMetricsEntry, error) { + return nil, nil +} + +func (g *Gardener) Lookup(handle string) (garden.Container, error) { + return nil, nil +} + +type container struct { +} + +func (c *container) Handle() string { + return "" +} + +func (c *container) Run(spec garden.ProcessSpec, io garden.ProcessIO) (garden.Process, error) { + return nil, nil +} + +func (c *container) Stop(kill bool) error { + return nil +} + +func (c *container) Info() (garden.ContainerInfo, error) { + return garden.ContainerInfo{}, nil +} + +func (c *container) StreamIn(garden.StreamInSpec) error { + return nil +} + +func (c *container) StreamOut(garden.StreamOutSpec) (io.ReadCloser, error) { + return nil, nil +} + +func (c *container) LimitBandwidth(limits garden.BandwidthLimits) error { + return nil +} + +func (c *container) CurrentBandwidthLimits() (garden.BandwidthLimits, error) { + return garden.BandwidthLimits{}, nil +} + +func (c *container) LimitCPU(limits garden.CPULimits) error { + return nil +} + +func (c *container) CurrentCPULimits() (garden.CPULimits, error) { + return garden.CPULimits{}, nil +} + +func (c *container) LimitDisk(limits garden.DiskLimits) error { + return nil +} + +func (c *container) CurrentDiskLimits() (garden.DiskLimits, error) { + return garden.DiskLimits{}, nil +} + +func (c *container) LimitMemory(limits garden.MemoryLimits) error { + return nil +} + +func (c *container) CurrentMemoryLimits() (garden.MemoryLimits, error) { + return garden.MemoryLimits{}, nil +} + +func (c *container) NetIn(hostPort, containerPort uint32) (uint32, uint32, error) { + return 0, 0, nil +} + +func (c *container) NetOut(netOutRule garden.NetOutRule) error { + return nil +} + +func (c *container) Attach(processID uint32, io garden.ProcessIO) (garden.Process, error) { + return nil, nil +} + +func (c *container) Metrics() (garden.Metrics, error) { + return garden.Metrics{}, nil +} + +func (c *container) Properties() (garden.Properties, error) { + return nil, nil +} + +func (c *container) Property(name string) (string, error) { + return "", nil +} + +func (c *container) SetProperty(name string, value string) error { + return nil +} + +func (c *container) RemoveProperty(name string) error { + return nil +} diff --git a/gardener/gardener_suite_test.go b/gardener/gardener_suite_test.go new file mode 100644 index 000000000..e91f5a249 --- /dev/null +++ b/gardener/gardener_suite_test.go @@ -0,0 +1,13 @@ +package gardener_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestGardener(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Gardener Suite") +} diff --git a/gardener/gardener_test.go b/gardener/gardener_test.go new file mode 100644 index 000000000..03c84ea89 --- /dev/null +++ b/gardener/gardener_test.go @@ -0,0 +1,37 @@ +package gardener_test + +import ( + "github.com/cloudfoundry-incubator/garden" + "github.com/cloudfoundry-incubator/guardian/gardener" + "github.com/cloudfoundry-incubator/guardian/gardener/fakes" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Gardener", func() { + var ( + containerizer *fakes.FakeContainerizer + gdnr *gardener.Gardener + ) + + BeforeEach(func() { + containerizer = new(fakes.FakeContainerizer) + gdnr = &gardener.Gardener{ + Containerizer: containerizer, + } + }) + + Describe("creating a container", func() { + It("asks the containerizer to create a container", func() { + _, err := gdnr.Create(garden.ContainerSpec{ + Handle: "bob", + }) + + Expect(err).NotTo(HaveOccurred()) + Expect(containerizer.CreateCallCount()).To(Equal(1)) + Expect(containerizer.CreateArgsForCall(0)).To(Equal(gardener.DesiredContainerSpec{ + Handle: "bob", + })) + }) + }) +}) diff --git a/gqt/create_test.go b/gqt/create_test.go new file mode 100644 index 000000000..e8e427c63 --- /dev/null +++ b/gqt/create_test.go @@ -0,0 +1,42 @@ +package gqt_test + +import ( + "path/filepath" + + "github.com/cloudfoundry-incubator/garden" + "github.com/cloudfoundry-incubator/guardian/gqt/runner" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Creating a Container", func() { + var client *runner.RunningGarden + + Context("after creating a container", func() { + BeforeEach(func() { + client = startGarden() + _, err := client.Create(garden.ContainerSpec{ + Handle: "fred", + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should create a depot subdirectory based on the container handle", func() { + Expect(filepath.Join(client.DepotDir, "fred")).To(BeADirectory()) + }) + + Describe("created container directories", func() { + It("should have a config.json", func() { + Expect(filepath.Join(client.DepotDir, "fred", "config.json")).To(BeARegularFile()) + }) + }) + }) +}) + +func startGarden(argv ...string) *runner.RunningGarden { + gardenBin, err := gexec.Build("github.com/cloudfoundry-incubator/guardian/cmd/guardian") + Expect(err).NotTo(HaveOccurred()) + + return runner.Start(gardenBin, argv...) +} diff --git a/gqt/gqt_suite_test.go b/gqt/gqt_suite_test.go new file mode 100644 index 000000000..6f95402d7 --- /dev/null +++ b/gqt/gqt_suite_test.go @@ -0,0 +1,13 @@ +package gqt_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestGqt(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Gqt Suite") +} diff --git a/gqt/runner/runner.go b/gqt/runner/runner.go new file mode 100644 index 000000000..c91d0b154 --- /dev/null +++ b/gqt/runner/runner.go @@ -0,0 +1,217 @@ +package runner + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "time" + + "github.com/cloudfoundry-incubator/garden/client" + "github.com/cloudfoundry-incubator/garden/client/connection" + "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/pivotal-golang/lager" + "github.com/pivotal-golang/lager/lagertest" + "github.com/tedsuo/ifrit" + "github.com/tedsuo/ifrit/ginkgomon" +) + +var RootFSPath = os.Getenv("GARDEN_TEST_ROOTFS") +var GraphRoot = os.Getenv("GARDEN_TEST_GRAPHPATH") + +type RunningGarden struct { + client.Client + process ifrit.Process + + Pid int + + tmpdir string + + DepotDir string + GraphRoot string + GraphPath string + + logger lager.Logger +} + +func Start(bin string, argv ...string) *RunningGarden { + network := "unix" + addr := fmt.Sprintf("/tmp/garden_%d.sock", GinkgoParallelNode()) + tmpDir := filepath.Join( + os.TempDir(), + fmt.Sprintf("test-garden-%d", ginkgo.GinkgoParallelNode()), + ) + + if GraphRoot == "" { + GraphRoot = filepath.Join(tmpDir, "graph") + } + + graphPath := filepath.Join(GraphRoot, fmt.Sprintf("node-%d", ginkgo.GinkgoParallelNode())) + depotDir := filepath.Join(tmpDir, "containers") + + r := &RunningGarden{ + DepotDir: depotDir, + + GraphRoot: GraphRoot, + GraphPath: graphPath, + tmpdir: tmpDir, + logger: lagertest.NewTestLogger("garden-runner"), + + Client: client.New(connection.New(network, addr)), + } + + c := cmd(tmpDir, depotDir, graphPath, network, addr, bin, RootFSPath, argv...) + r.process = ifrit.Invoke(&ginkgomon.Runner{ + Name: "guardian", + Command: c, + AnsiColorCode: "31m", + StartCheck: "guardian.started", + StartCheckTimeout: 30 * time.Second, + }) + + r.Pid = c.Process.Pid + + return r +} + +func (r *RunningGarden) Kill() error { + r.process.Signal(syscall.SIGKILL) + select { + case err := <-r.process.Wait(): + return err + case <-time.After(time.Second * 10): + r.process.Signal(syscall.SIGKILL) + return errors.New("timed out waiting for garden to shutdown after 10 seconds") + } +} + +func (r *RunningGarden) DestroyAndStop() error { + if err := r.DestroyContainers(); err != nil { + return err + } + + if err := r.Stop(); err != nil { + return err + } + + return nil +} + +func (r *RunningGarden) Stop() error { + r.process.Signal(syscall.SIGTERM) + select { + case err := <-r.process.Wait(): + return err + case <-time.After(time.Second * 10): + r.process.Signal(syscall.SIGKILL) + return errors.New("timed out waiting for garden to shutdown after 10 seconds") + } +} + +func cmd(tmpdir, depotDir, graphPath, network, addr, bin, RootFSPath string, argv ...string) *exec.Cmd { + Expect(os.MkdirAll(tmpdir, 0755)).To(Succeed()) + + snapshotsPath := filepath.Join(tmpdir, "snapshots") + + Expect(os.MkdirAll(depotDir, 0755)).To(Succeed()) + + Expect(os.MkdirAll(snapshotsPath, 0755)).To(Succeed()) + + appendDefaultFlag := func(ar []string, key, value string) []string { + for _, a := range argv { + if a == key { + return ar + } + } + + if value != "" { + return append(ar, key, value) + } else { + return append(ar, key) + } + } + + gardenArgs := make([]string, len(argv)) + copy(gardenArgs, argv) + + gardenArgs = appendDefaultFlag(gardenArgs, "--listenNetwork", network) + gardenArgs = appendDefaultFlag(gardenArgs, "--listenAddr", addr) + gardenArgs = appendDefaultFlag(gardenArgs, "--depot", depotDir) + gardenArgs = appendDefaultFlag(gardenArgs, "--logLevel", "debug") + gardenArgs = appendDefaultFlag(gardenArgs, "--debugAddr", fmt.Sprintf(":808%d", ginkgo.GinkgoParallelNode())) + + return exec.Command(bin, gardenArgs...) +} + +func (r *RunningGarden) Cleanup() { + if err := os.RemoveAll(r.GraphPath); err != nil { + r.logger.Error("remove graph", err) + } + + if os.Getenv("BTRFS_SUPPORTED") != "" { + r.cleanupSubvolumes() + } + + r.logger.Info("cleanup-tempdirs") + if err := os.RemoveAll(r.tmpdir); err != nil { + r.logger.Error("cleanup-tempdirs-failed", err, lager.Data{"tmpdir": r.tmpdir}) + } else { + r.logger.Info("tempdirs-removed") + } +} + +func (r *RunningGarden) cleanupSubvolumes() { + r.logger.Info("cleanup-subvolumes") + + // need to remove subvolumes before cleaning graphpath + subvolumesOutput, err := exec.Command("btrfs", "subvolume", "list", "-o", r.GraphRoot).CombinedOutput() + r.logger.Debug(fmt.Sprintf("listing-subvolumes: %s", string(subvolumesOutput))) + if err != nil { + r.logger.Fatal("listing-subvolumes-error", err) + } + + for _, line := range strings.Split(string(subvolumesOutput), "\n") { + fields := strings.Fields(line) + if len(fields) < 1 { + continue + } + + subvolumePath := fields[len(fields)-1] // this path is relative to the outer Garden-Linux BTRFS mount + idx := strings.Index(subvolumePath, r.GraphRoot) + if idx == -1 { + continue + } + subvolumeAbsolutePath := subvolumePath[idx:] + + if strings.Contains(subvolumeAbsolutePath, r.GraphPath) { + if b, err := exec.Command("btrfs", "subvolume", "delete", subvolumeAbsolutePath).CombinedOutput(); err != nil { + r.logger.Fatal(fmt.Sprintf("deleting-subvolume: %s", string(b)), err) + } + } + } + + if err := os.RemoveAll(r.GraphPath); err != nil { + r.logger.Error("remove-graph-again", err) + } +} + +func (r *RunningGarden) DestroyContainers() error { + containers, err := r.Containers(nil) + if err != nil { + return err + } + + for _, container := range containers { + err := r.Destroy(container.Handle()) + if err != nil { + return err + } + } + + return nil +} diff --git a/rundmc/README.md b/rundmc/README.md new file mode 100644 index 000000000..17a859660 --- /dev/null +++ b/rundmc/README.md @@ -0,0 +1,21 @@ +# RunDMC - Diminuatively Managed RunC Containers + +RunDMC is a small wrapper around runC. + + +## High Level Architecture + +Each container is stored as a subdirectory of a directory called 'the depot'. +The depot is the source of truth for RunDMC, when a container is created, this amounts +to creating an Open Container Spec compliant container as a subdirectory of the depot directory. +The subdirectory is named after the container's handle. Looking up a container amounts to +checking for the presence of a subdirectory with the right name. + +To execute processes in a container, we launch the runc binary inside the container directory +and pass it a custom process spec. Since we want to control the container lifecycle via the API without +the restriction that the container dies when its first process dies, the containers are always +created with a no-op initial process that never exits. User processes are all executed using `runc exec`. + +The process_tracker allows reattaching to running containers when RunDMC is restarted. It holds on to +process input/output streams and allows reconnecting to them later. + diff --git a/rundmc/dirdepot.go b/rundmc/dirdepot.go new file mode 100644 index 000000000..84b3bc4ad --- /dev/null +++ b/rundmc/dirdepot.go @@ -0,0 +1,17 @@ +package rundmc + +import ( + "io/ioutil" + "os" + "path/filepath" +) + +// a depot which stores containers as subdirs of a depot directory +type DirectoryDepot struct { + Dir string +} + +func (d *DirectoryDepot) Create(handle string) error { + os.MkdirAll(filepath.Join(d.Dir, handle), 0700) + return ioutil.WriteFile(filepath.Join(d.Dir, handle, "config.json"), nil, 0700) +} diff --git a/rundmc/dirdepot_test.go b/rundmc/dirdepot_test.go new file mode 100644 index 000000000..325a19c47 --- /dev/null +++ b/rundmc/dirdepot_test.go @@ -0,0 +1,38 @@ +package rundmc_test + +import ( + "io/ioutil" + "path/filepath" + + "github.com/cloudfoundry-incubator/guardian/rundmc" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Depot", func() { + Describe("create", func() { + var tmpDir string + BeforeEach(func() { + var err error + + tmpDir, err = ioutil.TempDir("", "depot-test") + Expect(err).NotTo(HaveOccurred()) + + depot := rundmc.DirectoryDepot{ + Dir: tmpDir, + } + + Expect(depot.Create("aardvaark")).To(Succeed()) + }) + + It("should create a directory", func() { + Expect(filepath.Join(tmpDir, "aardvaark")).To(BeADirectory()) + }) + + Describe("the container directory", func() { + It("should contain a config.json", func() { + Expect(filepath.Join(tmpDir, "aardvaark", "config.json")).To(BeARegularFile()) + }) + }) + }) +}) diff --git a/rundmc/fakes/fake_depot.go b/rundmc/fakes/fake_depot.go new file mode 100644 index 000000000..4730df76e --- /dev/null +++ b/rundmc/fakes/fake_depot.go @@ -0,0 +1,53 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + "github.com/cloudfoundry-incubator/guardian/rundmc" +) + +type FakeDepot struct { + CreateStub func(handle string) error + createMutex sync.RWMutex + createArgsForCall []struct { + handle string + } + createReturns struct { + result1 error + } +} + +func (fake *FakeDepot) Create(handle string) error { + fake.createMutex.Lock() + fake.createArgsForCall = append(fake.createArgsForCall, struct { + handle string + }{handle}) + fake.createMutex.Unlock() + if fake.CreateStub != nil { + return fake.CreateStub(handle) + } else { + return fake.createReturns.result1 + } +} + +func (fake *FakeDepot) CreateCallCount() int { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return len(fake.createArgsForCall) +} + +func (fake *FakeDepot) CreateArgsForCall(i int) string { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return fake.createArgsForCall[i].handle +} + +func (fake *FakeDepot) CreateReturns(result1 error) { + fake.CreateStub = nil + fake.createReturns = struct { + result1 error + }{result1} +} + +var _ rundmc.Depot = new(FakeDepot) diff --git a/rundmc/rundmc.go b/rundmc/rundmc.go new file mode 100644 index 000000000..44c4ca763 --- /dev/null +++ b/rundmc/rundmc.go @@ -0,0 +1,17 @@ +package rundmc + +import "github.com/cloudfoundry-incubator/guardian/gardener" + +//go:generate counterfeiter . Depot +type Depot interface { + Create(handle string) error +} + +type Containerizer struct { + Depot Depot +} + +func (c *Containerizer) Create(spec gardener.DesiredContainerSpec) error { + c.Depot.Create(spec.Handle) + return nil +} diff --git a/rundmc/rundmc_suite_test.go b/rundmc/rundmc_suite_test.go new file mode 100644 index 000000000..fa96ffa06 --- /dev/null +++ b/rundmc/rundmc_suite_test.go @@ -0,0 +1,13 @@ +package rundmc_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestRundmc(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Rundmc Suite") +} diff --git a/rundmc/rundmc_test.go b/rundmc/rundmc_test.go new file mode 100644 index 000000000..1755ebac2 --- /dev/null +++ b/rundmc/rundmc_test.go @@ -0,0 +1,26 @@ +package rundmc_test + +import ( + "github.com/cloudfoundry-incubator/guardian/gardener" + "github.com/cloudfoundry-incubator/guardian/rundmc" + "github.com/cloudfoundry-incubator/guardian/rundmc/fakes" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Rundmc", func() { + Describe("create", func() { + It("should ask the depot to create a container", func() { + fakeDepot := new(fakes.FakeDepot) + containerizer := rundmc.Containerizer{ + Depot: fakeDepot, + } + containerizer.Create(gardener.DesiredContainerSpec{ + Handle: "exuberant!", + }) + Expect(fakeDepot.CreateCallCount()).To(Equal(1)) + Expect(fakeDepot.CreateArgsForCall(0)).To(Equal("exuberant!")) + }) + }) + +})