package rollouts import ( "encoding/json" "fmt" "reflect" ) // RolloutPlan is the configuration, or a "template" to create rollouts from. It is a named object, eg my-rollout-plan, that // can be re-used with different inputs. For example, there may be a rollout plan for upgrading the version of Edge on clusters // in phases, from dev to staging, then to production. That type of plan could be reused for upgrading workloads or O/S versions // as well type RolloutPlan struct { ID string Name string Description string Initial []NodeKey Nodes NodeMap Edges []RolloutGraphEdge Configuration json.RawMessage // GraphConfigration ... // make note about how it can be stored in sql in a jsonb // GraphConfiguration json.RawMessage } // RolloutPlanJSON is the intermediate format used to implement custom json unmarshaling type RolloutPlanJSON struct { ID string `json:"id,omitempty"` Name string `json:"name"` Description string `json:"description"` Initial []NodeKey `json:"initial"` Nodes []BaseNodeJSON `json:"nodes"` Edges []RolloutGraphEdge `json:"edges"` } func (p *RolloutPlan) UnmarshalJSON(data []byte) error { // Unmarshal raw json payload into intermediate type planJSON := &RolloutPlanJSON{} err := json.Unmarshal(data, planJSON) if err != nil { return fmt.Errorf("failed to unmarshal rollout graph payload: %v", err) } // ID p.ID = planJSON.ID // Name p.Name = planJSON.Name // Description p.Description = planJSON.Description // Nodes. Unmarshal nodes into concrete types p.Nodes = make(map[NodeKey]RolloutGraphNode, len(planJSON.Nodes)) for _, n := range planJSON.Nodes { node, err := UnmarshalRolloutGraphNode(n) if err != nil { return err } p.Nodes[node.GetKey()] = node } // Edges. Unmarshal and update node connections based on edges p.Edges = make([]RolloutGraphEdge, len(planJSON.Edges)) for i, edge := range planJSON.Edges { // For each edge, create links between the respective nodes p.Edges[i] = RolloutGraphEdge{To: edge.To, From: edge.From} toNode, found := p.Nodes[edge.To] if !found { return fmt.Errorf("node key %s was stated in edge but not in nodes", edge.To) } fromNode, found := p.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) } // Initial. Copy all current node keys p.Initial = planJSON.Initial return nil } func (p *RolloutPlan) MarshalJSON() ([]byte, error) { // Nodes. Convert all nodes to serializable types nodes := make([]BaseNodeJSON, len(p.Nodes)) i := 0 for _, node := range p.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(p.Edges)) copy(edges, p.Edges) jsonData := &RolloutGraphJSON{ Nodes: nodes, Edges: edges, } return json.Marshal(jsonData) } func NewRolloutPlanFromJSON(data json.RawMessage) (RolloutPlan, error) { plan := &RolloutPlan{} err := json.Unmarshal(data, plan) if err != nil { return *plan, err } return *plan, nil }