package rollouts import ( "encoding/json" "fmt" "reflect" "strings" ) type RolloutState string const ( RolloutPending RolloutState = "pending" RolloutInProgress RolloutState = "in_progress" RolloutComplete RolloutState = "complete" RolloutError RolloutState = "error" ) // NodeKey is a key to look up a node within a graph. It must be unique within a graph type NodeKey string // NodeExecutionResult TODO(dk185217): can be extended to a struct type later if necessary // type NodeExecutionResult bool type NodeExecutionResult struct { Key NodeKey State NodeState Message string // TODO(dk185217): this signals the status of the overall rollout // - possibly move somewhere else. here or at a higher level? RolloutState RolloutState } // Done is ... TODO(dk185217): can be extended to a struct type later if necessary func (r NodeExecutionResult) Done() bool { switch r.State { case Complete: return true default: return false } } func (r NodeExecutionResult) String() string { return r.Message } // RolloutGraphEdge represents an edge in the graph. A graph is defined by a list of its edges type RolloutGraphEdge struct { From NodeKey To NodeKey } type NodeMap map[NodeKey]RolloutGraphNode func (nm NodeMap) AddNode(node RolloutGraphNode) { nm[node.GetKey()] = node } // RolloutGraph is an instance of a rollout plan type RolloutGraph struct { // TODO(dk185217): consider un-exporting these fields since we dont directly unmarshal into them ID string Nodes NodeMap Edges []RolloutGraphEdge Current []NodeKey NodeExecutionResults map[NodeKey]NodeExecutionResult opts *options } // RolloutGraphJSON is the intermediate format used to implement custom json unmarshaling type RolloutGraphJSON struct { ID string `json:"id,omitempty"` Current []NodeKey `json:"current"` Nodes []BaseNodeJSON `json:"nodes"` Edges []RolloutGraphEdge `json:"edges"` NodeExecutionResults map[NodeKey]NodeExecutionResult `json:"nodeExecutionResults"` GraphState string `json:"graph_state,omitempty"` } func NewRolloutGraphFromJSON(data json.RawMessage, opts ...Option) (*RolloutGraph, error) { graph := &RolloutGraph{ opts: makeOptions(opts...), } err := json.Unmarshal(data, graph) if err != nil { return nil, err } return graph, nil } // RolloutGraph ... func NewRolloutGraph(plan RolloutPlan, opts ...Option) *RolloutGraph { // TODO(https://github.com/ncr-swt-retail/edge-roadmap/issues/10936): Parameter 'plan' is unused. // This is where we get the GraphConfiguration to create a graph for the first time. Ie, unmarshal // the plan.GraphConfiguration into a RolloutGraph and initialize its state: // - when first created, all nodes should be Pending, and the current node will be a StartNode // - there will be no NodeExecutionResults yet, because nothing has been executed // But, now it is a valid RolloutGraph that can be Marshaled/Unmarshaled at will options := makeOptions(opts...) g := &RolloutGraph{ Current: plan.Initial, NodeExecutionResults: map[NodeKey]NodeExecutionResult{}, } for _, node := range plan.Nodes { // Initialize node states since plans are stateless node.initState() // Set per node graph initialization values switch n := node.(type) { case *TimerGate: if options.timeSource != nil { n.time = options.timeSource } else { n.time = defaultTimeSource{} } } } g.Nodes = plan.Nodes g.Edges = plan.Edges return g } func (g *RolloutGraph) String() string { if len(g.Current) == 0 { return "{ Current: [] }" } currents := []string{} for _, currKey := range g.Current { curr := g.Nodes[currKey] currents = append(currents, fmt.Sprintf("%s (%s)", curr.GetLabel(), curr.GetKey())) } currentsStr := strings.Join(currents, ",") // TODO: Expand - Printf calls are going to look empty return fmt.Sprintf("{ Current: [%v] }", currentsStr) } // UnmarshalJSON is ... func (g *RolloutGraph) UnmarshalJSON(data []byte) error { // Unmarshal raw json payload into intermediate type rolloutJSON := &RolloutGraphJSON{} err := json.Unmarshal(data, rolloutJSON) if err != nil { return fmt.Errorf("failed to unmarshal rollout graph payload: %v", err) } // ID g.ID = rolloutJSON.ID // Nodes. Unmarshal nodes into concrete types g.Nodes = make(map[NodeKey]RolloutGraphNode, len(rolloutJSON.Nodes)) for _, n := range rolloutJSON.Nodes { node, err := g.UnmarshalRolloutGraphNode(n) if err != nil { return err } g.Nodes[node.GetKey()] = node } g.Edges = make([]RolloutGraphEdge, len(rolloutJSON.Edges)) for i, edge := range rolloutJSON.Edges { // For each edge, create links between the respective nodes g.Edges[i] = RolloutGraphEdge{To: edge.To, From: edge.From} toNode, found := g.Nodes[edge.To] if !found { return fmt.Errorf("node key %s was stated in edge but not in nodes", edge.To) } fromNode, found := g.Nodes[edge.From] if !found { return fmt.Errorf("node key %s was stated in edge but not in nodes", edge.From) } toNode.AddDependency(fromNode) fromNode.AddNext(toNode) } // Current. Rote copying g.Current = rolloutJSON.Current // ExecutionResults. Rote copying g.NodeExecutionResults = make(map[NodeKey]NodeExecutionResult, len(rolloutJSON.NodeExecutionResults)) for key, result := range rolloutJSON.NodeExecutionResults { g.NodeExecutionResults[key] = result } return nil } // UnmarshalRolloutGraphNode unmarshals JSON into nodes. Some defaults are set // per node type to accommodate if the node comes from a Plan's JSON func (g *RolloutGraph) UnmarshalRolloutGraphNode(n BaseNodeJSON) (RolloutGraphNode, error) { switch n.NodeType { case "TargetGroup": tg := &TargetGroup{} if err := json.Unmarshal(n.Data, tg); err != nil { return nil, fmt.Errorf("failed to unmarshal node: %v", err) } return tg, nil case "TimerGate": tg := &TimerGate{} if err := json.Unmarshal(n.Data, tg); err != nil { return nil, fmt.Errorf("failed to unmarshal node: %v", err) } tg.time = g.opts.timeSource return tg, nil case "ApprovalGate": ag := &ApprovalGate{} if err := json.Unmarshal(n.Data, ag); err != nil { return nil, fmt.Errorf("failed to unmarshal node: %v", err) } return ag, nil case "ScheduleGate": sg := &ScheduleGate{} if err := json.Unmarshal(n.Data, sg); err != nil { return nil, fmt.Errorf("failed to unmarshal node: %v", err) } sg.time = g.opts.timeSource return sg, nil default: return nil, fmt.Errorf("failed to unmarshal node, unknown NodeType: %s", n.NodeType) } } func (g *RolloutGraph) MarshalJSON() ([]byte, error) { // Current. Copy all current node keys current := make([]NodeKey, len(g.Current)) for i, currKey := range g.Current { curr := g.Nodes[currKey] current[i] = curr.GetKey() } // Nodes. Convert all nodes to serializable types nodes := make([]BaseNodeJSON, len(g.Nodes)) i := 0 for _, node := range g.Nodes { data, err := json.Marshal(node) if err != nil { return nil, fmt.Errorf("failed to marshal node: %v", err) } baseNodeJSON := BaseNodeJSON{ // Seems to be necessary for pointer types, eg *TargetGroup -> TargetGroup. If this is a hassle to maintain // or causes issues, a GetType func could be added to the node interface and implemented by each node type NodeType: reflect.Indirect(reflect.ValueOf(node)).Type().Name(), Data: data, } nodes[i] = baseNodeJSON i++ } // Edges. Rote copying edges := make([]RolloutGraphEdge, len(g.Edges)) copy(edges, g.Edges) // ExecutionResults. Rote copying results := map[NodeKey]NodeExecutionResult{} for key, result := range g.NodeExecutionResults { results[key] = result } jsonData := &RolloutGraphJSON{ Current: current, Nodes: nodes, NodeExecutionResults: results, Edges: edges, } return json.Marshal(jsonData) } func ConnectEdge(from RolloutGraphNode, to RolloutGraphNode, edges []RolloutGraphEdge) []RolloutGraphEdge { from.AddNext(to) to.AddDependency(from) edge := RolloutGraphEdge{From: from.GetKey(), To: to.GetKey()} edges = append(edges, edge) return edges }