Przeglądaj źródła

control rel refactor & label rel

sunguroku 5 lat temu
rodzic
commit
04234d3f6d

+ 19 - 38
internal/helm/grapher/object.go

@@ -1,54 +1,35 @@
 package grapher
 
-import (
-	"encoding/json"
-	"strconv"
-)
-
 // Object contains information about each k8s component in the chart.
 type Object struct {
+	ID        int
 	Kind      string
 	Name      string
-	RawYAML   string
-	Relations []Relation
+	RawYAML   map[string]interface{}
+	Relations Relations
 }
 
-// ParseObj parses a k8s object from a single-document yaml
+// ParseObjs parses a k8s object from a single-document yaml
 // and returns an array of objects that includes its children.
-func ParseObj(obj map[string]interface{}) []Object {
-	kind := getField(obj, "kind").(string)
-	name := getField(obj, "metadata", "name").(string)
-	js, _ := json.Marshal(obj)
-
-	// First add the object that appears on the YAML
-	parent := Object{
-		Kind:      kind,
-		Name:      name,
-		RawYAML:   string(js),
-		Relations: make([]Relation, 0),
-	}
+func ParseObjs(objs []map[string]interface{}) []Object {
 	objArr := []Object{}
-	objArr = append(objArr, parent)
 
-	switch kind {
-	case "Deployment", "StatefulSet", "ReplicaSet", "DaemonSet", "Job":
-
-		rs := getField(obj, "spec", "replicas")
-		if rs == nil {
-			rs = 0
-		}
+	for i, obj := range objs {
+		kind := getField(obj, "kind").(string)
+		name := getField(obj, "metadata", "name").(string)
 
-		// Add Pods for controller objects
-		template, _ := json.Marshal(getField(obj, "spec", "template"))
-		for i := 0; i < rs.(int); i++ {
-			pod := Object{
-				Kind:      "Pod",
-				Name:      name + "-" + strconv.Itoa(i+1),
-				RawYAML:   string(template),
-				Relations: make([]Relation, 0),
-			}
-			objArr = append(objArr, pod)
+		// First add the object that appears on the YAML
+		parsedObj := Object{
+			ID:      i,
+			Kind:    kind,
+			Name:    name,
+			RawYAML: obj,
+			Relations: Relations{
+				ControlRels: []ControlRel{},
+				LabelRels:   []LabelRel{},
+			},
 		}
+		objArr = append(objArr, parsedObj)
 	}
 	return objArr
 }

+ 28 - 69
internal/helm/grapher/object_test.go

@@ -9,109 +9,73 @@ import (
 
 // Expected objects for helm Cassandra chart
 var c1 = grapher.Object{
-	Kind:      "Secret",
-	Name:      "my-release-cassandra",
-	Relations: make([]grapher.Relation, 0),
+	Kind: "Secret",
+	Name: "my-release-cassandra",
 }
 
 var c2 = grapher.Object{
-	Kind:      "Service",
-	Name:      "my-release-cassandra-headless",
-	Relations: make([]grapher.Relation, 0),
+	Kind: "Service",
+	Name: "my-release-cassandra-headless",
 }
 
 var c3 = grapher.Object{
-	Kind:      "Service",
-	Name:      "my-release-cassandra",
-	Relations: make([]grapher.Relation, 0),
+	Kind: "Service",
+	Name: "my-release-cassandra",
 }
 
 var c4 = grapher.Object{
-	Kind:      "StatefulSet",
-	Name:      "my-release-cassandra",
-	Relations: make([]grapher.Relation, 0),
-}
-
-var c5 = grapher.Object{
-	Kind:      "Pod",
-	Name:      "my-release-cassandra-1",
-	Relations: make([]grapher.Relation, 0),
-}
-
-var c6 = grapher.Object{
-	Kind:      "Pod",
-	Name:      "my-release-cassandra-2",
-	Relations: make([]grapher.Relation, 0),
+	Kind: "StatefulSet",
+	Name: "my-release-cassandra",
 }
 
 // Expected objects for helm Cassandra chart
 var k1 = grapher.Object{
-	Kind:      "ServiceAccount",
-	Name:      "my-release-kafka",
-	Relations: make([]grapher.Relation, 0),
+	Kind: "ServiceAccount",
+	Name: "my-release-kafka",
 }
 
 var k2 = grapher.Object{
-	Kind:      "ConfigMap",
-	Name:      "my-release-kafka-scripts",
-	Relations: make([]grapher.Relation, 0),
+	Kind: "ConfigMap",
+	Name: "my-release-kafka-scripts",
 }
 
 var k3 = grapher.Object{
-	Kind:      "Service",
-	Name:      "my-release-zookeeper-headless",
-	Relations: make([]grapher.Relation, 0),
+	Kind: "Service",
+	Name: "my-release-zookeeper-headless",
 }
 
 var k4 = grapher.Object{
-	Kind:      "Service",
-	Name:      "my-release-zookeeper",
-	Relations: make([]grapher.Relation, 0),
+	Kind: "Service",
+	Name: "my-release-zookeeper",
 }
 
 var k5 = grapher.Object{
-	Kind:      "Service",
-	Name:      "my-release-kafka-headless",
-	Relations: make([]grapher.Relation, 0),
+	Kind: "Service",
+	Name: "my-release-kafka-headless",
 }
 
 var k6 = grapher.Object{
-	Kind:      "Service",
-	Name:      "my-release-kafka",
-	Relations: make([]grapher.Relation, 0),
+	Kind: "Service",
+	Name: "my-release-kafka",
 }
 
 var k7 = grapher.Object{
-	Kind:      "StatefulSet",
-	Name:      "my-release-zookeeper",
-	Relations: make([]grapher.Relation, 0),
+	Kind: "StatefulSet",
+	Name: "my-release-zookeeper",
 }
 
 var k8 = grapher.Object{
-	Kind:      "Pod",
-	Name:      "my-release-zookeeper-1",
-	Relations: make([]grapher.Relation, 0),
-}
-
-var k9 = grapher.Object{
-	Kind:      "StatefulSet",
-	Name:      "my-release-kafka",
-	Relations: make([]grapher.Relation, 0),
-}
-
-var k10 = grapher.Object{
-	Kind:      "Pod",
-	Name:      "my-release-kafka-1",
-	Relations: make([]grapher.Relation, 0),
+	Kind: "StatefulSet",
+	Name: "my-release-kafka",
 }
 
 var expObjs1 = []grapher.Object{
-	c1, c2, c3, c4, c5, c6,
+	c1, c2, c3, c4,
 }
 
 var expObjs2 = []grapher.Object{
-	k1, k2, k3, k4, k5,
-	k6, k7, k8, k9, k10,
+	k1, k2, k3, k4,
+	k5, k6, k7, k8,
 }
 
 type k8sObj struct {
@@ -140,12 +104,7 @@ func TestParseObj(t *testing.T) {
 		}
 
 		yamlArr := grapher.ImportMultiDocYAML(file)
-		objects := []grapher.Object{}
-
-		for _, y := range yamlArr {
-			strmap := grapher.ConvertYAMLToStringKeys(y)
-			objects = append(objects, grapher.ParseObj(strmap)...)
-		}
+		objects := grapher.ParseObjs(yamlArr)
 
 		for i, o := range objects {
 			if k8sObj.Expected[i].Kind != o.Kind {

+ 3 - 9
internal/helm/grapher/parser.go

@@ -9,7 +9,7 @@ import (
 
 // ImportMultiDocYAML is a helper function that goes through a yaml file with multiple documents (or objects)
 // separated by '---' or '...' and returns an array of yamls.
-func ImportMultiDocYAML(source []byte) (arr []map[interface{}]interface{}) {
+func ImportMultiDocYAML(source []byte) (arr []map[string]interface{}) {
 	dec := yaml.NewDecoder(bytes.NewReader(source))
 
 	for {
@@ -17,15 +17,9 @@ func ImportMultiDocYAML(source []byte) (arr []map[interface{}]interface{}) {
 		if dec.Decode(&doc) != nil {
 			return arr
 		}
-		arr = append(arr, doc)
+		strmap := recursiveConv(doc).(map[string]interface{})
+		arr = append(arr, strmap)
 	}
-
-}
-
-// ConvertYAMLToStringKeys enforces all non-string keys in a map to be string keys.
-func ConvertYAMLToStringKeys(yaml map[interface{}]interface{}) map[string]interface{} {
-	strmap := recursiveConv(yaml).(map[string]interface{})
-	return strmap
 }
 
 // recursive helper function that type asserts each layer of nested interfaces to

+ 154 - 25
internal/helm/grapher/relation.go

@@ -1,21 +1,46 @@
 package grapher
 
-import "fmt"
+import (
+	"strconv"
+)
 
 // Relation describes the relationship between k8s components. Type is one of CostrolRel, LabelRel, AnnotationsRel, SpecRel.
-// Source and Target contains the name of the k8s component that is either the giver or recipient of a relationship.
+// Source and Target contains the ID of the k8s component that is either the giver or recipient of a relationship.
 // All relations are bi-directional in that each object contains both the incoming and outbound relationships.
 type Relation struct {
-	Type   string
-	Source string
-	Target string
+	Source int
+	Target int
 }
 
 // ControlRel describes the relationship between a controller and its children pod.
 type ControlRel struct {
 	Relation
 	Replicas int
-	Template string
+	Template map[string]interface{}
+}
+
+type LabelRel struct {
+	Relation
+}
+
+type ParsedObjs struct {
+	Objects []Object
+}
+
+type Relations struct {
+	ControlRels []ControlRel
+	LabelRels   []LabelRel
+}
+
+type MatchLabel struct {
+	key   string
+	value string
+}
+
+type MatchExpression struct {
+	key      string
+	operator string // In, NotIn, Exists, DoesNotExist are valid
+	values   []string
 }
 
 // =============== helpers for parsing relationships from YAML ===============
@@ -23,28 +48,132 @@ type ControlRel struct {
 // GetControlRel generates relationships and children objects for common k8s controller types.
 // Note that this only includes controllers whose children are 1) pods and 2) do not have its own YAML.
 // i.e. Children relies entirely on the parent's template. Controllers like CronJob are excluded because its children are not pods.
-func GetControlRel(yaml map[string]interface{}) *ControlRel {
-	switch kind := getField(yaml, "kind").(string); kind {
-	// Parse for all possible controller types
-	case "Deployment", "StatefulSet", "ReplicaSet", "DaemonSet", "Job":
-		rs := getField(yaml, "spec", "replicas")
-		if rs == nil {
-			rs = 0
+func (parsed *ParsedObjs) GetControlRel() {
+	// First collect all children.
+	children := []Object{}
+	for i, obj := range parsed.Objects {
+		yaml := obj.RawYAML
+
+		switch kind := getField(yaml, "kind").(string); kind {
+		// Parse for all possible controller types
+		case "Deployment", "StatefulSet", "ReplicaSet", "DaemonSet", "Job":
+			rs := getField(yaml, "spec", "replicas")
+
+			if rs != nil && rs.(int) > 0 {
+				// Add Pods for controller objects
+				template := getField(yaml, "spec", "template").(map[string]interface{})
+				for j := 0; j < rs.(int); j++ {
+					cid := len(parsed.Objects) + len(children)
+					crel := ControlRel{
+						Relation: Relation{
+							Source: obj.ID,
+							Target: cid,
+						},
+						Replicas: rs.(int),
+					}
+
+					pod := Object{
+						ID:      cid,
+						Kind:    "Pod",
+						Name:    obj.Name + "-" + strconv.Itoa(i), // tentative name pre-deploy
+						RawYAML: template,
+						Relations: Relations{
+							ControlRels: []ControlRel{
+								crel,
+							},
+						},
+					}
+
+					children = append(children, pod)
+					obj.Relations.ControlRels = append(obj.Relations.ControlRels, crel)
+					parsed.Objects[i] = obj
+				}
+			}
 		}
+	}
 
-		template := fmt.Sprint(getField(yaml, "spec", "template"))
-		crel := &ControlRel{
-			Relation: Relation{
-				Type:   "ControlRel",
-				Source: getField(yaml, "metadata", "name").(string),
-				Target: "temp",
-			},
-			Replicas: rs.(int),
-			Template: template,
+	// add children to the objects array
+	parsed.Objects = append(parsed.Objects, children...)
+}
+
+// GetLabelRel is sdflk
+func (parsed *ParsedObjs) GetLabelRel() {
+	for i, o := range parsed.Objects {
+		yaml := o.RawYAML
+		matchLabels := []MatchLabel{}
+		matchExpressions := []MatchExpression{}
+
+		if l := getField(yaml, "spec", "selector"); l != nil {
+			simple := true
+			if ml := getField(yaml, "spec", "selector", "matchLabels"); ml != nil {
+				matchLabels = addMatchLabels(matchLabels, ml.(map[string]interface{}))
+				simple = false
+			}
+
+			if me := getField(yaml, "spec", "selector", "matchExpressions"); me != nil {
+				for _, o := range me.([]interface{}) {
+					ot := o.(map[string]interface{})
+					values := []string{}
+					for _, arg := range ot["values"].([]interface{}) {
+						values = append(values, arg.(string))
+					}
+					matchExpressions = append(matchExpressions, MatchExpression{
+						key:      ot["key"].(string),
+						operator: ot["operator"].(string),
+						values:   values,
+					})
+				}
+				simple = false
+			}
+
+			if simple {
+				matchLabels = addMatchLabels(matchLabels, l.(map[string]interface{}))
+			}
+		}
+		// fmt.Println("For object", o.Name)
+		// fmt.Println("matchLabels", matchLabels)
+		// fmt.Println("matchExp", matchExpressions)
+
+		targetID := parsed.findLabelsBySelector(matchLabels, matchExpressions)
+		lrels := o.Relations.LabelRels
+		for _, tid := range targetID {
+			newrel := LabelRel{
+				Relation{
+					Source: o.ID,
+					Target: tid,
+				},
+			}
+			lrels = append(lrels, newrel)
 		}
 
-		return crel
-	default:
-		return nil
+		parsed.Objects[i].Relations.LabelRels = lrels
+	}
+}
+
+func addMatchLabels(matchLabels []MatchLabel, ml map[string]interface{}) []MatchLabel {
+	for k, v := range ml {
+		matchLabels = append(matchLabels, MatchLabel{
+			key:   k,
+			value: v.(string),
+		})
+	}
+	return matchLabels
+}
+
+func (parsed *ParsedObjs) findLabelsBySelector(ml []MatchLabel, me []MatchExpression) []int {
+	matchedObjs := []int{}
+	for _, o := range parsed.Objects {
+		// find objects that match labels
+		labels := getField(o.RawYAML, "metadata", "labels")
+		match := 0
+		for _, l := range ml {
+			if labels.(map[string]interface{})[l.key] == l.value {
+				match++
+			}
+		}
+		if match == len(ml) && match > 0 {
+			matchedObjs = append(matchedObjs, o.ID)
+		}
 	}
+	return matchedObjs
 }

+ 100 - 40
internal/helm/grapher/relation_test.go

@@ -7,38 +7,64 @@ import (
 	"github.com/porter-dev/porter/internal/helm/grapher"
 )
 
-var expControlRels1 = []grapher.ControlRel{
-	grapher.ControlRel{
-		Relation: grapher.Relation{
-			Type:   "ControlRel",
-			Source: "my-release-cassandra",
-			Target: "temp",
+var c7 = grapher.Object{
+	Kind: "StatefulSet",
+	Relations: grapher.Relations{
+		ControlRels: []grapher.ControlRel{
+			grapher.ControlRel{
+				Relation: grapher.Relation{
+					Source: 3,
+					Target: 4,
+				},
+				Replicas: 2,
+			},
+			grapher.ControlRel{
+				Relation: grapher.Relation{
+					Source: 3,
+					Target: 5,
+				},
+				Replicas: 2,
+			},
 		},
-		Replicas: 1,
 	},
 }
 
-var expControlRels2 = []grapher.ControlRel{
-	grapher.ControlRel{
-		Relation: grapher.Relation{
-			Type:   "ControlRel",
-			Source: "my-release-zookeeper",
-			Target: "temp",
+var c5 = grapher.Object{
+	Kind: "Pod",
+	Relations: grapher.Relations{
+		ControlRels: []grapher.ControlRel{
+			grapher.ControlRel{
+				Relation: grapher.Relation{
+					Source: 3,
+					Target: 4,
+				},
+				Replicas: 2,
+			},
 		},
-		Replicas: 1,
 	},
-	grapher.ControlRel{
-		Relation: grapher.Relation{
-			Type:   "ControlRel",
-			Source: "my-release-kafka",
-			Target: "temp",
+}
+
+var c6 = grapher.Object{
+	Kind: "Pod",
+	Relations: grapher.Relations{
+		ControlRels: []grapher.ControlRel{
+			grapher.ControlRel{
+				Relation: grapher.Relation{
+					Source: 3,
+					Target: 5,
+				},
+				Replicas: 2,
+			},
 		},
-		Replicas: 1,
 	},
 }
 
+var expControlRels1 = []grapher.Object{
+	c1, c2, c3, c7, c5, c6,
+}
+
 type test struct {
-	Expected []grapher.ControlRel
+	Expected []grapher.Object
 	FilePath string
 }
 
@@ -48,10 +74,6 @@ func TestControlRels(t *testing.T) {
 			Expected: expControlRels1,
 			FilePath: "./test_yaml/cassandra.yaml",
 		},
-		test{
-			Expected: expControlRels2,
-			FilePath: "./test_yaml/kafka.yaml",
-		},
 	}
 
 	for _, r := range ts {
@@ -63,27 +85,65 @@ func TestControlRels(t *testing.T) {
 		}
 
 		yamlArr := grapher.ImportMultiDocYAML(file)
-		rs := []*grapher.ControlRel{}
-
-		for _, y := range yamlArr {
-			strmap := grapher.ConvertYAMLToStringKeys(y)
-			if crel := grapher.GetControlRel(strmap); crel != nil {
-				rs = append(rs, crel)
-			}
+		objects := grapher.ParseObjs(yamlArr)
+		parsed := grapher.ParsedObjs{
+			Objects: objects,
 		}
 
-		for i, o := range rs {
-			if r.Expected[i].Replicas != o.Replicas {
-				t.Errorf("Number of Replicas are different at position %d. Expected %d, Got %d\n", i, r.Expected[i].Replicas, o.Replicas)
-			}
+		parsed.GetControlRel()
 
-			if r.Expected[i].Source != o.Source {
-				t.Errorf("Source names are different at position %d. Expected %s, Got %s\n", i, r.Expected[i].Source, o.Source)
+		for i, o := range parsed.Objects {
+			e := r.Expected[i]
+			if len(e.Relations.ControlRels) != len(o.Relations.ControlRels) {
+				t.Errorf("Number of ControlRel differs for %s of type %s. Expected %d. Got %d",
+					e.Name, e.Kind, len(e.Relations.ControlRels), len(o.Relations.ControlRels))
 			}
 
-			if r.Expected[i].Target != o.Target {
-				t.Errorf("Target names are different at position %d. Expected %s, Got %s\n", i, r.Expected[i].Target, o.Target)
+			for j, crel := range o.Relations.ControlRels {
+				expCrel := e.Relations.ControlRels[j]
+
+				if expCrel.Relation.Source != crel.Relation.Source {
+					t.Errorf("Source in ControlRel differs for %s of type %s. Expected %d. Got %d",
+						o.Name, o.Kind, expCrel.Relation.Source, crel.Relation.Source)
+				}
+
+				if expCrel.Relation.Target != crel.Relation.Target {
+					t.Errorf("Target in ControlRel differs for %s of type %s. Expected %d. Got %d",
+						o.Name, o.Kind, expCrel.Relation.Target, crel.Relation.Target)
+				}
+
+				if expCrel.Replicas != crel.Replicas {
+					t.Errorf("Number of replicas in ControlRel differs for %s of type %s. Expected %d. Got %d",
+						o.Name, o.Kind, expCrel.Replicas, crel.Replicas)
+				}
 			}
 		}
 	}
 }
+
+func TestLabelRels(t *testing.T) {
+	ts := []test{
+		test{
+			Expected: expControlRels1,
+			FilePath: "./test_yaml/cassandra.yaml",
+		},
+	}
+
+	for _, r := range ts {
+		// Load in yaml from test files
+		file, err := ioutil.ReadFile(r.FilePath)
+
+		if err != nil {
+			t.Errorf("Error reading file %s", r.FilePath)
+		}
+
+		yamlArr := grapher.ImportMultiDocYAML(file)
+		objects := grapher.ParseObjs(yamlArr)
+		parsed := grapher.ParsedObjs{
+			Objects: objects,
+		}
+
+		parsed.GetLabelRel()
+		t.Errorf("label")
+	}
+}

+ 3 - 0
internal/helm/grapher/test_yaml/cassandra.yaml

@@ -93,6 +93,9 @@ spec:
     matchLabels:
       app.kubernetes.io/name: cassandra
       app.kubernetes.io/instance: my-release
+    matchExpressions:
+      - {key: tier, operator: In, values: [cache]}
+      - {key: environment, operator: NotIn, values: [dev]}
   serviceName: my-release-cassandra-headless
   podManagementPolicy: OrderedReady
   replicas: 2