Repository navigation
Expand file tree
/
Copy pathdeploy.go
More file actions
118 lines (102 loc) · 3.35 KB
/
Copy pathdeploy.go
File metadata and controls
118 lines (102 loc) · 3.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package bluecat
import (
"context"
"fmt"
"net/http"
"strings"
"time"
)
// bluecatDeployment is the subset of the Bluecat deployment resource we need.
type bluecatDeployment struct {
ID int64 `json:"id"`
Status string `json:"status"`
}
// Deployment statuses Bluecat reports. Compared case-insensitively, since the
// exact casing has varied across BAM versions.
var (
deploySucceeded = []string{"COMPLETE", "COMPLETED", "DONE", "SUCCESS", "SUCCEEDED", "FINISHED"}
deployFailed = []string{"FAILED", "ERROR", "CANCELLED", "CANCELED", "ABORTED"}
)
// DeployZone triggers a quick deployment of a zone and waits until Bluecat
// confirms it is complete, so callers don't begin DNS propagation checks
// before the records are actually live on the authoritative servers.
func (c *Client) DeployZone(ctx context.Context, zoneID int64) error {
path := fmt.Sprintf("/api/v2/zones/%d/deployments", zoneID)
var dep bluecatDeployment
var status int
// Deployment is not idempotent in the sense that a replay creates a
// second deployment, so don't retry it automatically.
policy := unsafeRetry()
err := c.do(ctx, apiRequest{
Method: http.MethodPost,
Path: path,
Body: map[string]string{"type": "QuickDeployment"},
Out: &dep,
Status: &status,
OK: []int{http.StatusCreated, http.StatusOK, http.StatusAccepted},
Retry: &policy,
})
if err != nil {
return fmt.Errorf("deploy zone %d: %w", zoneID, err)
}
// 200 and 201 mean the deployment already finished. Only 202 means it was
// queued and still needs to be waited on.
if status != http.StatusAccepted {
return nil
}
// Without a deployment ID there is nothing to poll. Wait one interval as
// a best-effort settle rather than returning immediately.
if dep.ID == 0 {
return c.sleep(ctx, c.deployPollInterval)
}
return c.waitForDeployment(ctx, dep.ID)
}
// waitForDeployment polls a deployment until it reaches a terminal status.
func (c *Client) waitForDeployment(ctx context.Context, deployID int64) error {
path := fmt.Sprintf("/api/v2/deployments/%d", deployID)
deadline := time.Now().Add(c.deployPollTimeout)
lastStatus := "unknown"
for {
if err := c.sleep(ctx, c.deployPollInterval); err != nil {
return err
}
var dep bluecatDeployment
if err := c.do(ctx, apiRequest{
Method: http.MethodGet,
Path: path,
Out: &dep,
}); err != nil {
// A transient failure mid-deployment shouldn't abort the wait,
// but a permanent one should.
if !IsRetryable(err) && !IsNotFound(err) {
return fmt.Errorf("poll deployment %d: %w", deployID, err)
}
if time.Now().After(deadline) {
return fmt.Errorf("timed out polling deployment %d (last error: %w)", deployID, err)
}
continue
}
if dep.Status != "" {
lastStatus = dep.Status
}
switch {
case matchesStatus(dep.Status, deploySucceeded):
return nil
case matchesStatus(dep.Status, deployFailed):
return fmt.Errorf("deployment %d finished with status %s", deployID, dep.Status)
}
// QUEUED, RUNNING, and anything we don't recognise: keep polling.
if time.Now().After(deadline) {
return fmt.Errorf("timed out after %s waiting for deployment %d to complete (last status %q)",
c.deployPollTimeout, deployID, lastStatus)
}
}
}
func matchesStatus(status string, want []string) bool {
for _, w := range want {
if strings.EqualFold(status, w) {
return true
}
}
return false
}