From f1c9d4063f12f2ae2b71bb82c07b78d5ff518c73 Mon Sep 17 00:00:00 2001 From: boucher Date: Thu, 14 Apr 2016 09:13:42 -0400 Subject: [PATCH] Add Checkpoint Create/List/Delete methods. Maps directly to the API exposed by containerd. Signed-off-by: boucher --- client/checkpoint_create.go | 13 ++++++ client/checkpoint_create_test.go | 73 ++++++++++++++++++++++++++++++++ client/checkpoint_delete.go | 12 ++++++ client/checkpoint_delete_test.go | 47 ++++++++++++++++++++ client/checkpoint_list.go | 22 ++++++++++ client/checkpoint_list_test.go | 57 +++++++++++++++++++++++++ client/container_start.go | 13 ++++-- client/container_start_test.go | 10 ++++- client/interface.go | 5 ++- types/client.go | 6 +++ types/types.go | 5 +++ 11 files changed, 257 insertions(+), 6 deletions(-) create mode 100644 client/checkpoint_create.go create mode 100644 client/checkpoint_create_test.go create mode 100644 client/checkpoint_delete.go create mode 100644 client/checkpoint_delete_test.go create mode 100644 client/checkpoint_list.go create mode 100644 client/checkpoint_list_test.go diff --git a/client/checkpoint_create.go b/client/checkpoint_create.go new file mode 100644 index 0000000..23883cc --- /dev/null +++ b/client/checkpoint_create.go @@ -0,0 +1,13 @@ +package client + +import ( + "github.com/docker/engine-api/types" + "golang.org/x/net/context" +) + +// CheckpointCreate creates a checkpoint from the given container with the given name +func (cli *Client) CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error { + resp, err := cli.post(ctx, "/containers/"+container+"/checkpoints", nil, options, nil) + ensureReaderClosed(resp) + return err +} diff --git a/client/checkpoint_create_test.go b/client/checkpoint_create_test.go new file mode 100644 index 0000000..a3014c4 --- /dev/null +++ b/client/checkpoint_create_test.go @@ -0,0 +1,73 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/engine-api/types" + "golang.org/x/net/context" +) + +func TestCheckpointCreateError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + err := client.CheckpointCreate(context.Background(), "nothing", types.CheckpointCreateOptions{ + CheckpointID: "noting", + Exit: true, + }) + + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestCheckpointCreate(t *testing.T) { + expectedContainerID := "container_id" + expectedCheckpointID := "checkpoint_id" + expectedURL := "/containers/container_id/checkpoints" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + + if req.Method != "POST" { + return nil, fmt.Errorf("expected POST method, got %s", req.Method) + } + + createOptions := &types.CheckpointCreateOptions{} + if err := json.NewDecoder(req.Body).Decode(createOptions); err != nil { + return nil, err + } + + if createOptions.CheckpointID != expectedCheckpointID { + return nil, fmt.Errorf("expected CheckpointID to be 'checkpoint_id', got %v", createOptions.CheckpointID) + } + + if !createOptions.Exit { + return nil, fmt.Errorf("expected Exit to be true") + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.CheckpointCreate(context.Background(), expectedContainerID, types.CheckpointCreateOptions{ + CheckpointID: expectedCheckpointID, + Exit: true, + }) + + if err != nil { + t.Fatal(err) + } +} diff --git a/client/checkpoint_delete.go b/client/checkpoint_delete.go new file mode 100644 index 0000000..a4e9ed0 --- /dev/null +++ b/client/checkpoint_delete.go @@ -0,0 +1,12 @@ +package client + +import ( + "golang.org/x/net/context" +) + +// CheckpointDelete deletes the checkpoint with the given name from the given container +func (cli *Client) CheckpointDelete(ctx context.Context, containerID string, checkpointID string) error { + resp, err := cli.delete(ctx, "/containers/"+containerID+"/checkpoints/"+checkpointID, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/client/checkpoint_delete_test.go b/client/checkpoint_delete_test.go new file mode 100644 index 0000000..097ab37 --- /dev/null +++ b/client/checkpoint_delete_test.go @@ -0,0 +1,47 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "golang.org/x/net/context" +) + +func TestCheckpointDeleteError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + err := client.CheckpointDelete(context.Background(), "container_id", "checkpoint_id") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestCheckpointDelete(t *testing.T) { + expectedURL := "/containers/container_id/checkpoints/checkpoint_id" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + if req.Method != "DELETE" { + return nil, fmt.Errorf("expected DELETE method, got %s", req.Method) + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), + }, nil + }), + } + + err := client.CheckpointDelete(context.Background(), "container_id", "checkpoint_id") + if err != nil { + t.Fatal(err) + } +} diff --git a/client/checkpoint_list.go b/client/checkpoint_list.go new file mode 100644 index 0000000..ef5ec26 --- /dev/null +++ b/client/checkpoint_list.go @@ -0,0 +1,22 @@ +package client + +import ( + "encoding/json" + + "github.com/docker/engine-api/types" + "golang.org/x/net/context" +) + +// CheckpointList returns the volumes configured in the docker host. +func (cli *Client) CheckpointList(ctx context.Context, container string) ([]types.Checkpoint, error) { + var checkpoints []types.Checkpoint + + resp, err := cli.get(ctx, "/containers/"+container+"/checkpoints", nil, nil) + if err != nil { + return checkpoints, err + } + + err = json.NewDecoder(resp.body).Decode(&checkpoints) + ensureReaderClosed(resp) + return checkpoints, err +} diff --git a/client/checkpoint_list_test.go b/client/checkpoint_list_test.go new file mode 100644 index 0000000..63c86f1 --- /dev/null +++ b/client/checkpoint_list_test.go @@ -0,0 +1,57 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/engine-api/types" + "golang.org/x/net/context" +) + +func TestCheckpointListError(t *testing.T) { + client := &Client{ + transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), + } + + _, err := client.CheckpointList(context.Background(), "container_id") + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestCheckpointList(t *testing.T) { + expectedURL := "/containers/container_id/checkpoints" + + client := &Client{ + transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + content, err := json.Marshal([]types.Checkpoint{ + { + Name: "checkpoint", + }, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + } + + checkpoints, err := client.CheckpointList(context.Background(), "container_id") + if err != nil { + t.Fatal(err) + } + if len(checkpoints) != 1 { + t.Fatalf("expected 1 checkpoint, got %v", checkpoints) + } +} diff --git a/client/container_start.go b/client/container_start.go index 12a9794..ff11c4c 100644 --- a/client/container_start.go +++ b/client/container_start.go @@ -1,10 +1,17 @@ package client -import "golang.org/x/net/context" +import ( + "net/url" + + "golang.org/x/net/context" +) // ContainerStart sends a request to the docker daemon to start a container. -func (cli *Client) ContainerStart(ctx context.Context, containerID string) error { - resp, err := cli.post(ctx, "/containers/"+containerID+"/start", nil, nil, nil) +func (cli *Client) ContainerStart(ctx context.Context, containerID string, checkpointID string) error { + query := url.Values{} + query.Set("checkpoint", checkpointID) + + resp, err := cli.post(ctx, "/containers/"+containerID+"/start", query, nil, nil) ensureReaderClosed(resp) return err } diff --git a/client/container_start_test.go b/client/container_start_test.go index d88dc54..0d885ca 100644 --- a/client/container_start_test.go +++ b/client/container_start_test.go @@ -15,7 +15,7 @@ func TestContainerStartError(t *testing.T) { client := &Client{ transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")), } - err := client.ContainerStart(context.Background(), "nothing") + err := client.ContainerStart(context.Background(), "nothing", "") if err == nil || err.Error() != "Error response from daemon: Server error" { t.Fatalf("expected a Server Error, got %v", err) } @@ -31,6 +31,12 @@ func TestContainerStart(t *testing.T) { return nil, fmt.Errorf("Unable to parse json: %s", err) } } + + checkpoint := req.URL.Query().Get("checkpoint") + if checkpoint != "checkpoint_id" { + return nil, fmt.Errorf("checkpoint not set in URL query properly. Expected 'checkpoint_id', got %s", checkpoint) + } + return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewReader([]byte(""))), @@ -38,7 +44,7 @@ func TestContainerStart(t *testing.T) { }), } - err := client.ContainerStart(context.Background(), "container_id") + err := client.ContainerStart(context.Background(), "container_id", "checkpoint_id") if err != nil { t.Fatal(err) } diff --git a/client/interface.go b/client/interface.go index 2c6872f..2dc9b22 100644 --- a/client/interface.go +++ b/client/interface.go @@ -15,6 +15,9 @@ import ( // APIClient is an interface that clients that talk with a docker server must implement. type APIClient interface { ClientVersion() string + CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error + CheckpointDelete(ctx context.Context, container string, checkpointID string) error + CheckpointList(ctx context.Context, container string) ([]types.Checkpoint, error) ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error) ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (types.ContainerCreateResponse, error) @@ -37,7 +40,7 @@ type APIClient interface { ContainerRestart(ctx context.Context, container string, timeout int) error ContainerStatPath(ctx context.Context, container, path string) (types.ContainerPathStat, error) ContainerStats(ctx context.Context, container string, stream bool) (io.ReadCloser, error) - ContainerStart(ctx context.Context, container string) error + ContainerStart(ctx context.Context, container string, checkpointID string) error ContainerStop(ctx context.Context, container string, timeout int) error ContainerTop(ctx context.Context, container string, arguments []string) (types.ContainerProcessList, error) ContainerUnpause(ctx context.Context, container string) error diff --git a/types/client.go b/types/client.go index fa3b2cf..1b529a9 100644 --- a/types/client.go +++ b/types/client.go @@ -10,6 +10,12 @@ import ( "github.com/docker/go-units" ) +// CheckpointCreateOptions holds parameters to create a checkpoint from a container +type CheckpointCreateOptions struct { + CheckpointID string + Exit bool +} + // ContainerAttachOptions holds parameters to attach to a container. type ContainerAttachOptions struct { Stream bool diff --git a/types/types.go b/types/types.go index cb2dc9a..7994c11 100644 --- a/types/types.go +++ b/types/types.go @@ -471,3 +471,8 @@ type NetworkDisconnect struct { Container string Force bool } + +// Checkpoint represents the details of a checkpoint +type Checkpoint struct { + Name string // Name is the name of the checkpoint +}