| 1 | 1 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,203 @@ |
| 0 |
+package docker |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/json" |
|
| 4 |
+ "errors" |
|
| 5 |
+ "io" |
|
| 6 |
+ "io/ioutil" |
|
| 7 |
+ "os" |
|
| 8 |
+ "os/exec" |
|
| 9 |
+ "path" |
|
| 10 |
+ "syscall" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+type Container struct {
|
|
| 14 |
+ Name string |
|
| 15 |
+ Root string |
|
| 16 |
+ Path string |
|
| 17 |
+ Args []string |
|
| 18 |
+ |
|
| 19 |
+ *Config |
|
| 20 |
+ *Filesystem |
|
| 21 |
+ *State |
|
| 22 |
+ |
|
| 23 |
+ lxcConfigPath string |
|
| 24 |
+ cmd *exec.Cmd |
|
| 25 |
+ stdout *writeBroadcaster |
|
| 26 |
+ stderr *writeBroadcaster |
|
| 27 |
+} |
|
| 28 |
+ |
|
| 29 |
+type Config struct {
|
|
| 30 |
+ Hostname string |
|
| 31 |
+ Ram int64 |
|
| 32 |
+} |
|
| 33 |
+ |
|
| 34 |
+func createContainer(name string, root string, command string, args []string, layers []string, config *Config) (*Container, error) {
|
|
| 35 |
+ container := &Container{
|
|
| 36 |
+ Name: name, |
|
| 37 |
+ Root: root, |
|
| 38 |
+ Path: command, |
|
| 39 |
+ Args: args, |
|
| 40 |
+ Config: config, |
|
| 41 |
+ Filesystem: newFilesystem(path.Join(root, "rootfs"), path.Join(root, "rw"), layers), |
|
| 42 |
+ State: newState(), |
|
| 43 |
+ |
|
| 44 |
+ lxcConfigPath: path.Join(root, "config.lxc"), |
|
| 45 |
+ stdout: newWriteBroadcaster(), |
|
| 46 |
+ stderr: newWriteBroadcaster(), |
|
| 47 |
+ } |
|
| 48 |
+ |
|
| 49 |
+ if err := os.Mkdir(root, 0700); err != nil {
|
|
| 50 |
+ return nil, err |
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ if err := container.save(); err != nil {
|
|
| 54 |
+ return nil, err |
|
| 55 |
+ } |
|
| 56 |
+ if err := container.generateLXCConfig(); err != nil {
|
|
| 57 |
+ return nil, err |
|
| 58 |
+ } |
|
| 59 |
+ return container, nil |
|
| 60 |
+} |
|
| 61 |
+ |
|
| 62 |
+func loadContainer(containerPath string) (*Container, error) {
|
|
| 63 |
+ configPath := path.Join(containerPath, "config.json") |
|
| 64 |
+ fi, err := os.Open(configPath) |
|
| 65 |
+ if err != nil {
|
|
| 66 |
+ return nil, err |
|
| 67 |
+ } |
|
| 68 |
+ defer fi.Close() |
|
| 69 |
+ enc := json.NewDecoder(fi) |
|
| 70 |
+ container := &Container{}
|
|
| 71 |
+ if err := enc.Decode(container); err != nil {
|
|
| 72 |
+ return nil, err |
|
| 73 |
+ } |
|
| 74 |
+ return container, nil |
|
| 75 |
+} |
|
| 76 |
+ |
|
| 77 |
+func (container *Container) save() error {
|
|
| 78 |
+ configPath := path.Join(container.Root, "config.json") |
|
| 79 |
+ fo, err := os.Create(configPath) |
|
| 80 |
+ if err != nil {
|
|
| 81 |
+ return err |
|
| 82 |
+ } |
|
| 83 |
+ defer fo.Close() |
|
| 84 |
+ enc := json.NewEncoder(fo) |
|
| 85 |
+ if err := enc.Encode(container); err != nil {
|
|
| 86 |
+ return err |
|
| 87 |
+ } |
|
| 88 |
+ return nil |
|
| 89 |
+} |
|
| 90 |
+ |
|
| 91 |
+func (container *Container) generateLXCConfig() error {
|
|
| 92 |
+ fo, err := os.Create(container.lxcConfigPath) |
|
| 93 |
+ if err != nil {
|
|
| 94 |
+ return err |
|
| 95 |
+ } |
|
| 96 |
+ defer fo.Close() |
|
| 97 |
+ |
|
| 98 |
+ if err := LxcTemplateCompiled.Execute(fo, container); err != nil {
|
|
| 99 |
+ return err |
|
| 100 |
+ } |
|
| 101 |
+ return nil |
|
| 102 |
+} |
|
| 103 |
+ |
|
| 104 |
+func (container *Container) Start() error {
|
|
| 105 |
+ if err := container.Filesystem.Mount(); err != nil {
|
|
| 106 |
+ return err |
|
| 107 |
+ } |
|
| 108 |
+ |
|
| 109 |
+ params := []string{
|
|
| 110 |
+ "-n", container.Name, |
|
| 111 |
+ "-f", container.lxcConfigPath, |
|
| 112 |
+ "--", |
|
| 113 |
+ container.Path, |
|
| 114 |
+ } |
|
| 115 |
+ params = append(params, container.Args...) |
|
| 116 |
+ |
|
| 117 |
+ container.cmd = exec.Command("/usr/bin/lxc-start", params...)
|
|
| 118 |
+ container.cmd.Stdout = container.stdout |
|
| 119 |
+ container.cmd.Stderr = container.stderr |
|
| 120 |
+ |
|
| 121 |
+ if err := container.cmd.Start(); err != nil {
|
|
| 122 |
+ return err |
|
| 123 |
+ } |
|
| 124 |
+ container.State.setRunning(container.cmd.Process.Pid) |
|
| 125 |
+ go container.monitor() |
|
| 126 |
+ |
|
| 127 |
+ // Wait until we are out of the STARTING state before returning |
|
| 128 |
+ // |
|
| 129 |
+ // Even though lxc-wait blocks until the container reaches a given state, |
|
| 130 |
+ // sometimes it returns an error code, which is why we have to retry. |
|
| 131 |
+ // |
|
| 132 |
+ // This is a rare race condition that happens for short lived programs |
|
| 133 |
+ for retries := 0; retries < 3; retries++ {
|
|
| 134 |
+ err := exec.Command("/usr/bin/lxc-wait", "-n", container.Name, "-s", "RUNNING|STOPPED").Run()
|
|
| 135 |
+ if err == nil {
|
|
| 136 |
+ return nil |
|
| 137 |
+ } |
|
| 138 |
+ } |
|
| 139 |
+ return errors.New("Container failed to start")
|
|
| 140 |
+} |
|
| 141 |
+ |
|
| 142 |
+func (container *Container) Run() error {
|
|
| 143 |
+ if err := container.Start(); err != nil {
|
|
| 144 |
+ return err |
|
| 145 |
+ } |
|
| 146 |
+ container.Wait() |
|
| 147 |
+ return nil |
|
| 148 |
+} |
|
| 149 |
+ |
|
| 150 |
+func (container *Container) Output() (output []byte, err error) {
|
|
| 151 |
+ pipe, err := container.StdoutPipe() |
|
| 152 |
+ if err != nil {
|
|
| 153 |
+ return nil, err |
|
| 154 |
+ } |
|
| 155 |
+ defer pipe.Close() |
|
| 156 |
+ if err := container.Start(); err != nil {
|
|
| 157 |
+ return nil, err |
|
| 158 |
+ } |
|
| 159 |
+ output, err = ioutil.ReadAll(pipe) |
|
| 160 |
+ container.Wait() |
|
| 161 |
+ return output, err |
|
| 162 |
+} |
|
| 163 |
+ |
|
| 164 |
+func (container *Container) StdoutPipe() (io.ReadCloser, error) {
|
|
| 165 |
+ reader, writer := io.Pipe() |
|
| 166 |
+ container.stdout.AddWriter(writer) |
|
| 167 |
+ return newBufReader(reader), nil |
|
| 168 |
+} |
|
| 169 |
+ |
|
| 170 |
+func (container *Container) StderrPipe() (io.ReadCloser, error) {
|
|
| 171 |
+ reader, writer := io.Pipe() |
|
| 172 |
+ container.stderr.AddWriter(writer) |
|
| 173 |
+ return newBufReader(reader), nil |
|
| 174 |
+} |
|
| 175 |
+ |
|
| 176 |
+func (container *Container) monitor() {
|
|
| 177 |
+ container.cmd.Wait() |
|
| 178 |
+ container.stdout.Close() |
|
| 179 |
+ container.stderr.Close() |
|
| 180 |
+ container.State.setStopped(container.cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()) |
|
| 181 |
+} |
|
| 182 |
+ |
|
| 183 |
+func (container *Container) Stop() error {
|
|
| 184 |
+ if container.State.Running {
|
|
| 185 |
+ if err := exec.Command("/usr/bin/lxc-stop", "-n", container.Name).Run(); err != nil {
|
|
| 186 |
+ return err |
|
| 187 |
+ } |
|
| 188 |
+ //FIXME: We should lxc-wait for the container to stop |
|
| 189 |
+ } |
|
| 190 |
+ |
|
| 191 |
+ if err := container.Filesystem.Umount(); err != nil {
|
|
| 192 |
+ // FIXME: Do not abort, probably already umounted? |
|
| 193 |
+ return nil |
|
| 194 |
+ } |
|
| 195 |
+ return nil |
|
| 196 |
+} |
|
| 197 |
+ |
|
| 198 |
+func (container *Container) Wait() {
|
|
| 199 |
+ for container.State.Running {
|
|
| 200 |
+ container.State.wait() |
|
| 201 |
+ } |
|
| 202 |
+} |
| 0 | 203 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,186 @@ |
| 0 |
+package docker |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "testing" |
|
| 4 |
+) |
|
| 5 |
+ |
|
| 6 |
+func TestStart(t *testing.T) {
|
|
| 7 |
+ docker, err := newTestDocker() |
|
| 8 |
+ if err != nil {
|
|
| 9 |
+ t.Fatal(err) |
|
| 10 |
+ } |
|
| 11 |
+ container, err := docker.Create( |
|
| 12 |
+ "start_test", |
|
| 13 |
+ "ls", |
|
| 14 |
+ []string{"-al"},
|
|
| 15 |
+ []string{"/var/lib/docker/images/ubuntu"},
|
|
| 16 |
+ &Config{
|
|
| 17 |
+ Ram: 33554432, |
|
| 18 |
+ }, |
|
| 19 |
+ ) |
|
| 20 |
+ if err != nil {
|
|
| 21 |
+ t.Fatal(err) |
|
| 22 |
+ } |
|
| 23 |
+ defer docker.Destroy(container) |
|
| 24 |
+ |
|
| 25 |
+ if container.State.Running {
|
|
| 26 |
+ t.Errorf("Container shouldn't be running")
|
|
| 27 |
+ } |
|
| 28 |
+ if err := container.Start(); err != nil {
|
|
| 29 |
+ t.Fatal(err) |
|
| 30 |
+ } |
|
| 31 |
+ container.Wait() |
|
| 32 |
+ if container.State.Running {
|
|
| 33 |
+ t.Errorf("Container shouldn't be running")
|
|
| 34 |
+ } |
|
| 35 |
+ // We should be able to call Wait again |
|
| 36 |
+ container.Wait() |
|
| 37 |
+ if container.State.Running {
|
|
| 38 |
+ t.Errorf("Container shouldn't be running")
|
|
| 39 |
+ } |
|
| 40 |
+} |
|
| 41 |
+ |
|
| 42 |
+func TestRun(t *testing.T) {
|
|
| 43 |
+ docker, err := newTestDocker() |
|
| 44 |
+ if err != nil {
|
|
| 45 |
+ t.Fatal(err) |
|
| 46 |
+ } |
|
| 47 |
+ container, err := docker.Create( |
|
| 48 |
+ "run_test", |
|
| 49 |
+ "ls", |
|
| 50 |
+ []string{"-al"},
|
|
| 51 |
+ []string{"/var/lib/docker/images/ubuntu"},
|
|
| 52 |
+ &Config{
|
|
| 53 |
+ Ram: 33554432, |
|
| 54 |
+ }, |
|
| 55 |
+ ) |
|
| 56 |
+ if err != nil {
|
|
| 57 |
+ t.Fatal(err) |
|
| 58 |
+ } |
|
| 59 |
+ defer docker.Destroy(container) |
|
| 60 |
+ |
|
| 61 |
+ if container.State.Running {
|
|
| 62 |
+ t.Errorf("Container shouldn't be running")
|
|
| 63 |
+ } |
|
| 64 |
+ if err := container.Run(); err != nil {
|
|
| 65 |
+ t.Fatal(err) |
|
| 66 |
+ } |
|
| 67 |
+ if container.State.Running {
|
|
| 68 |
+ t.Errorf("Container shouldn't be running")
|
|
| 69 |
+ } |
|
| 70 |
+} |
|
| 71 |
+ |
|
| 72 |
+func TestOutput(t *testing.T) {
|
|
| 73 |
+ docker, err := newTestDocker() |
|
| 74 |
+ if err != nil {
|
|
| 75 |
+ t.Fatal(err) |
|
| 76 |
+ } |
|
| 77 |
+ container, err := docker.Create( |
|
| 78 |
+ "output_test", |
|
| 79 |
+ "echo", |
|
| 80 |
+ []string{"-n", "foobar"},
|
|
| 81 |
+ []string{"/var/lib/docker/images/ubuntu"},
|
|
| 82 |
+ &Config{},
|
|
| 83 |
+ ) |
|
| 84 |
+ if err != nil {
|
|
| 85 |
+ t.Fatal(err) |
|
| 86 |
+ } |
|
| 87 |
+ defer docker.Destroy(container) |
|
| 88 |
+ |
|
| 89 |
+ pipe, err := container.StdoutPipe() |
|
| 90 |
+ defer pipe.Close() |
|
| 91 |
+ output, err := container.Output() |
|
| 92 |
+ if err != nil {
|
|
| 93 |
+ t.Fatal(err) |
|
| 94 |
+ } |
|
| 95 |
+ if string(output) != "foobar" {
|
|
| 96 |
+ t.Error(string(output)) |
|
| 97 |
+ } |
|
| 98 |
+} |
|
| 99 |
+ |
|
| 100 |
+func TestStop(t *testing.T) {
|
|
| 101 |
+ docker, err := newTestDocker() |
|
| 102 |
+ if err != nil {
|
|
| 103 |
+ t.Fatal(err) |
|
| 104 |
+ } |
|
| 105 |
+ container, err := docker.Create( |
|
| 106 |
+ "stop_test", |
|
| 107 |
+ "sleep", |
|
| 108 |
+ []string{"300"},
|
|
| 109 |
+ []string{"/var/lib/docker/images/ubuntu"},
|
|
| 110 |
+ &Config{},
|
|
| 111 |
+ ) |
|
| 112 |
+ if err != nil {
|
|
| 113 |
+ t.Fatal(err) |
|
| 114 |
+ } |
|
| 115 |
+ defer docker.Destroy(container) |
|
| 116 |
+ |
|
| 117 |
+ if container.State.Running {
|
|
| 118 |
+ t.Errorf("Container shouldn't be running")
|
|
| 119 |
+ } |
|
| 120 |
+ if err := container.Start(); err != nil {
|
|
| 121 |
+ t.Fatal(err) |
|
| 122 |
+ } |
|
| 123 |
+ if !container.State.Running {
|
|
| 124 |
+ t.Errorf("Container should be running")
|
|
| 125 |
+ } |
|
| 126 |
+ if err := container.Stop(); err != nil {
|
|
| 127 |
+ t.Fatal(err) |
|
| 128 |
+ } |
|
| 129 |
+ if container.State.Running {
|
|
| 130 |
+ t.Errorf("Container shouldn't be running")
|
|
| 131 |
+ } |
|
| 132 |
+ container.Wait() |
|
| 133 |
+ if container.State.Running {
|
|
| 134 |
+ t.Errorf("Container shouldn't be running")
|
|
| 135 |
+ } |
|
| 136 |
+ // Try stopping twice |
|
| 137 |
+ if err := container.Stop(); err != nil {
|
|
| 138 |
+ t.Fatal(err) |
|
| 139 |
+ } |
|
| 140 |
+} |
|
| 141 |
+ |
|
| 142 |
+func TestExitCode(t *testing.T) {
|
|
| 143 |
+ docker, err := newTestDocker() |
|
| 144 |
+ if err != nil {
|
|
| 145 |
+ t.Fatal(err) |
|
| 146 |
+ } |
|
| 147 |
+ |
|
| 148 |
+ trueContainer, err := docker.Create( |
|
| 149 |
+ "exit_test_1", |
|
| 150 |
+ "/bin/true", |
|
| 151 |
+ []string{""},
|
|
| 152 |
+ []string{"/var/lib/docker/images/ubuntu"},
|
|
| 153 |
+ &Config{},
|
|
| 154 |
+ ) |
|
| 155 |
+ if err != nil {
|
|
| 156 |
+ t.Fatal(err) |
|
| 157 |
+ } |
|
| 158 |
+ defer docker.Destroy(trueContainer) |
|
| 159 |
+ if err := trueContainer.Run(); err != nil {
|
|
| 160 |
+ t.Fatal(err) |
|
| 161 |
+ } |
|
| 162 |
+ |
|
| 163 |
+ falseContainer, err := docker.Create( |
|
| 164 |
+ "exit_test_2", |
|
| 165 |
+ "/bin/false", |
|
| 166 |
+ []string{""},
|
|
| 167 |
+ []string{"/var/lib/docker/images/ubuntu"},
|
|
| 168 |
+ &Config{},
|
|
| 169 |
+ ) |
|
| 170 |
+ if err != nil {
|
|
| 171 |
+ t.Fatal(err) |
|
| 172 |
+ } |
|
| 173 |
+ defer docker.Destroy(falseContainer) |
|
| 174 |
+ if err := falseContainer.Run(); err != nil {
|
|
| 175 |
+ t.Fatal(err) |
|
| 176 |
+ } |
|
| 177 |
+ |
|
| 178 |
+ if trueContainer.State.ExitCode != 0 {
|
|
| 179 |
+ t.Errorf("Unexpected exit code %v", trueContainer.State.ExitCode)
|
|
| 180 |
+ } |
|
| 181 |
+ |
|
| 182 |
+ if falseContainer.State.ExitCode != 1 {
|
|
| 183 |
+ t.Errorf("Unexpected exit code %v", falseContainer.State.ExitCode)
|
|
| 184 |
+ } |
|
| 185 |
+} |
| 0 | 186 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,112 @@ |
| 0 |
+package docker |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "container/list" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "io/ioutil" |
|
| 6 |
+ "os" |
|
| 7 |
+ "path" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+type Docker struct {
|
|
| 11 |
+ root string |
|
| 12 |
+ repository string |
|
| 13 |
+ containers *list.List |
|
| 14 |
+} |
|
| 15 |
+ |
|
| 16 |
+func (docker *Docker) List() []*Container {
|
|
| 17 |
+ containers := []*Container{}
|
|
| 18 |
+ for e := docker.containers.Front(); e != nil; e = e.Next() {
|
|
| 19 |
+ containers = append(containers, e.Value.(*Container)) |
|
| 20 |
+ } |
|
| 21 |
+ return containers |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+func (docker *Docker) getContainerElement(name string) *list.Element {
|
|
| 25 |
+ for e := docker.containers.Front(); e != nil; e = e.Next() {
|
|
| 26 |
+ container := e.Value.(*Container) |
|
| 27 |
+ if container.Name == name {
|
|
| 28 |
+ return e |
|
| 29 |
+ } |
|
| 30 |
+ } |
|
| 31 |
+ return nil |
|
| 32 |
+} |
|
| 33 |
+ |
|
| 34 |
+func (docker *Docker) Get(name string) *Container {
|
|
| 35 |
+ e := docker.getContainerElement(name) |
|
| 36 |
+ if e == nil {
|
|
| 37 |
+ return nil |
|
| 38 |
+ } |
|
| 39 |
+ return e.Value.(*Container) |
|
| 40 |
+} |
|
| 41 |
+ |
|
| 42 |
+func (docker *Docker) Exists(name string) bool {
|
|
| 43 |
+ return docker.Get(name) != nil |
|
| 44 |
+} |
|
| 45 |
+ |
|
| 46 |
+func (docker *Docker) Create(name string, command string, args []string, layers []string, config *Config) (*Container, error) {
|
|
| 47 |
+ if docker.Exists(name) {
|
|
| 48 |
+ return nil, fmt.Errorf("Container %v already exists", name)
|
|
| 49 |
+ } |
|
| 50 |
+ root := path.Join(docker.repository, name) |
|
| 51 |
+ container, err := createContainer(name, root, command, args, layers, config) |
|
| 52 |
+ if err != nil {
|
|
| 53 |
+ return nil, err |
|
| 54 |
+ } |
|
| 55 |
+ docker.containers.PushBack(container) |
|
| 56 |
+ return container, nil |
|
| 57 |
+} |
|
| 58 |
+ |
|
| 59 |
+func (docker *Docker) Destroy(container *Container) error {
|
|
| 60 |
+ element := docker.getContainerElement(container.Name) |
|
| 61 |
+ if element == nil {
|
|
| 62 |
+ return fmt.Errorf("Container %v not found - maybe it was already destroyed?", container.Name)
|
|
| 63 |
+ } |
|
| 64 |
+ |
|
| 65 |
+ if err := container.Stop(); err != nil {
|
|
| 66 |
+ return err |
|
| 67 |
+ } |
|
| 68 |
+ if err := os.RemoveAll(container.Root); err != nil {
|
|
| 69 |
+ return err |
|
| 70 |
+ } |
|
| 71 |
+ |
|
| 72 |
+ docker.containers.Remove(element) |
|
| 73 |
+ return nil |
|
| 74 |
+} |
|
| 75 |
+ |
|
| 76 |
+func (docker *Docker) restore() error {
|
|
| 77 |
+ dir, err := ioutil.ReadDir(docker.repository) |
|
| 78 |
+ if err != nil {
|
|
| 79 |
+ return err |
|
| 80 |
+ } |
|
| 81 |
+ for _, v := range dir {
|
|
| 82 |
+ container, err := loadContainer(path.Join(docker.repository, v.Name())) |
|
| 83 |
+ if err != nil {
|
|
| 84 |
+ fmt.Errorf("Failed to load %v: %v", v.Name(), err)
|
|
| 85 |
+ continue |
|
| 86 |
+ } |
|
| 87 |
+ docker.containers.PushBack(container) |
|
| 88 |
+ } |
|
| 89 |
+ return nil |
|
| 90 |
+} |
|
| 91 |
+ |
|
| 92 |
+func New() (*Docker, error) {
|
|
| 93 |
+ return NewFromDirectory("/var/lib/docker")
|
|
| 94 |
+} |
|
| 95 |
+ |
|
| 96 |
+func NewFromDirectory(root string) (*Docker, error) {
|
|
| 97 |
+ docker := &Docker{
|
|
| 98 |
+ root: root, |
|
| 99 |
+ repository: path.Join(root, "containers"), |
|
| 100 |
+ containers: list.New(), |
|
| 101 |
+ } |
|
| 102 |
+ |
|
| 103 |
+ if err := os.Mkdir(docker.repository, 0700); err != nil && !os.IsExist(err) {
|
|
| 104 |
+ return nil, err |
|
| 105 |
+ } |
|
| 106 |
+ |
|
| 107 |
+ if err := docker.restore(); err != nil {
|
|
| 108 |
+ return nil, err |
|
| 109 |
+ } |
|
| 110 |
+ return docker, nil |
|
| 111 |
+} |
| 0 | 112 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,175 @@ |
| 0 |
+package docker |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "io/ioutil" |
|
| 4 |
+ "os" |
|
| 5 |
+ "testing" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+func newTestDocker() (*Docker, error) {
|
|
| 9 |
+ root, err := ioutil.TempDir("", "docker-test")
|
|
| 10 |
+ if err != nil {
|
|
| 11 |
+ return nil, err |
|
| 12 |
+ } |
|
| 13 |
+ docker, err := NewFromDirectory(root) |
|
| 14 |
+ if err != nil {
|
|
| 15 |
+ return nil, err |
|
| 16 |
+ } |
|
| 17 |
+ return docker, nil |
|
| 18 |
+} |
|
| 19 |
+ |
|
| 20 |
+func TestCreate(t *testing.T) {
|
|
| 21 |
+ docker, err := newTestDocker() |
|
| 22 |
+ if err != nil {
|
|
| 23 |
+ t.Fatal(err) |
|
| 24 |
+ } |
|
| 25 |
+ |
|
| 26 |
+ // Make sure we start we 0 containers |
|
| 27 |
+ if len(docker.List()) != 0 {
|
|
| 28 |
+ t.Errorf("Expected 0 containers, %v found", len(docker.List()))
|
|
| 29 |
+ } |
|
| 30 |
+ container, err := docker.Create( |
|
| 31 |
+ "test_create", |
|
| 32 |
+ "ls", |
|
| 33 |
+ []string{"-al"},
|
|
| 34 |
+ []string{"/var/lib/docker/images/ubuntu"},
|
|
| 35 |
+ &Config{},
|
|
| 36 |
+ ) |
|
| 37 |
+ if err != nil {
|
|
| 38 |
+ t.Fatal(err) |
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ defer func() {
|
|
| 42 |
+ if err := docker.Destroy(container); err != nil {
|
|
| 43 |
+ t.Error(err) |
|
| 44 |
+ } |
|
| 45 |
+ }() |
|
| 46 |
+ |
|
| 47 |
+ // Make sure we can find the newly created container with List() |
|
| 48 |
+ if len(docker.List()) != 1 {
|
|
| 49 |
+ t.Errorf("Expected 1 container, %v found", len(docker.List()))
|
|
| 50 |
+ } |
|
| 51 |
+ |
|
| 52 |
+ // Make sure the container List() returns is the right one |
|
| 53 |
+ if docker.List()[0].Name != "test_create" {
|
|
| 54 |
+ t.Errorf("Unexpected container %v returned by List", docker.List()[0])
|
|
| 55 |
+ } |
|
| 56 |
+ |
|
| 57 |
+ // Make sure we can get the container with Get() |
|
| 58 |
+ if docker.Get("test_create") == nil {
|
|
| 59 |
+ t.Errorf("Unable to get newly created container")
|
|
| 60 |
+ } |
|
| 61 |
+ |
|
| 62 |
+ // Make sure it is the right container |
|
| 63 |
+ if docker.Get("test_create") != container {
|
|
| 64 |
+ t.Errorf("Get() returned the wrong container")
|
|
| 65 |
+ } |
|
| 66 |
+ |
|
| 67 |
+ // Make sure Exists returns it as existing |
|
| 68 |
+ if !docker.Exists("test_create") {
|
|
| 69 |
+ t.Errorf("Exists() returned false for a newly created container")
|
|
| 70 |
+ } |
|
| 71 |
+} |
|
| 72 |
+ |
|
| 73 |
+func TestDestroy(t *testing.T) {
|
|
| 74 |
+ docker, err := newTestDocker() |
|
| 75 |
+ if err != nil {
|
|
| 76 |
+ t.Fatal(err) |
|
| 77 |
+ } |
|
| 78 |
+ container, err := docker.Create( |
|
| 79 |
+ "test_destroy", |
|
| 80 |
+ "ls", |
|
| 81 |
+ []string{"-al"},
|
|
| 82 |
+ []string{"/var/lib/docker/images/ubuntu"},
|
|
| 83 |
+ &Config{},
|
|
| 84 |
+ ) |
|
| 85 |
+ if err != nil {
|
|
| 86 |
+ t.Fatal(err) |
|
| 87 |
+ } |
|
| 88 |
+ // Destroy |
|
| 89 |
+ if err := docker.Destroy(container); err != nil {
|
|
| 90 |
+ t.Error(err) |
|
| 91 |
+ } |
|
| 92 |
+ |
|
| 93 |
+ // Make sure docker.Exists() behaves correctly |
|
| 94 |
+ if docker.Exists("test_destroy") {
|
|
| 95 |
+ t.Errorf("Exists() returned true")
|
|
| 96 |
+ } |
|
| 97 |
+ |
|
| 98 |
+ // Make sure docker.List() doesn't list the destroyed container |
|
| 99 |
+ if len(docker.List()) != 0 {
|
|
| 100 |
+ t.Errorf("Expected 0 container, %v found", len(docker.List()))
|
|
| 101 |
+ } |
|
| 102 |
+ |
|
| 103 |
+ // Make sure docker.Get() refuses to return the unexisting container |
|
| 104 |
+ if docker.Get("test_destroy") != nil {
|
|
| 105 |
+ t.Errorf("Unable to get newly created container")
|
|
| 106 |
+ } |
|
| 107 |
+ |
|
| 108 |
+ // Make sure the container root directory does not exist anymore |
|
| 109 |
+ _, err = os.Stat(container.Root) |
|
| 110 |
+ if err == nil || !os.IsNotExist(err) {
|
|
| 111 |
+ t.Errorf("Container root directory still exists after destroy")
|
|
| 112 |
+ } |
|
| 113 |
+ |
|
| 114 |
+ // Test double destroy |
|
| 115 |
+ if err := docker.Destroy(container); err == nil {
|
|
| 116 |
+ // It should have failed |
|
| 117 |
+ t.Errorf("Double destroy did not fail")
|
|
| 118 |
+ } |
|
| 119 |
+} |
|
| 120 |
+ |
|
| 121 |
+func TestGet(t *testing.T) {
|
|
| 122 |
+ docker, err := newTestDocker() |
|
| 123 |
+ if err != nil {
|
|
| 124 |
+ t.Fatal(err) |
|
| 125 |
+ } |
|
| 126 |
+ container1, err := docker.Create( |
|
| 127 |
+ "test1", |
|
| 128 |
+ "ls", |
|
| 129 |
+ []string{"-al"},
|
|
| 130 |
+ []string{"/var/lib/docker/images/ubuntu"},
|
|
| 131 |
+ &Config{},
|
|
| 132 |
+ ) |
|
| 133 |
+ if err != nil {
|
|
| 134 |
+ t.Fatal(err) |
|
| 135 |
+ } |
|
| 136 |
+ defer docker.Destroy(container1) |
|
| 137 |
+ |
|
| 138 |
+ container2, err := docker.Create( |
|
| 139 |
+ "test2", |
|
| 140 |
+ "ls", |
|
| 141 |
+ []string{"-al"},
|
|
| 142 |
+ []string{"/var/lib/docker/images/ubuntu"},
|
|
| 143 |
+ &Config{},
|
|
| 144 |
+ ) |
|
| 145 |
+ if err != nil {
|
|
| 146 |
+ t.Fatal(err) |
|
| 147 |
+ } |
|
| 148 |
+ defer docker.Destroy(container2) |
|
| 149 |
+ |
|
| 150 |
+ container3, err := docker.Create( |
|
| 151 |
+ "test3", |
|
| 152 |
+ "ls", |
|
| 153 |
+ []string{"-al"},
|
|
| 154 |
+ []string{"/var/lib/docker/images/ubuntu"},
|
|
| 155 |
+ &Config{},
|
|
| 156 |
+ ) |
|
| 157 |
+ if err != nil {
|
|
| 158 |
+ t.Fatal(err) |
|
| 159 |
+ } |
|
| 160 |
+ defer docker.Destroy(container3) |
|
| 161 |
+ |
|
| 162 |
+ if docker.Get("test1") != container1 {
|
|
| 163 |
+ t.Errorf("Get(test1) returned %v while expecting %v", docker.Get("test1"), container1)
|
|
| 164 |
+ } |
|
| 165 |
+ |
|
| 166 |
+ if docker.Get("test2") != container2 {
|
|
| 167 |
+ t.Errorf("Get(test2) returned %v while expecting %v", docker.Get("test2"), container2)
|
|
| 168 |
+ } |
|
| 169 |
+ |
|
| 170 |
+ if docker.Get("test3") != container3 {
|
|
| 171 |
+ t.Errorf("Get(test3) returned %v while expecting %v", docker.Get("test3"), container3)
|
|
| 172 |
+ } |
|
| 173 |
+ |
|
| 174 |
+} |
| 0 | 175 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,52 @@ |
| 0 |
+package docker |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "os" |
|
| 5 |
+ "os/exec" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+type Filesystem struct {
|
|
| 9 |
+ RootFS string |
|
| 10 |
+ RWPath string |
|
| 11 |
+ Layers []string |
|
| 12 |
+} |
|
| 13 |
+ |
|
| 14 |
+func (fs *Filesystem) createMountPoints() error {
|
|
| 15 |
+ if err := os.Mkdir(fs.RootFS, 0700); err != nil && !os.IsExist(err) {
|
|
| 16 |
+ return err |
|
| 17 |
+ } |
|
| 18 |
+ if err := os.Mkdir(fs.RWPath, 0700); err != nil && !os.IsExist(err) {
|
|
| 19 |
+ return err |
|
| 20 |
+ } |
|
| 21 |
+ return nil |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+func (fs *Filesystem) Mount() error {
|
|
| 25 |
+ if err := fs.createMountPoints(); err != nil {
|
|
| 26 |
+ return err |
|
| 27 |
+ } |
|
| 28 |
+ rwBranch := fmt.Sprintf("%v=rw", fs.RWPath)
|
|
| 29 |
+ roBranches := "" |
|
| 30 |
+ for _, layer := range fs.Layers {
|
|
| 31 |
+ roBranches += fmt.Sprintf("%v=ro:", layer)
|
|
| 32 |
+ } |
|
| 33 |
+ branches := fmt.Sprintf("br:%v:%v", rwBranch, roBranches)
|
|
| 34 |
+ cmd := exec.Command("mount", "-t", "aufs", "-o", branches, "none", fs.RootFS)
|
|
| 35 |
+ if err := cmd.Run(); err != nil {
|
|
| 36 |
+ return err |
|
| 37 |
+ } |
|
| 38 |
+ return nil |
|
| 39 |
+} |
|
| 40 |
+ |
|
| 41 |
+func (fs *Filesystem) Umount() error {
|
|
| 42 |
+ return exec.Command("umount", fs.RootFS).Run()
|
|
| 43 |
+} |
|
| 44 |
+ |
|
| 45 |
+func newFilesystem(rootfs string, rwpath string, layers []string) *Filesystem {
|
|
| 46 |
+ return &Filesystem{
|
|
| 47 |
+ RootFS: rootfs, |
|
| 48 |
+ RWPath: rwpath, |
|
| 49 |
+ Layers: layers, |
|
| 50 |
+ } |
|
| 51 |
+} |
| 0 | 52 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,35 @@ |
| 0 |
+package docker |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "io/ioutil" |
|
| 4 |
+ "testing" |
|
| 5 |
+) |
|
| 6 |
+ |
|
| 7 |
+func TestFilesystem(t *testing.T) {
|
|
| 8 |
+ rootfs, err := ioutil.TempDir("", "docker-test-root")
|
|
| 9 |
+ if err != nil {
|
|
| 10 |
+ t.Fatal(err) |
|
| 11 |
+ } |
|
| 12 |
+ rwpath, err := ioutil.TempDir("", "docker-test-rw")
|
|
| 13 |
+ if err != nil {
|
|
| 14 |
+ t.Fatal(err) |
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ filesystem := newFilesystem(rootfs, rwpath, []string{"/var/lib/docker/images/ubuntu", "/var/lib/docker/images/test"})
|
|
| 18 |
+ |
|
| 19 |
+ if err := filesystem.Umount(); err == nil {
|
|
| 20 |
+ t.Errorf("Umount succeeded even though the filesystem was not mounted")
|
|
| 21 |
+ } |
|
| 22 |
+ |
|
| 23 |
+ if err := filesystem.Mount(); err != nil {
|
|
| 24 |
+ t.Fatal(err) |
|
| 25 |
+ } |
|
| 26 |
+ |
|
| 27 |
+ if err := filesystem.Umount(); err != nil {
|
|
| 28 |
+ t.Fatal(err) |
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ if err := filesystem.Umount(); err == nil {
|
|
| 32 |
+ t.Errorf("Umount succeeded even though the filesystem was already umounted")
|
|
| 33 |
+ } |
|
| 34 |
+} |
| 0 | 35 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,94 @@ |
| 0 |
+package docker |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "text/template" |
|
| 4 |
+) |
|
| 5 |
+ |
|
| 6 |
+const LxcTemplate = ` |
|
| 7 |
+# hostname |
|
| 8 |
+{{if .Config.Hostname}}
|
|
| 9 |
+lxc.utsname = {{.Config.Hostname}}
|
|
| 10 |
+{{else}}
|
|
| 11 |
+lxc.utsname = {{.Name}}
|
|
| 12 |
+{{end}}
|
|
| 13 |
+#lxc.aa_profile = unconfined |
|
| 14 |
+ |
|
| 15 |
+# network configuration |
|
| 16 |
+#lxc.network.type = veth |
|
| 17 |
+#lxc.network.flags = up |
|
| 18 |
+#lxc.network.link = br0 |
|
| 19 |
+#lxc.network.name = eth0 # Internal container network interface name |
|
| 20 |
+#lxc.network.mtu = 1500 |
|
| 21 |
+#lxc.network.ipv4 = {ip_address}/{ip_prefix_len}
|
|
| 22 |
+ |
|
| 23 |
+# root filesystem |
|
| 24 |
+lxc.rootfs = {{.Filesystem.RootFS}}
|
|
| 25 |
+ |
|
| 26 |
+# use a dedicated pts for the container (and limit the number of pseudo terminal |
|
| 27 |
+# available) |
|
| 28 |
+lxc.pts = 1024 |
|
| 29 |
+ |
|
| 30 |
+# disable the main console |
|
| 31 |
+lxc.console = none |
|
| 32 |
+ |
|
| 33 |
+# no controlling tty at all |
|
| 34 |
+lxc.tty = 1 |
|
| 35 |
+ |
|
| 36 |
+# no implicit access to devices |
|
| 37 |
+lxc.cgroup.devices.deny = a |
|
| 38 |
+ |
|
| 39 |
+# /dev/null and zero |
|
| 40 |
+lxc.cgroup.devices.allow = c 1:3 rwm |
|
| 41 |
+lxc.cgroup.devices.allow = c 1:5 rwm |
|
| 42 |
+ |
|
| 43 |
+# consoles |
|
| 44 |
+lxc.cgroup.devices.allow = c 5:1 rwm |
|
| 45 |
+lxc.cgroup.devices.allow = c 5:0 rwm |
|
| 46 |
+lxc.cgroup.devices.allow = c 4:0 rwm |
|
| 47 |
+lxc.cgroup.devices.allow = c 4:1 rwm |
|
| 48 |
+ |
|
| 49 |
+# /dev/urandom,/dev/random |
|
| 50 |
+lxc.cgroup.devices.allow = c 1:9 rwm |
|
| 51 |
+lxc.cgroup.devices.allow = c 1:8 rwm |
|
| 52 |
+ |
|
| 53 |
+# /dev/pts/* - pts namespaces are "coming soon" |
|
| 54 |
+lxc.cgroup.devices.allow = c 136:* rwm |
|
| 55 |
+lxc.cgroup.devices.allow = c 5:2 rwm |
|
| 56 |
+ |
|
| 57 |
+# tuntap |
|
| 58 |
+lxc.cgroup.devices.allow = c 10:200 rwm |
|
| 59 |
+ |
|
| 60 |
+# fuse |
|
| 61 |
+#lxc.cgroup.devices.allow = c 10:229 rwm |
|
| 62 |
+ |
|
| 63 |
+# rtc |
|
| 64 |
+#lxc.cgroup.devices.allow = c 254:0 rwm |
|
| 65 |
+ |
|
| 66 |
+ |
|
| 67 |
+# standard mount point |
|
| 68 |
+lxc.mount.entry = proc {{.Filesystem.RootFS}}/proc proc nosuid,nodev,noexec 0 0
|
|
| 69 |
+lxc.mount.entry = sysfs {{.Filesystem.RootFS}}/sys sysfs nosuid,nodev,noexec 0 0
|
|
| 70 |
+lxc.mount.entry = devpts {{.Filesystem.RootFS}}/dev/pts devpts newinstance,ptmxmode=0666,nosuid,noexec 0 0
|
|
| 71 |
+#lxc.mount.entry = varrun {{.Filesystem.RootFS}}/var/run tmpfs mode=755,size=4096k,nosuid,nodev,noexec 0 0
|
|
| 72 |
+#lxc.mount.entry = varlock {{.Filesystem.RootFS}}/var/lock tmpfs size=1024k,nosuid,nodev,noexec 0 0
|
|
| 73 |
+#lxc.mount.entry = shm {{.Filesystem.RootFS}}/dev/shm tmpfs size=65536k,nosuid,nodev,noexec 0 0
|
|
| 74 |
+ |
|
| 75 |
+ |
|
| 76 |
+# drop linux capabilities (apply mainly to the user root in the container) |
|
| 77 |
+lxc.cap.drop = audit_control audit_write mac_admin mac_override mknod net_raw setfcap setpcap sys_admin sys_boot sys_module sys_nice sys_pacct sys_rawio sys_resource sys_time sys_tty_config |
|
| 78 |
+ |
|
| 79 |
+# limits |
|
| 80 |
+{{if .Config.Ram}}
|
|
| 81 |
+lxc.cgroup.memory.limit_in_bytes = {{.Config.Ram}}
|
|
| 82 |
+{{end}}
|
|
| 83 |
+` |
|
| 84 |
+ |
|
| 85 |
+var LxcTemplateCompiled *template.Template |
|
| 86 |
+ |
|
| 87 |
+func init() {
|
|
| 88 |
+ var err error |
|
| 89 |
+ LxcTemplateCompiled, err = template.New("lxc").Parse(LxcTemplate)
|
|
| 90 |
+ if err != nil {
|
|
| 91 |
+ panic(err) |
|
| 92 |
+ } |
|
| 93 |
+} |
| 0 | 94 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,48 @@ |
| 0 |
+package docker |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "sync" |
|
| 4 |
+) |
|
| 5 |
+ |
|
| 6 |
+type State struct {
|
|
| 7 |
+ Running bool |
|
| 8 |
+ Pid int |
|
| 9 |
+ ExitCode int |
|
| 10 |
+ |
|
| 11 |
+ stateChangeLock *sync.Mutex |
|
| 12 |
+ stateChangeCond *sync.Cond |
|
| 13 |
+} |
|
| 14 |
+ |
|
| 15 |
+func newState() *State {
|
|
| 16 |
+ lock := new(sync.Mutex) |
|
| 17 |
+ return &State{
|
|
| 18 |
+ stateChangeLock: lock, |
|
| 19 |
+ stateChangeCond: sync.NewCond(lock), |
|
| 20 |
+ } |
|
| 21 |
+} |
|
| 22 |
+ |
|
| 23 |
+func (s *State) setRunning(pid int) {
|
|
| 24 |
+ s.Running = true |
|
| 25 |
+ s.ExitCode = 0 |
|
| 26 |
+ s.Pid = pid |
|
| 27 |
+ s.broadcast() |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+func (s *State) setStopped(exitCode int) {
|
|
| 31 |
+ s.Running = false |
|
| 32 |
+ s.Pid = 0 |
|
| 33 |
+ s.ExitCode = exitCode |
|
| 34 |
+ s.broadcast() |
|
| 35 |
+} |
|
| 36 |
+ |
|
| 37 |
+func (s *State) broadcast() {
|
|
| 38 |
+ s.stateChangeLock.Lock() |
|
| 39 |
+ s.stateChangeCond.Broadcast() |
|
| 40 |
+ s.stateChangeLock.Unlock() |
|
| 41 |
+} |
|
| 42 |
+ |
|
| 43 |
+func (s *State) wait() {
|
|
| 44 |
+ s.stateChangeLock.Lock() |
|
| 45 |
+ s.stateChangeCond.Wait() |
|
| 46 |
+ s.stateChangeLock.Unlock() |
|
| 47 |
+} |
| 0 | 48 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,115 @@ |
| 0 |
+package docker |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "container/list" |
|
| 5 |
+ "io" |
|
| 6 |
+ "sync" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+type bufReader struct {
|
|
| 10 |
+ buf *bytes.Buffer |
|
| 11 |
+ reader io.Reader |
|
| 12 |
+ err error |
|
| 13 |
+ l sync.Mutex |
|
| 14 |
+ wait sync.Cond |
|
| 15 |
+} |
|
| 16 |
+ |
|
| 17 |
+func newBufReader(r io.Reader) *bufReader {
|
|
| 18 |
+ reader := &bufReader{
|
|
| 19 |
+ buf: &bytes.Buffer{},
|
|
| 20 |
+ reader: r, |
|
| 21 |
+ } |
|
| 22 |
+ reader.wait.L = &reader.l |
|
| 23 |
+ go reader.drain() |
|
| 24 |
+ return reader |
|
| 25 |
+} |
|
| 26 |
+ |
|
| 27 |
+func (r *bufReader) drain() {
|
|
| 28 |
+ buf := make([]byte, 1024) |
|
| 29 |
+ for {
|
|
| 30 |
+ n, err := r.reader.Read(buf) |
|
| 31 |
+ if err != nil {
|
|
| 32 |
+ r.err = err |
|
| 33 |
+ } else {
|
|
| 34 |
+ r.buf.Write(buf[0:n]) |
|
| 35 |
+ } |
|
| 36 |
+ r.l.Lock() |
|
| 37 |
+ r.wait.Signal() |
|
| 38 |
+ r.l.Unlock() |
|
| 39 |
+ if err != nil {
|
|
| 40 |
+ break |
|
| 41 |
+ } |
|
| 42 |
+ } |
|
| 43 |
+} |
|
| 44 |
+ |
|
| 45 |
+func (r *bufReader) Read(p []byte) (n int, err error) {
|
|
| 46 |
+ for {
|
|
| 47 |
+ n, err = r.buf.Read(p) |
|
| 48 |
+ if n > 0 {
|
|
| 49 |
+ return n, err |
|
| 50 |
+ } |
|
| 51 |
+ if r.err != nil {
|
|
| 52 |
+ return 0, r.err |
|
| 53 |
+ } |
|
| 54 |
+ r.l.Lock() |
|
| 55 |
+ r.wait.Wait() |
|
| 56 |
+ r.l.Unlock() |
|
| 57 |
+ } |
|
| 58 |
+ return |
|
| 59 |
+} |
|
| 60 |
+ |
|
| 61 |
+func (r *bufReader) Close() error {
|
|
| 62 |
+ closer, ok := r.reader.(io.ReadCloser) |
|
| 63 |
+ if !ok {
|
|
| 64 |
+ return nil |
|
| 65 |
+ } |
|
| 66 |
+ return closer.Close() |
|
| 67 |
+} |
|
| 68 |
+ |
|
| 69 |
+type writeBroadcaster struct {
|
|
| 70 |
+ writers *list.List |
|
| 71 |
+} |
|
| 72 |
+ |
|
| 73 |
+func (w *writeBroadcaster) AddWriter(writer io.WriteCloser) {
|
|
| 74 |
+ w.writers.PushBack(writer) |
|
| 75 |
+} |
|
| 76 |
+ |
|
| 77 |
+func (w *writeBroadcaster) RemoveWriter(writer io.WriteCloser) {
|
|
| 78 |
+ for e := w.writers.Front(); e != nil; e = e.Next() {
|
|
| 79 |
+ v := e.Value.(io.Writer) |
|
| 80 |
+ if v == writer {
|
|
| 81 |
+ w.writers.Remove(e) |
|
| 82 |
+ return |
|
| 83 |
+ } |
|
| 84 |
+ } |
|
| 85 |
+} |
|
| 86 |
+ |
|
| 87 |
+func (w *writeBroadcaster) Write(p []byte) (n int, err error) {
|
|
| 88 |
+ failed := []*list.Element{}
|
|
| 89 |
+ for e := w.writers.Front(); e != nil; e = e.Next() {
|
|
| 90 |
+ writer := e.Value.(io.Writer) |
|
| 91 |
+ if n, err := writer.Write(p); err != nil || n != len(p) {
|
|
| 92 |
+ // On error, evict the writer |
|
| 93 |
+ failed = append(failed, e) |
|
| 94 |
+ } |
|
| 95 |
+ } |
|
| 96 |
+ // We cannot remove while iterating, so it has to be done in |
|
| 97 |
+ // a separate step |
|
| 98 |
+ for _, e := range failed {
|
|
| 99 |
+ w.writers.Remove(e) |
|
| 100 |
+ } |
|
| 101 |
+ return len(p), nil |
|
| 102 |
+} |
|
| 103 |
+ |
|
| 104 |
+func (w *writeBroadcaster) Close() error {
|
|
| 105 |
+ for e := w.writers.Front(); e != nil; e = e.Next() {
|
|
| 106 |
+ writer := e.Value.(io.WriteCloser) |
|
| 107 |
+ writer.Close() |
|
| 108 |
+ } |
|
| 109 |
+ return nil |
|
| 110 |
+} |
|
| 111 |
+ |
|
| 112 |
+func newWriteBroadcaster() *writeBroadcaster {
|
|
| 113 |
+ return &writeBroadcaster{list.New()}
|
|
| 114 |
+} |
| 0 | 115 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,126 @@ |
| 0 |
+package docker |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "errors" |
|
| 5 |
+ "io" |
|
| 6 |
+ "io/ioutil" |
|
| 7 |
+ "testing" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+func TestBufReader(t *testing.T) {
|
|
| 11 |
+ reader, writer := io.Pipe() |
|
| 12 |
+ bufreader := newBufReader(reader) |
|
| 13 |
+ |
|
| 14 |
+ // Write everything down to a Pipe |
|
| 15 |
+ // Usually, a pipe should block but because of the buffered reader, |
|
| 16 |
+ // the writes will go through |
|
| 17 |
+ done := make(chan bool) |
|
| 18 |
+ go func() {
|
|
| 19 |
+ writer.Write([]byte("hello world"))
|
|
| 20 |
+ writer.Close() |
|
| 21 |
+ done <- true |
|
| 22 |
+ }() |
|
| 23 |
+ |
|
| 24 |
+ // Drain the reader *after* everything has been written, just to verify |
|
| 25 |
+ // it is indeed buffering |
|
| 26 |
+ <-done |
|
| 27 |
+ output, err := ioutil.ReadAll(bufreader) |
|
| 28 |
+ if err != nil {
|
|
| 29 |
+ t.Fatal(err) |
|
| 30 |
+ } |
|
| 31 |
+ if !bytes.Equal(output, []byte("hello world")) {
|
|
| 32 |
+ t.Error(string(output)) |
|
| 33 |
+ } |
|
| 34 |
+} |
|
| 35 |
+ |
|
| 36 |
+type dummyWriter struct {
|
|
| 37 |
+ buffer bytes.Buffer |
|
| 38 |
+ failOnWrite bool |
|
| 39 |
+} |
|
| 40 |
+ |
|
| 41 |
+func (dw *dummyWriter) Write(p []byte) (n int, err error) {
|
|
| 42 |
+ if dw.failOnWrite {
|
|
| 43 |
+ return 0, errors.New("Fake fail")
|
|
| 44 |
+ } |
|
| 45 |
+ return dw.buffer.Write(p) |
|
| 46 |
+} |
|
| 47 |
+ |
|
| 48 |
+func (dw *dummyWriter) String() string {
|
|
| 49 |
+ return dw.buffer.String() |
|
| 50 |
+} |
|
| 51 |
+ |
|
| 52 |
+func (dw *dummyWriter) Close() error {
|
|
| 53 |
+ return nil |
|
| 54 |
+} |
|
| 55 |
+ |
|
| 56 |
+func TestWriteBroadcaster(t *testing.T) {
|
|
| 57 |
+ writer := newWriteBroadcaster() |
|
| 58 |
+ |
|
| 59 |
+ // Test 1: Both bufferA and bufferB should contain "foo" |
|
| 60 |
+ bufferA := &dummyWriter{}
|
|
| 61 |
+ writer.AddWriter(bufferA) |
|
| 62 |
+ bufferB := &dummyWriter{}
|
|
| 63 |
+ writer.AddWriter(bufferB) |
|
| 64 |
+ writer.Write([]byte("foo"))
|
|
| 65 |
+ |
|
| 66 |
+ if bufferA.String() != "foo" {
|
|
| 67 |
+ t.Errorf("Buffer contains %v", bufferA.String())
|
|
| 68 |
+ } |
|
| 69 |
+ |
|
| 70 |
+ if bufferB.String() != "foo" {
|
|
| 71 |
+ t.Errorf("Buffer contains %v", bufferB.String())
|
|
| 72 |
+ } |
|
| 73 |
+ |
|
| 74 |
+ // Test2: bufferA and bufferB should contain "foobar", |
|
| 75 |
+ // while bufferC should only contain "bar" |
|
| 76 |
+ bufferC := &dummyWriter{}
|
|
| 77 |
+ writer.AddWriter(bufferC) |
|
| 78 |
+ writer.Write([]byte("bar"))
|
|
| 79 |
+ |
|
| 80 |
+ if bufferA.String() != "foobar" {
|
|
| 81 |
+ t.Errorf("Buffer contains %v", bufferA.String())
|
|
| 82 |
+ } |
|
| 83 |
+ |
|
| 84 |
+ if bufferB.String() != "foobar" {
|
|
| 85 |
+ t.Errorf("Buffer contains %v", bufferB.String())
|
|
| 86 |
+ } |
|
| 87 |
+ |
|
| 88 |
+ if bufferC.String() != "bar" {
|
|
| 89 |
+ t.Errorf("Buffer contains %v", bufferC.String())
|
|
| 90 |
+ } |
|
| 91 |
+ |
|
| 92 |
+ // Test3: Test removal |
|
| 93 |
+ writer.RemoveWriter(bufferB) |
|
| 94 |
+ writer.Write([]byte("42"))
|
|
| 95 |
+ if bufferA.String() != "foobar42" {
|
|
| 96 |
+ t.Errorf("Buffer contains %v", bufferA.String())
|
|
| 97 |
+ } |
|
| 98 |
+ if bufferB.String() != "foobar" {
|
|
| 99 |
+ t.Errorf("Buffer contains %v", bufferB.String())
|
|
| 100 |
+ } |
|
| 101 |
+ if bufferC.String() != "bar42" {
|
|
| 102 |
+ t.Errorf("Buffer contains %v", bufferC.String())
|
|
| 103 |
+ } |
|
| 104 |
+ |
|
| 105 |
+ // Test4: Test eviction on failure |
|
| 106 |
+ bufferA.failOnWrite = true |
|
| 107 |
+ writer.Write([]byte("fail"))
|
|
| 108 |
+ if bufferA.String() != "foobar42" {
|
|
| 109 |
+ t.Errorf("Buffer contains %v", bufferA.String())
|
|
| 110 |
+ } |
|
| 111 |
+ if bufferC.String() != "bar42fail" {
|
|
| 112 |
+ t.Errorf("Buffer contains %v", bufferC.String())
|
|
| 113 |
+ } |
|
| 114 |
+ // Even though we reset the flag, no more writes should go in there |
|
| 115 |
+ bufferA.failOnWrite = false |
|
| 116 |
+ writer.Write([]byte("test"))
|
|
| 117 |
+ if bufferA.String() != "foobar42" {
|
|
| 118 |
+ t.Errorf("Buffer contains %v", bufferA.String())
|
|
| 119 |
+ } |
|
| 120 |
+ if bufferC.String() != "bar42failtest" {
|
|
| 121 |
+ t.Errorf("Buffer contains %v", bufferC.String())
|
|
| 122 |
+ } |
|
| 123 |
+ |
|
| 124 |
+ writer.Close() |
|
| 125 |
+} |