1 package rollouts
2
3 import (
4 "encoding/json"
5 "fmt"
6 "reflect"
7 "strings"
8 )
9
10 type RolloutState string
11
12 const (
13 RolloutPending RolloutState = "pending"
14 RolloutInProgress RolloutState = "in_progress"
15 RolloutComplete RolloutState = "complete"
16 RolloutError RolloutState = "error"
17 )
18
19
20 type NodeKey string
21
22
23
24
25 type NodeExecutionResult struct {
26 Key NodeKey
27 State NodeState
28 Message string
29
30
31 RolloutState RolloutState
32 }
33
34
35 func (r NodeExecutionResult) Done() bool {
36 switch r.State {
37 case Complete:
38 return true
39 default:
40 return false
41 }
42 }
43
44 func (r NodeExecutionResult) String() string {
45 return r.Message
46 }
47
48
49 type RolloutGraphEdge struct {
50 From NodeKey
51 To NodeKey
52 }
53
54 type NodeMap map[NodeKey]RolloutGraphNode
55
56 func (nm NodeMap) AddNode(node RolloutGraphNode) {
57 nm[node.GetKey()] = node
58 }
59
60
61 type RolloutGraph struct {
62
63 ID string
64 Nodes NodeMap
65 Edges []RolloutGraphEdge
66 Current []NodeKey
67 NodeExecutionResults map[NodeKey]NodeExecutionResult
68
69 opts *options
70 }
71
72
73 type RolloutGraphJSON struct {
74 ID string `json:"id,omitempty"`
75 Current []NodeKey `json:"current"`
76 Nodes []BaseNodeJSON `json:"nodes"`
77 Edges []RolloutGraphEdge `json:"edges"`
78 NodeExecutionResults map[NodeKey]NodeExecutionResult `json:"nodeExecutionResults"`
79 GraphState string `json:"graph_state,omitempty"`
80 }
81
82 func NewRolloutGraphFromJSON(data json.RawMessage, opts ...Option) (*RolloutGraph, error) {
83 graph := &RolloutGraph{
84 opts: makeOptions(opts...),
85 }
86 err := json.Unmarshal(data, graph)
87 if err != nil {
88 return nil, err
89 }
90 return graph, nil
91 }
92
93
94 func NewRolloutGraph(plan RolloutPlan, opts ...Option) *RolloutGraph {
95
96
97
98
99
100
101 options := makeOptions(opts...)
102
103 g := &RolloutGraph{
104 Current: plan.Initial,
105 NodeExecutionResults: map[NodeKey]NodeExecutionResult{},
106 }
107
108 for _, node := range plan.Nodes {
109
110 node.initState()
111
112
113 switch n := node.(type) {
114 case *TimerGate:
115 if options.timeSource != nil {
116 n.time = options.timeSource
117 } else {
118 n.time = defaultTimeSource{}
119 }
120 }
121 }
122
123 g.Nodes = plan.Nodes
124 g.Edges = plan.Edges
125
126 return g
127 }
128
129 func (g *RolloutGraph) String() string {
130 if len(g.Current) == 0 {
131 return "{ Current: [] }"
132 }
133 currents := []string{}
134 for _, currKey := range g.Current {
135 curr := g.Nodes[currKey]
136 currents = append(currents, fmt.Sprintf("%s (%s)", curr.GetLabel(), curr.GetKey()))
137 }
138 currentsStr := strings.Join(currents, ",")
139
140 return fmt.Sprintf("{ Current: [%v] }", currentsStr)
141 }
142
143
144 func (g *RolloutGraph) UnmarshalJSON(data []byte) error {
145
146 rolloutJSON := &RolloutGraphJSON{}
147 err := json.Unmarshal(data, rolloutJSON)
148 if err != nil {
149 return fmt.Errorf("failed to unmarshal rollout graph payload: %v", err)
150 }
151
152
153 g.ID = rolloutJSON.ID
154
155
156 g.Nodes = make(map[NodeKey]RolloutGraphNode, len(rolloutJSON.Nodes))
157 for _, n := range rolloutJSON.Nodes {
158 node, err := g.UnmarshalRolloutGraphNode(n)
159 if err != nil {
160 return err
161 }
162 g.Nodes[node.GetKey()] = node
163 }
164
165 g.Edges = make([]RolloutGraphEdge, len(rolloutJSON.Edges))
166 for i, edge := range rolloutJSON.Edges {
167
168 g.Edges[i] = RolloutGraphEdge{To: edge.To, From: edge.From}
169 toNode, found := g.Nodes[edge.To]
170 if !found {
171 return fmt.Errorf("node key %s was stated in edge but not in nodes", edge.To)
172 }
173 fromNode, found := g.Nodes[edge.From]
174 if !found {
175 return fmt.Errorf("node key %s was stated in edge but not in nodes", edge.From)
176 }
177 toNode.AddDependency(fromNode)
178 fromNode.AddNext(toNode)
179 }
180
181
182 g.Current = rolloutJSON.Current
183
184
185 g.NodeExecutionResults = make(map[NodeKey]NodeExecutionResult, len(rolloutJSON.NodeExecutionResults))
186 for key, result := range rolloutJSON.NodeExecutionResults {
187 g.NodeExecutionResults[key] = result
188 }
189
190 return nil
191 }
192
193
194
195 func (g *RolloutGraph) UnmarshalRolloutGraphNode(n BaseNodeJSON) (RolloutGraphNode, error) {
196 switch n.NodeType {
197 case "TargetGroup":
198 tg := &TargetGroup{}
199 if err := json.Unmarshal(n.Data, tg); err != nil {
200 return nil, fmt.Errorf("failed to unmarshal node: %v", err)
201 }
202 return tg, nil
203 case "TimerGate":
204 tg := &TimerGate{}
205 if err := json.Unmarshal(n.Data, tg); err != nil {
206 return nil, fmt.Errorf("failed to unmarshal node: %v", err)
207 }
208 tg.time = g.opts.timeSource
209 return tg, nil
210 case "ApprovalGate":
211 ag := &ApprovalGate{}
212 if err := json.Unmarshal(n.Data, ag); err != nil {
213 return nil, fmt.Errorf("failed to unmarshal node: %v", err)
214 }
215 return ag, nil
216 case "ScheduleGate":
217 sg := &ScheduleGate{}
218 if err := json.Unmarshal(n.Data, sg); err != nil {
219 return nil, fmt.Errorf("failed to unmarshal node: %v", err)
220 }
221 sg.time = g.opts.timeSource
222 return sg, nil
223 default:
224 return nil, fmt.Errorf("failed to unmarshal node, unknown NodeType: %s", n.NodeType)
225 }
226 }
227
228 func (g *RolloutGraph) MarshalJSON() ([]byte, error) {
229
230 current := make([]NodeKey, len(g.Current))
231 for i, currKey := range g.Current {
232 curr := g.Nodes[currKey]
233 current[i] = curr.GetKey()
234 }
235
236
237 nodes := make([]BaseNodeJSON, len(g.Nodes))
238 i := 0
239 for _, node := range g.Nodes {
240 data, err := json.Marshal(node)
241 if err != nil {
242 return nil, fmt.Errorf("failed to marshal node: %v", err)
243 }
244 baseNodeJSON := BaseNodeJSON{
245
246
247 NodeType: reflect.Indirect(reflect.ValueOf(node)).Type().Name(),
248 Data: data,
249 }
250 nodes[i] = baseNodeJSON
251 i++
252 }
253
254
255 edges := make([]RolloutGraphEdge, len(g.Edges))
256 copy(edges, g.Edges)
257
258
259 results := map[NodeKey]NodeExecutionResult{}
260 for key, result := range g.NodeExecutionResults {
261 results[key] = result
262 }
263
264 jsonData := &RolloutGraphJSON{
265 Current: current,
266 Nodes: nodes,
267 NodeExecutionResults: results,
268 Edges: edges,
269 }
270
271 return json.Marshal(jsonData)
272 }
273
274 func ConnectEdge(from RolloutGraphNode, to RolloutGraphNode, edges []RolloutGraphEdge) []RolloutGraphEdge {
275 from.AddNext(to)
276 to.AddDependency(from)
277 edge := RolloutGraphEdge{From: from.GetKey(), To: to.GetKey()}
278 edges = append(edges, edge)
279 return edges
280 }
281
View as plain text