Repository: hetznercloud/hcloud-go
Pull request #64 (2018-01-22 08:39:32)

Add ActionClient.Watch to watch progress of actions


Patch:
diff --git a/hcloud/action.go b/hcloud/action.go
index 673d6b1..a18c63d 100644
--- a/hcloud/action.go
+++ b/hcloud/action.go
@@ -48,9 +48,22 @@ const (
 	ActionResourceTypeFloatingIP                    = "floating_ip"
 )
 
+// ActionError is the error of an action.
+type ActionError struct {
+	Code    string
+	Message string
+}
+
+func (e ActionError) Error() string {
+	return fmt.Sprintf("%s (%s)", e.Message, e.Code)
+}
+
 func (a *Action) Error() error {
 	if a.ErrorCode != "" && a.ErrorMessage != "" {
-		return fmt.Errorf("%s (%s)", a.ErrorMessage, a.ErrorCode)
+		return ActionError{
+			Code:    a.ErrorCode,
+			Message: a.ErrorMessage,
+		}
 	}
 	return nil
 }
@@ -125,3 +138,62 @@ func (c *ActionClient) All(ctx context.Context) ([]*Action, error) {
 
 	return allActions, nil
 }
+
+// WatchProgress watches the actions progress until it completes with success or error.
+func (c *ActionClient) WatchProgress(ctx context.Context, action *Action) (<-chan int, <-chan error) {
+	errCh := make(chan error, 1)
+	progressCh := make(chan int)
+
+	go func() {
+		defer close(errCh)
+		defer close(progressCh)
+
+		ticker := time.NewTicker(100 * time.Millisecond)
+		sendProgress := func(p int) {
+			select {
+			case progressCh <- p:
+				break
+			default:
+				break
+			}
+		}
+
+		var retries = 0
+		for {
+			select {
+			case <-ctx.Done():
+				errCh <- ctx.Err()
+				return
+			case <-ticker.C:
+				break
+			}
+
+			action, _, err := c.GetByID(ctx, action.ID)
+			if err != nil {
+				if err, ok := err.(Error); ok && err.Code == ErrorCodeLimitReached {
+					c.client.backoff(retries)
+					retries++
+					continue
+				}
+				errCh <- ctx.Err()
+				return
+			}
+			retries = 0
+
+			switch action.Status {
+			case ActionStatusRunning:
+				sendProgress(action.Progress)
+				break
+			case ActionStatusSuccess:
+				sendProgress(100)
+				errCh <- nil
+				return
+			case ActionStatusError:
+				errCh <- action.Error()
+				return
+			}
+		}
+	}()
+
+	return progressCh, errCh
+}
diff --git a/hcloud/client.go b/hcloud/client.go
index 220f9c4..8e47d9f 100644
--- a/hcloud/client.go
+++ b/hcloud/client.go
@@ -179,12 +179,10 @@ func (c *Client) all(f func(int) (*Response, error)) (*Response, error) {
 	for {
 		resp, err := f(page)
 		if err != nil {
-			if err, ok := err.(Error); ok {
-				if err.Code == ErrorCodeLimitReached {
-					c.backoff(retries)
-					retries++
-					continue
-				}
+			if err, ok := err.(Error); ok && err.Code == ErrorCodeLimitReached {
+				c.backoff(retries)
+				retries++
+				continue
 			}
 			return nil, err
 		}


Test patch:
diff --git a/hcloud/action_test.go b/hcloud/action_test.go
index 5475c68..9713b9d 100644
--- a/hcloud/action_test.go
+++ b/hcloud/action_test.go
@@ -134,3 +134,79 @@ func TestActionClientAll(t *testing.T) {
 		t.Errorf("unexpected actions")
 	}
 }
+
+func TestActionClientWatchProgress(t *testing.T) {
+	env := newTestEnv()
+	defer env.Teardown()
+
+	callCount := 0
+	env.Mux.HandleFunc("/actions/1", func(w http.ResponseWriter, r *http.Request) {
+		callCount++
+		w.Header().Set("Content-Type", "application/json")
+		switch callCount {
+		case 1:
+			json.NewEncoder(w).Encode(schema.ActionGetResponse{
+				Action: schema.Action{
+					ID:       1,
+					Status:   "running",
+					Progress: 50,
+				},
+			})
+		case 2:
+			w.WriteHeader(http.StatusTooManyRequests)
+			json.NewEncoder(w).Encode(schema.ErrorResponse{
+				Error: schema.Error{
+					Code:    "limit_reached",
+					Message: "ratelimited",
+				},
+			})
+			return
+		case 3:
+			json.NewEncoder(w).Encode(schema.ActionGetResponse{
+				Action: schema.Action{
+					ID:       1,
+					Status:   "error",
+					Progress: 100,
+					Error: &schema.ActionError{
+						Code:    "action_failed",
+						Message: "action failed",
+					},
+				},
+			})
+		default:
+			t.Errorf("unexpected number of calls to the test server: %v", callCount)
+		}
+	})
+	action := &Action{
+		ID:       1,
+		Status:   ActionStatusRunning,
+		Progress: 0,
+	}
+
+	ctx := context.Background()
+	progressCh, errCh := env.Client.Action.WatchProgress(ctx, action)
+	var (
+		progressUpdates []int
+		err             error
+	)
+
+loop:
+	for {
+		select {
+		case progress := <-progressCh:
+			progressUpdates = append(progressUpdates, progress)
+		case err = <-errCh:
+			break loop
+		}
+	}
+
+	if err == nil {
+		t.Fatal("expected an error")
+	}
+	if e, ok := err.(ActionError); !ok || e.Code != "action_failed" {
+		t.Fatalf("expected hcloud.Error, but got: %#v", err)
+	}
+	if len(progressUpdates) != 1 || progressUpdates[0] != 50 {
+		t.Fatalf("unexpected progress updates: %v", progressUpdates)
+	}
+}