From 389e5ea6a07ad30cab3bfd521fc80f6ea1a77a0a Mon Sep 17 00:00:00 2001 From: elwin cheng Date: Mon, 7 Jul 2025 20:37:02 -0400 Subject: [PATCH 01/11] create a container --- src/images/serve_image.go | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/images/serve_image.go diff --git a/src/images/serve_image.go b/src/images/serve_image.go new file mode 100644 index 0000000..57f45ae --- /dev/null +++ b/src/images/serve_image.go @@ -0,0 +1,52 @@ +package main + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" +) + +func main() { + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + panic(err) + } + + hostConfig := &container.HostConfig{ + Runtime: "nvidia", + } + + resp, err := cli.ContainerCreate(ctx, &container.Config{ + Image: "pytorch-cuda", + Cmd: []string{"sleep", "1000"}, + }, hostConfig, nil, nil, "") + if err != nil { + panic(err) + } + fmt.Println(resp.ID) + + if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { + panic(err) + } + + statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) + select { + case err := <-errCh: + if err != nil { + panic(err) + } + case <-statusCh: + } + + out, err := cli.ContainerLogs(ctx, resp.ID, container.LogsOptions{ShowStdout: true}) + if err != nil { + panic(err) + } + + io.Copy(os.Stdout, out) +} From c3d2d1779baa21b64dc11d962f5e68cd2a522982 Mon Sep 17 00:00:00 2001 From: elwin cheng Date: Sun, 13 Jul 2025 17:17:58 -0400 Subject: [PATCH 02/11] Create and mount a volume --- src/images/serve_image.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/images/serve_image.go b/src/images/serve_image.go index 57f45ae..c5de9dc 100644 --- a/src/images/serve_image.go +++ b/src/images/serve_image.go @@ -7,6 +7,8 @@ import ( "os" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/volume" "github.com/docker/docker/client" ) @@ -17,8 +19,24 @@ func main() { panic(err) } + // Create a Docker volume + vol, err := cli.VolumeCreate(ctx, volume.CreateOptions{ + Name: "my_volume", // You can leave this empty for a random name + }) + if err != nil { + panic(err) + } + fmt.Println("Created volume:", vol.Name) + hostConfig := &container.HostConfig{ Runtime: "nvidia", + Mounts: []mount.Mount{ + { + Type: mount.TypeVolume, + Source: vol.Name, + Target: "/data", + }, + }, } resp, err := cli.ContainerCreate(ctx, &container.Config{ From dbf7b9c6ea8ef5a264927032dd5d2b31a781d0ea Mon Sep 17 00:00:00 2001 From: elwin cheng Date: Sun, 13 Jul 2025 17:45:07 -0400 Subject: [PATCH 03/11] OOP --- src/images/serve_image.go | 57 +++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/src/images/serve_image.go b/src/images/serve_image.go index c5de9dc..33c8a2d 100644 --- a/src/images/serve_image.go +++ b/src/images/serve_image.go @@ -12,28 +12,37 @@ import ( "github.com/docker/docker/client" ) -func main() { - ctx := context.Background() - cli, err := client.NewClientWithOpts(client.FromEnv) - if err != nil { - panic(err) - } +type ContainerMgr struct { + ctx context.Context + cli *client.Client +} - // Create a Docker volume - vol, err := cli.VolumeCreate(ctx, volume.CreateOptions{ - Name: "my_volume", // You can leave this empty for a random name - }) - if err != nil { - panic(err) +func NewContainerMgr(client *client.Client) *ContainerMgr { + return &ContainerMgr{ + ctx: context.Background(), + cli: client, } - fmt.Println("Created volume:", vol.Name) +} + +func (containerMgr *ContainerMgr) spinUpContainer() { +} + +func (containerMgr *ContainerMgr) spinUpContainerRocm() { +} + +func (containerMgr *ContainerMgr) spinUpContainerCpu() { +} + +func (containerMgr *ContainerMgr) spinUpContainerCuda(volName string) { + ctx := containerMgr.ctx + cli := containerMgr.cli hostConfig := &container.HostConfig{ Runtime: "nvidia", Mounts: []mount.Mount{ { Type: mount.TypeVolume, - Source: vol.Name, + Source: volName, Target: "/data", }, }, @@ -68,3 +77,23 @@ func main() { io.Copy(os.Stdout, out) } + +func main() { + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + panic(err) + } + + // Create a Docker volume + vol, err := cli.VolumeCreate(ctx, volume.CreateOptions{ + Name: "my_volume", // You can leave this empty for a random name + }) + if err != nil { + panic(err) + } + fmt.Println("Created volume:", vol.Name) + containerMgr := NewContainerMgr(cli) + containerMgr.spinUpContainerCuda("my_volume") + +} From f08545debcc66477c16b5566c75f950875c86ea0 Mon Sep 17 00:00:00 2001 From: elwin cheng Date: Sun, 20 Jul 2025 13:57:38 -0400 Subject: [PATCH 04/11] api --- src/images/serve_image.go | 42 +++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/images/serve_image.go b/src/images/serve_image.go index 33c8a2d..fa86971 100644 --- a/src/images/serve_image.go +++ b/src/images/serve_image.go @@ -24,16 +24,37 @@ func NewContainerMgr(client *client.Client) *ContainerMgr { } } -func (containerMgr *ContainerMgr) spinUpContainer() { +func (containerMgr *ContainerMgr) runContainer() { } -func (containerMgr *ContainerMgr) spinUpContainerRocm() { +func (containerMgr *ContainerMgr) runContainerRocm() { } -func (containerMgr *ContainerMgr) spinUpContainerCpu() { +func (containerMgr *ContainerMgr) runContainerCpu() { } -func (containerMgr *ContainerMgr) spinUpContainerCuda(volName string) { +func (containerMgr *ContainerMgr) killContainer() { + +} + +func (containerMgr *ContainerMgr) createVolume(volumeName string) volume.Volume { + ctx := containerMgr.ctx + cli := containerMgr.cli + + vol, err := cli.VolumeCreate(ctx, volume.CreateOptions{ + Name: "my_volume", // You can leave this empty for a random name + }) + if err != nil { + panic(err) + } + fmt.Println("Created volume:", vol.Name) + return vol +} + +func (containerMgr *ContainerMgr) deleteVolume() { +} + +func (containerMgr *ContainerMgr) runContainerCuda(volName string) { ctx := containerMgr.ctx cli := containerMgr.cli @@ -79,21 +100,16 @@ func (containerMgr *ContainerMgr) spinUpContainerCuda(volName string) { } func main() { - ctx := context.Background() + // ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { panic(err) } // Create a Docker volume - vol, err := cli.VolumeCreate(ctx, volume.CreateOptions{ - Name: "my_volume", // You can leave this empty for a random name - }) - if err != nil { - panic(err) - } - fmt.Println("Created volume:", vol.Name) containerMgr := NewContainerMgr(cli) - containerMgr.spinUpContainerCuda("my_volume") + volumeName := "my_volume" + containerMgr.createVolume(volumeName) + containerMgr.runContainerCuda(volumeName) } From 07d187313ef207de0ee623d3cc623d479ab5845b Mon Sep 17 00:00:00 2001 From: elwin cheng Date: Mon, 21 Jul 2025 20:20:54 -0400 Subject: [PATCH 05/11] add all apis --- src/images/serve_image.go | 63 +++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/src/images/serve_image.go b/src/images/serve_image.go index fa86971..e28d1ce 100644 --- a/src/images/serve_image.go +++ b/src/images/serve_image.go @@ -33,7 +33,27 @@ func (containerMgr *ContainerMgr) runContainerRocm() { func (containerMgr *ContainerMgr) runContainerCpu() { } -func (containerMgr *ContainerMgr) killContainer() { +func (containerMgr *ContainerMgr) stopContainer(containerID string) { + ctx := containerMgr.ctx + cli := containerMgr.cli + + err := cli.ContainerStop(ctx, containerID, container.StopOptions{}) + if err != nil { + panic(err) + } + fmt.Println("stopped container:", containerID) + +} + +func (containerMgr *ContainerMgr) removeContainer(containerID string) { + ctx := containerMgr.ctx + cli := containerMgr.cli + + err := cli.ContainerRemove(ctx, containerID, container.RemoveOptions{RemoveVolumes: true}) + if err != nil { + panic(err) + } + fmt.Println("remove container:", containerID) } @@ -42,7 +62,7 @@ func (containerMgr *ContainerMgr) createVolume(volumeName string) volume.Volume cli := containerMgr.cli vol, err := cli.VolumeCreate(ctx, volume.CreateOptions{ - Name: "my_volume", // You can leave this empty for a random name + Name: volumeName, // You can leave this empty for a random name }) if err != nil { panic(err) @@ -51,10 +71,19 @@ func (containerMgr *ContainerMgr) createVolume(volumeName string) volume.Volume return vol } -func (containerMgr *ContainerMgr) deleteVolume() { +func (containerMgr *ContainerMgr) removeVolume(volumeName string, force bool) bool { + ctx := containerMgr.ctx + cli := containerMgr.cli + + err := cli.VolumeRemove(ctx, volumeName, force) + if err != nil { + panic(err) + } + fmt.Println("Remove volume:", volumeName) + return true } -func (containerMgr *ContainerMgr) runContainerCuda(volName string) { +func (containerMgr *ContainerMgr) runContainerCuda(volName string) string { ctx := containerMgr.ctx cli := containerMgr.cli @@ -82,14 +111,14 @@ func (containerMgr *ContainerMgr) runContainerCuda(volName string) { panic(err) } - statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) - select { - case err := <-errCh: - if err != nil { - panic(err) - } - case <-statusCh: - } + // statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) + // select { + // case err := <-errCh: + // if err != nil { + // panic(err) + // } + // case <-statusCh: + // } out, err := cli.ContainerLogs(ctx, resp.ID, container.LogsOptions{ShowStdout: true}) if err != nil { @@ -97,10 +126,10 @@ func (containerMgr *ContainerMgr) runContainerCuda(volName string) { } io.Copy(os.Stdout, out) + return resp.ID } func main() { - // ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { panic(err) @@ -108,8 +137,12 @@ func main() { // Create a Docker volume containerMgr := NewContainerMgr(cli) - volumeName := "my_volume" + volumeName := "my_volume1" + containerMgr.createVolume(volumeName) - containerMgr.runContainerCuda(volumeName) + id := containerMgr.runContainerCuda(volumeName) + containerMgr.stopContainer(id) + containerMgr.removeContainer(id) + containerMgr.removeVolume(volumeName, true) } From 1b4f7009e5d9997ac43938d3aa7819f8a3e059b9 Mon Sep 17 00:00:00 2001 From: elwin cheng Date: Sat, 2 Aug 2025 16:59:43 -0700 Subject: [PATCH 06/11] Add testing plan --- src/images/serve_image.go | 18 ------ src/images/serve_image_test.go | 103 +++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 18 deletions(-) create mode 100644 src/images/serve_image_test.go diff --git a/src/images/serve_image.go b/src/images/serve_image.go index e28d1ce..833bd9e 100644 --- a/src/images/serve_image.go +++ b/src/images/serve_image.go @@ -24,15 +24,6 @@ func NewContainerMgr(client *client.Client) *ContainerMgr { } } -func (containerMgr *ContainerMgr) runContainer() { -} - -func (containerMgr *ContainerMgr) runContainerRocm() { -} - -func (containerMgr *ContainerMgr) runContainerCpu() { -} - func (containerMgr *ContainerMgr) stopContainer(containerID string) { ctx := containerMgr.ctx cli := containerMgr.cli @@ -111,15 +102,6 @@ func (containerMgr *ContainerMgr) runContainerCuda(volName string) string { panic(err) } - // statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) - // select { - // case err := <-errCh: - // if err != nil { - // panic(err) - // } - // case <-statusCh: - // } - out, err := cli.ContainerLogs(ctx, resp.ID, container.LogsOptions{ShowStdout: true}) if err != nil { panic(err) diff --git a/src/images/serve_image_test.go b/src/images/serve_image_test.go new file mode 100644 index 0000000..24dc08a --- /dev/null +++ b/src/images/serve_image_test.go @@ -0,0 +1,103 @@ +package main + +import ( + "testing" + + "github.com/docker/docker/client" +) + +func TestImage(t *testing.T) { + + t.Log("Hello world") + // t.Error("F") + + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + panic(err) + } + + // Create a Docker volume + containerMgr := NewContainerMgr(cli) + volumeName := "my_volume1" + + containerMgr.createVolume(volumeName) + // assert volume meets certain requirements + // 1. The total capacity of the volume + // 2. + + // What if you create a volume with the same name + + // What if you delete a volume with a name that doesn't exist + + // T 1 + // create a volume + // check volume exists + // delete volume + // check volume does not exist + // cleanup + + // T 2 + // create a volume + // start container and attach volume + // check volume is accessible + // write to the volume + // stop the container + // start the containerr + // check that data is persisted + // cleanup + + // T3 + // Create a volume name + // createa a volume name -> should fail + // cleanup + + // T4 + // No volume + // remove volume that doesn't exist -> should fail + + // T5 + // Remove volume in the middle of running -> should fail + + // T6 + // Attach a volume that does not exist -> should fail + + // T7 + // two containers attach to the same volume -> should fail + + // T8 + // Two containers trying to attach to the same volume -> should fail + + // how to test things? + + // spawn 10,000 containers. + + // set a limit of 100 volumes. + // set a limit of 1000 containers + // + + // id := containerMgr.runContainerCuda(volumeName) + // containerMgr.stopContainer(id) + // containerMgr.removeContainer(id) + // containerMgr.removeVolume(volumeName, true) +} + +// func TestImage(t *testing.T) { + +// t.Log("Hello world") +// t.Error("F") + +// cli, err := client.NewClientWithOpts(client.FromEnv) +// if err != nil { +// panic(err) +// } + +// // Create a Docker volume +// containerMgr := NewContainerMgr(cli) +// volumeName := "my_volume1" + +// containerMgr.createVolume(volumeName) +// id := containerMgr.runContainerCuda(volumeName) +// containerMgr.stopContainer(id) +// containerMgr.removeContainer(id) +// containerMgr.removeVolume(volumeName, true) +// } From 7f1173777004fb3b6fcd8c5f35c9348d8ae191b7 Mon Sep 17 00:00:00 2001 From: elwin cheng Date: Sun, 10 Aug 2025 16:25:36 -0700 Subject: [PATCH 07/11] tests --- src/images/serve_image_test.go | 241 ++++++++++++++++++++++----------- 1 file changed, 160 insertions(+), 81 deletions(-) diff --git a/src/images/serve_image_test.go b/src/images/serve_image_test.go index 24dc08a..497dcc7 100644 --- a/src/images/serve_image_test.go +++ b/src/images/serve_image_test.go @@ -1,103 +1,182 @@ package main import ( + "fmt" "testing" + "github.com/docker/docker/api/types/volume" "github.com/docker/docker/client" ) -func TestImage(t *testing.T) { - - t.Log("Hello world") - // t.Error("F") - +func setupMgr(t *testing.T) *ContainerMgr { cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { - panic(err) + t.Fatalf("Failed to create Docker client: %v", err) } + return NewContainerMgr(cli) +} - // Create a Docker volume - containerMgr := NewContainerMgr(cli) - volumeName := "my_volume1" - - containerMgr.createVolume(volumeName) - // assert volume meets certain requirements - // 1. The total capacity of the volume - // 2. - - // What if you create a volume with the same name - - // What if you delete a volume with a name that doesn't exist - - // T 1 - // create a volume - // check volume exists - // delete volume - // check volume does not exist - // cleanup - - // T 2 - // create a volume - // start container and attach volume - // check volume is accessible - // write to the volume - // stop the container - // start the containerr - // check that data is persisted - // cleanup - - // T3 - // Create a volume name - // createa a volume name -> should fail - // cleanup - - // T4 - // No volume - // remove volume that doesn't exist -> should fail - - // T5 - // Remove volume in the middle of running -> should fail - - // T6 - // Attach a volume that does not exist -> should fail - - // T7 - // two containers attach to the same volume -> should fail - - // T8 - // Two containers trying to attach to the same volume -> should fail +// T1: create a volume, check exists, delete, check not exists +func TestCreateDeleteVolume(t *testing.T) { + mgr := setupMgr(t) + volName := "test_volume_t1" + mgr.createVolume(volName) + // vols, _ := mgr.cli.VolumeList(mgr.ctx, *opts*/ {}) + vols, _ := mgr.cli.VolumeList(mgr.ctx, volume.ListOptions{}) + found := false + for _, v := range vols.Volumes { + if v.Name == volName { + found = true + break + } + } + if !found { + t.Errorf("Volume %s not found after creation", volName) + } + mgr.removeVolume(volName, true) + // vols, _ = mgr.cli.VolumeList(mgr.ctx, /*opts*/ {}) + vols, _ = mgr.cli.VolumeList(mgr.ctx, volume.ListOptions{}) + for _, v := range vols.Volumes { + if v.Name == volName { + t.Errorf("Volume %s still exists after deletion", volName) + } + } +} - // how to test things? +// T2: create volume, start container, attach, write, stop, start, check persistence, cleanup +func TestVolumePersistence(t *testing.T) { + mgr := setupMgr(t) + volName := "test_volume_t2" + mgr.createVolume(volName) + containerID := mgr.runContainerCuda(volName) + // Write to volume (you'd need to exec into container or mount and write a file) + // For example, use mgr.execInContainer(containerID, "sh", "-c", "echo hello > /data/test.txt") + // Stop and start container-p + mgr.stopContainer(containerID) + // mgr.startContainer(containerID) + // Check file exists (again, exec into container and check) + // Cleanup + mgr.stopContainer(containerID) + mgr.removeContainer(containerID) + mgr.removeVolume(volName, true) +} - // spawn 10,000 containers. +// T3: create a volume with same name twice (should not fail) +func TestCreateVolumeTwice(t *testing.T) { + mgr := setupMgr(t) + volName := "test_volume_t3" + mgr.createVolume(volName) + defer mgr.removeVolume(volName, true) + mgr.createVolume(volName) // Should not fail +} - // set a limit of 100 volumes. - // set a limit of 1000 containers - // +// T4: remove volume that doesn't exist (should fail or panic) +func TestRemoveNonexistentVolume(t *testing.T) { + mgr := setupMgr(t) + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected panic when removing nonexistent volume, but did not panic") + } + }() + mgr.removeVolume("nonexistent_volume_t4", true) // Maybe this function never panics +} - // id := containerMgr.runContainerCuda(volumeName) - // containerMgr.stopContainer(id) - // containerMgr.removeContainer(id) - // containerMgr.removeVolume(volumeName, true) +// T5: remove volume in use (should fail or panic) +func TestRemoveVolumeInUse(t *testing.T) { + mgr := setupMgr(t) + volName := "test_volume_t5" + mgr.createVolume(volName) + containerID := mgr.runContainerCuda(volName) + defer func() { + mgr.stopContainer(containerID) + mgr.removeContainer(containerID) + mgr.removeVolume(volName, true) + }() + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected panic when removing volume in use, but did not panic") + } + }() + mgr.removeVolume(volName, true) // why didn't this panic? } -// func TestImage(t *testing.T) { +// T6: attach a volume that does not exist (should fail or panic) +func TestAttachNonexistentVolume(t *testing.T) { + mgr := setupMgr(t) + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected panic when attaching nonexistent volume, but did not panic") + } + }() + mgr.runContainerCuda("nonexistent_volume_t6") // why did this one panic/work +} -// t.Log("Hello world") -// t.Error("F") +// T7: two containers attach to the same volume (should succeed in Docker, but test for your policy) +func TestTwoContainersSameVolume(t *testing.T) { + mgr := setupMgr(t) + volName := "test_volume_t7" + mgr.createVolume(volName) + id1 := mgr.runContainerCuda(volName) + id2 := mgr.runContainerCuda(volName) + mgr.stopContainer(id1) + mgr.removeContainer(id1) + mgr.stopContainer(id2) + mgr.removeContainer(id2) + mgr.removeVolume(volName, true) +} -// cli, err := client.NewClientWithOpts(client.FromEnv) -// if err != nil { -// panic(err) -// } +// T8: two containers try to attach to the same volume at the same time (should succeed in Docker) +func TestTwoContainersSameVolumeConcurrent(t *testing.T) { + mgr := setupMgr(t) + volName := "test_volume_t8" + mgr.createVolume(volName) + id1 := mgr.runContainerCuda(volName) + id2 := mgr.runContainerCuda(volName) + mgr.stopContainer(id1) + mgr.removeContainer(id1) + mgr.stopContainer(id2) + mgr.removeContainer(id2) + mgr.removeVolume(volName, true) +} -// // Create a Docker volume -// containerMgr := NewContainerMgr(cli) -// volumeName := "my_volume1" +// T9: set a limit of 100 volumes (should fail on 101st if you enforce a limit) +func TestVolumeLimit(t *testing.T) { + mgr := setupMgr(t) + limit := 100 + created := []string{} + for i := 0; i < limit; i++ { + name := "test_volume_t9_" + fmt.Sprint(i) + mgr.createVolume(name) + created = append(created, name) + } + defer func() { + for _, name := range created { + mgr.removeVolume(name, true) + } + }() + // why didn't you clean up the volumes + // Try to create one more if you enforce a limit + // If not enforced, this will succeed +} -// containerMgr.createVolume(volumeName) -// id := containerMgr.runContainerCuda(volumeName) -// containerMgr.stopContainer(id) -// containerMgr.removeContainer(id) -// containerMgr.removeVolume(volumeName, true) -// } +// T10: set a limit of 10 containers (should fail on 11th if you enforce a limit) +func TestContainerLimit(t *testing.T) { + mgr := setupMgr(t) + volName := "test_volume_t10" + mgr.createVolume(volName) + ids := []string{} + limit := 10 + for i := 0; i < limit; i++ { + id := mgr.runContainerCuda(volName) + ids = append(ids, id) + } + defer func() { + for _, id := range ids { + mgr.stopContainer(id) + mgr.removeContainer(id) + } + mgr.removeVolume(volName, true) // why didnt you clean up the containers? + }() + // Try to create one more if you enforce a limit + // If not enforced, this will succeed +} From c9d913732948da0cdd7cd3c2af02662e35789634 Mon Sep 17 00:00:00 2001 From: elwin cheng Date: Thu, 14 Aug 2025 06:22:37 -0700 Subject: [PATCH 08/11] fix test4 failure --- src/images/serve_image.go | 19 ++++++++++++++++--- src/images/serve_image_test.go | 15 +++++++++------ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/images/serve_image.go b/src/images/serve_image.go index 833bd9e..e56d269 100644 --- a/src/images/serve_image.go +++ b/src/images/serve_image.go @@ -62,16 +62,29 @@ func (containerMgr *ContainerMgr) createVolume(volumeName string) volume.Volume return vol } -func (containerMgr *ContainerMgr) removeVolume(volumeName string, force bool) bool { +func (containerMgr *ContainerMgr) removeVolume(volumeName string, force bool) error { ctx := containerMgr.ctx cli := containerMgr.cli + vols, _ := cli.VolumeList(ctx, volume.ListOptions{}) + found := false + for _, v := range vols.Volumes { + if v.Name == volumeName { + found = true + break + } + } + if !found { + return fmt.Errorf("volume %s does not exist", volumeName) + } + err := cli.VolumeRemove(ctx, volumeName, force) + fmt.Printf("Remove error: %#v\n", err) if err != nil { - panic(err) + return err } fmt.Println("Remove volume:", volumeName) - return true + return nil } func (containerMgr *ContainerMgr) runContainerCuda(volName string) string { diff --git a/src/images/serve_image_test.go b/src/images/serve_image_test.go index 497dcc7..0d4ccdd 100644 --- a/src/images/serve_image_test.go +++ b/src/images/serve_image_test.go @@ -73,12 +73,15 @@ func TestCreateVolumeTwice(t *testing.T) { // T4: remove volume that doesn't exist (should fail or panic) func TestRemoveNonexistentVolume(t *testing.T) { mgr := setupMgr(t) - defer func() { - if r := recover(); r == nil { - t.Errorf("Expected panic when removing nonexistent volume, but did not panic") - } - }() - mgr.removeVolume("nonexistent_volume_t4", true) // Maybe this function never panics + // defer func() { + // if r := recover(); r == nil { + // t.Errorf("Expected panic when removing nonexistent volume, but did not panic") + // } + // }() + err := mgr.removeVolume("nonexistent_volume_t4", true) // Maybe this function never panics + if err == nil { + t.Errorf("Expected panic when removing nonexistent volume, but did not panic") + } } // T5: remove volume in use (should fail or panic) From 5f38881063ab5d4b8f8fac7eeece402a32be6266 Mon Sep 17 00:00:00 2001 From: elwin cheng Date: Thu, 14 Aug 2025 06:31:41 -0700 Subject: [PATCH 09/11] fix t5 --- src/images/serve_image_test.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/images/serve_image_test.go b/src/images/serve_image_test.go index 0d4ccdd..85da74a 100644 --- a/src/images/serve_image_test.go +++ b/src/images/serve_image_test.go @@ -80,7 +80,7 @@ func TestRemoveNonexistentVolume(t *testing.T) { // }() err := mgr.removeVolume("nonexistent_volume_t4", true) // Maybe this function never panics if err == nil { - t.Errorf("Expected panic when removing nonexistent volume, but did not panic") + t.Errorf("Expected error when removing nonexistent volume, but no error") } } @@ -95,12 +95,10 @@ func TestRemoveVolumeInUse(t *testing.T) { mgr.removeContainer(containerID) mgr.removeVolume(volName, true) }() - defer func() { - if r := recover(); r == nil { - t.Errorf("Expected panic when removing volume in use, but did not panic") - } - }() - mgr.removeVolume(volName, true) // why didn't this panic? + err := mgr.removeVolume(volName, true) // why didn't this panic? + if err == nil { + t.Errorf("Expected error when removing nonexistent volume, but no error") + } } // T6: attach a volume that does not exist (should fail or panic) From d179a765334846fc13f5b7997cb29ef9af65c944 Mon Sep 17 00:00:00 2001 From: elwin cheng Date: Sun, 17 Aug 2025 08:52:57 -0700 Subject: [PATCH 10/11] fix all tests --- src/images/serve_image.go | 28 +++++++++++------- src/images/serve_image_test.go | 54 +++++++++++++++++++++++++--------- 2 files changed, 58 insertions(+), 24 deletions(-) diff --git a/src/images/serve_image.go b/src/images/serve_image.go index e56d269..4e649ea 100644 --- a/src/images/serve_image.go +++ b/src/images/serve_image.go @@ -32,7 +32,6 @@ func (containerMgr *ContainerMgr) stopContainer(containerID string) { if err != nil { panic(err) } - fmt.Println("stopped container:", containerID) } @@ -44,7 +43,6 @@ func (containerMgr *ContainerMgr) removeContainer(containerID string) { if err != nil { panic(err) } - fmt.Println("remove container:", containerID) } @@ -58,7 +56,6 @@ func (containerMgr *ContainerMgr) createVolume(volumeName string) volume.Volume if err != nil { panic(err) } - fmt.Println("Created volume:", vol.Name) return vol } @@ -79,15 +76,13 @@ func (containerMgr *ContainerMgr) removeVolume(volumeName string, force bool) er } err := cli.VolumeRemove(ctx, volumeName, force) - fmt.Printf("Remove error: %#v\n", err) if err != nil { return err } - fmt.Println("Remove volume:", volumeName) return nil } -func (containerMgr *ContainerMgr) runContainerCuda(volName string) string { +func (containerMgr *ContainerMgr) runContainerCuda(volumeName string) (string, error) { ctx := containerMgr.ctx cli := containerMgr.cli @@ -96,7 +91,7 @@ func (containerMgr *ContainerMgr) runContainerCuda(volName string) string { Mounts: []mount.Mount{ { Type: mount.TypeVolume, - Source: volName, + Source: volumeName, Target: "/data", }, }, @@ -106,10 +101,20 @@ func (containerMgr *ContainerMgr) runContainerCuda(volName string) string { Image: "pytorch-cuda", Cmd: []string{"sleep", "1000"}, }, hostConfig, nil, nil, "") + vols, _ := cli.VolumeList(ctx, volume.ListOptions{}) + found := false + for _, v := range vols.Volumes { + if v.Name == volumeName { + found = true + break + } + } + if !found { + return "", fmt.Errorf("volume %s does not exist", volumeName) + } if err != nil { panic(err) } - fmt.Println(resp.ID) if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { panic(err) @@ -121,7 +126,7 @@ func (containerMgr *ContainerMgr) runContainerCuda(volName string) string { } io.Copy(os.Stdout, out) - return resp.ID + return resp.ID, nil } func main() { @@ -135,7 +140,10 @@ func main() { volumeName := "my_volume1" containerMgr.createVolume(volumeName) - id := containerMgr.runContainerCuda(volumeName) + id, err := containerMgr.runContainerCuda(volumeName) + if err != nil { + fmt.Errorf("Failed to start container: %v", err.Error()) + } containerMgr.stopContainer(id) containerMgr.removeContainer(id) containerMgr.removeVolume(volumeName, true) diff --git a/src/images/serve_image_test.go b/src/images/serve_image_test.go index 85da74a..71a4cba 100644 --- a/src/images/serve_image_test.go +++ b/src/images/serve_image_test.go @@ -48,7 +48,10 @@ func TestVolumePersistence(t *testing.T) { mgr := setupMgr(t) volName := "test_volume_t2" mgr.createVolume(volName) - containerID := mgr.runContainerCuda(volName) + containerID, err := mgr.runContainerCuda(volName) + if err != nil { + fmt.Errorf("Failed to start container: %v", err.Error()) + } // Write to volume (you'd need to exec into container or mount and write a file) // For example, use mgr.execInContainer(containerID, "sh", "-c", "echo hello > /data/test.txt") // Stop and start container-p @@ -89,13 +92,16 @@ func TestRemoveVolumeInUse(t *testing.T) { mgr := setupMgr(t) volName := "test_volume_t5" mgr.createVolume(volName) - containerID := mgr.runContainerCuda(volName) + containerID, err := mgr.runContainerCuda(volName) + if err != nil { + fmt.Errorf("Failed to start container: %v", err.Error()) + } defer func() { mgr.stopContainer(containerID) mgr.removeContainer(containerID) mgr.removeVolume(volName, true) }() - err := mgr.removeVolume(volName, true) // why didn't this panic? + err = mgr.removeVolume(volName, true) // why didn't this panic? if err == nil { t.Errorf("Expected error when removing nonexistent volume, but no error") } @@ -104,12 +110,17 @@ func TestRemoveVolumeInUse(t *testing.T) { // T6: attach a volume that does not exist (should fail or panic) func TestAttachNonexistentVolume(t *testing.T) { mgr := setupMgr(t) - defer func() { - if r := recover(); r == nil { - t.Errorf("Expected panic when attaching nonexistent volume, but did not panic") - } - }() - mgr.runContainerCuda("nonexistent_volume_t6") // why did this one panic/work + // defer func() { + // if r := recover(); r == nil { + // t.Errorf("Expected panic when attaching nonexistent volume, but did not panic") + // } + // }() + + id, err := mgr.runContainerCuda("nonexistent_volume_t6") // why did this one panic/work + // fmt.Printf("ALLO: %v, %v", id, err) + if id != "" && err != nil { + t.Errorf("Expected error when removing nonexistent volume, but no error") + } } // T7: two containers attach to the same volume (should succeed in Docker, but test for your policy) @@ -117,8 +128,14 @@ func TestTwoContainersSameVolume(t *testing.T) { mgr := setupMgr(t) volName := "test_volume_t7" mgr.createVolume(volName) - id1 := mgr.runContainerCuda(volName) - id2 := mgr.runContainerCuda(volName) + id1, err := mgr.runContainerCuda(volName) + if err != nil { + fmt.Errorf("Failed to start container: %v", err.Error()) + } + id2, err := mgr.runContainerCuda(volName) + if err != nil { + fmt.Errorf("Failed to start container: %v", err.Error()) + } mgr.stopContainer(id1) mgr.removeContainer(id1) mgr.stopContainer(id2) @@ -131,8 +148,14 @@ func TestTwoContainersSameVolumeConcurrent(t *testing.T) { mgr := setupMgr(t) volName := "test_volume_t8" mgr.createVolume(volName) - id1 := mgr.runContainerCuda(volName) - id2 := mgr.runContainerCuda(volName) + id1, err := mgr.runContainerCuda(volName) + if err != nil { + fmt.Errorf("Failed to start container: %v", err.Error()) + } + id2, err2 := mgr.runContainerCuda(volName) + if err2 != nil { + fmt.Errorf("Failed to start container: %v", err2.Error()) + } mgr.stopContainer(id1) mgr.removeContainer(id1) mgr.stopContainer(id2) @@ -168,7 +191,10 @@ func TestContainerLimit(t *testing.T) { ids := []string{} limit := 10 for i := 0; i < limit; i++ { - id := mgr.runContainerCuda(volName) + id, err := mgr.runContainerCuda(volName) + if err != nil { + fmt.Errorf("Failed to start container: %v", err.Error()) + } ids = append(ids, id) } defer func() { From 9b54bf2d09f4c96b3161f7ad02483eb967914925 Mon Sep 17 00:00:00 2001 From: elwin cheng Date: Sun, 17 Aug 2025 09:08:07 -0700 Subject: [PATCH 11/11] Add container and volume limit --- src/images/serve_image.go | 86 ++++++++++++++++++++-------------- src/images/serve_image_test.go | 12 ++++- 2 files changed, 61 insertions(+), 37 deletions(-) diff --git a/src/images/serve_image.go b/src/images/serve_image.go index 4e649ea..6084d2c 100644 --- a/src/images/serve_image.go +++ b/src/images/serve_image.go @@ -13,20 +13,28 @@ import ( ) type ContainerMgr struct { - ctx context.Context - cli *client.Client + ctx context.Context + cli *client.Client + containerLimit int + volumeLimit int + containers map[string]struct{} + volumes map[string]struct{} } -func NewContainerMgr(client *client.Client) *ContainerMgr { +func NewContainerMgr(client *client.Client, containerLimit, volumeLimit int) *ContainerMgr { return &ContainerMgr{ - ctx: context.Background(), - cli: client, + ctx: context.Background(), + cli: client, + containerLimit: containerLimit, + volumeLimit: volumeLimit, + containers: make(map[string]struct{}), + volumes: make(map[string]struct{}), } } -func (containerMgr *ContainerMgr) stopContainer(containerID string) { - ctx := containerMgr.ctx - cli := containerMgr.cli +func (mgr *ContainerMgr) stopContainer(containerID string) { + ctx := mgr.ctx + cli := mgr.cli err := cli.ContainerStop(ctx, containerID, container.StopOptions{}) if err != nil { @@ -35,33 +43,37 @@ func (containerMgr *ContainerMgr) stopContainer(containerID string) { } -func (containerMgr *ContainerMgr) removeContainer(containerID string) { - ctx := containerMgr.ctx - cli := containerMgr.cli - +func (mgr *ContainerMgr) removeContainer(containerID string) error { + ctx := mgr.ctx + cli := mgr.cli err := cli.ContainerRemove(ctx, containerID, container.RemoveOptions{RemoveVolumes: true}) if err != nil { - panic(err) + return err } - + delete(mgr.containers, containerID) + return nil } -func (containerMgr *ContainerMgr) createVolume(volumeName string) volume.Volume { - ctx := containerMgr.ctx - cli := containerMgr.cli +func (mgr *ContainerMgr) createVolume(volumeName string) (volume.Volume, error) { + if len(mgr.volumes) >= mgr.volumeLimit { + return volume.Volume{}, fmt.Errorf("volume limit reached") + } + ctx := mgr.ctx + cli := mgr.cli vol, err := cli.VolumeCreate(ctx, volume.CreateOptions{ Name: volumeName, // You can leave this empty for a random name }) if err != nil { - panic(err) + return volume.Volume{}, err } - return vol + mgr.volumes[vol.Name] = struct{}{} + return vol, nil } -func (containerMgr *ContainerMgr) removeVolume(volumeName string, force bool) error { - ctx := containerMgr.ctx - cli := containerMgr.cli +func (mgr *ContainerMgr) removeVolume(volumeName string, force bool) error { + ctx := mgr.ctx + cli := mgr.cli vols, _ := cli.VolumeList(ctx, volume.ListOptions{}) found := false @@ -79,14 +91,21 @@ func (containerMgr *ContainerMgr) removeVolume(volumeName string, force bool) er if err != nil { return err } + delete(mgr.volumes, volumeName) return nil } -func (containerMgr *ContainerMgr) runContainerCuda(volumeName string) (string, error) { - ctx := containerMgr.ctx - cli := containerMgr.cli +func (mgr *ContainerMgr) runContainerCuda(volumeName string) (string, error) { + if len(mgr.containers) >= mgr.containerLimit { + return "", fmt.Errorf("container limit reached") + } + ctx := mgr.ctx + cli := mgr.cli - hostConfig := &container.HostConfig{ + resp, err := cli.ContainerCreate(ctx, &container.Config{ + Image: "pytorch-cuda", + Cmd: []string{"sleep", "1000"}, + }, &container.HostConfig{ Runtime: "nvidia", Mounts: []mount.Mount{ { @@ -95,12 +114,7 @@ func (containerMgr *ContainerMgr) runContainerCuda(volumeName string) (string, e Target: "/data", }, }, - } - - resp, err := cli.ContainerCreate(ctx, &container.Config{ - Image: "pytorch-cuda", - Cmd: []string{"sleep", "1000"}, - }, hostConfig, nil, nil, "") + }, nil, nil, "") vols, _ := cli.VolumeList(ctx, volume.ListOptions{}) found := false for _, v := range vols.Volumes { @@ -113,11 +127,11 @@ func (containerMgr *ContainerMgr) runContainerCuda(volumeName string) (string, e return "", fmt.Errorf("volume %s does not exist", volumeName) } if err != nil { - panic(err) + return "", err } - + mgr.containers[resp.ID] = struct{}{} if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { - panic(err) + return "", err } out, err := cli.ContainerLogs(ctx, resp.ID, container.LogsOptions{ShowStdout: true}) @@ -136,7 +150,7 @@ func main() { } // Create a Docker volume - containerMgr := NewContainerMgr(cli) + containerMgr := NewContainerMgr(cli, 10, 10) volumeName := "my_volume1" containerMgr.createVolume(volumeName) diff --git a/src/images/serve_image_test.go b/src/images/serve_image_test.go index 71a4cba..d4a77db 100644 --- a/src/images/serve_image_test.go +++ b/src/images/serve_image_test.go @@ -13,7 +13,7 @@ func setupMgr(t *testing.T) *ContainerMgr { if err != nil { t.Fatalf("Failed to create Docker client: %v", err) } - return NewContainerMgr(cli) + return NewContainerMgr(cli, 10, 100) } // T1: create a volume, check exists, delete, check not exists @@ -173,6 +173,12 @@ func TestVolumeLimit(t *testing.T) { mgr.createVolume(name) created = append(created, name) } + name := "test_volume_fail" + _, err := mgr.createVolume(name) + if err == nil { + fmt.Errorf("Volume limit not enforced") + } + defer func() { for _, name := range created { mgr.removeVolume(name, true) @@ -197,6 +203,10 @@ func TestContainerLimit(t *testing.T) { } ids = append(ids, id) } + _, err := mgr.runContainerCuda(volName) + if err == nil { + fmt.Errorf("Container limit not enforced") + } defer func() { for _, id := range ids { mgr.stopContainer(id)