| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 |
- package grapher
- import (
- "strconv"
- )
- // Relation describes the relationship between k8s components. Type is one of CostrolRel, LabelRel, AnnotationsRel, SpecRel.
- // 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 {
- Source int
- Target int
- }
- // ControlRel describes the relationship between a controller and its children pod.
- type ControlRel struct {
- Relation
- Replicas int
- Template map[string]interface{}
- }
- // LabelRel connects objects with spec.selector with pods that have corresponding metadata.labels.
- type LabelRel struct {
- Relation
- }
- // SpecRel connects objects via various spec properties.
- type SpecRel struct {
- Relation
- }
- // ParsedObjs has methods GetControlRel and GetLabelRel that updates its objects array.
- type ParsedObjs struct {
- Objects []Object
- PodSelectors []string
- }
- // Relations is embedded into the Object struct and contains arrays of the three types of relationships.
- type Relations struct {
- ControlRels []ControlRel
- LabelRels []LabelRel
- SpecRels []SpecRel
- }
- // MatchLabel is used to match Equality label selector.
- type MatchLabel struct {
- key string
- value string
- }
- // MatchExpression is used to match Set-based label selectors.
- type MatchExpression struct {
- key string
- operator string // In, NotIn, Exists, DoesNotExist are valid
- values []string
- }
- // =============== helpers for parsing relationships from YAML ===============
- // 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 (parsed *ParsedObjs) GetControlRel() {
- // First collect all children (Pods) that are not included in the yaml as top-level object.
- children := []Object{}
- selectors := []string{}
- for i, obj := range parsed.Objects {
- yaml := obj.RawYAML
- kind := getField(yaml, "kind")
- if kind == nil {
- kind = ""
- }
- switch kind.(string) {
- // Parse for all possible controller types
- case "Deployment", "StatefulSet", "ReplicaSet", "DaemonSet", "Job":
- rs := getField(yaml, "spec", "replicas")
- // replica defaults to 1 if unspecified
- if rs == nil {
- rs = 1
- }
- // Add Pods for controller objects
- template := getField(yaml, "spec", "template")
- if template == nil {
- continue
- }
- 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(j), // tentative name pre-deploy
- Namespace: obj.Namespace,
- RawYAML: template.(map[string]interface{}),
- Relations: Relations{
- ControlRels: []ControlRel{
- crel,
- },
- },
- }
- children = append(children, pod)
- obj.Relations.ControlRels = append(obj.Relations.ControlRels, crel)
- parsed.Objects[i] = obj
- }
- // Get pod label selectors
- matchLabels, _ := aggregateLabelSelectors(yaml)
- // stringify selectors
- selector := ""
- for i, ml := range matchLabels {
- selector = selector + ml.key + "=" + ml.value
- if i != len(matchLabels)-1 {
- selector = selector + ","
- }
- }
- selectors = appendIfNotDuplicate(selectors, selector)
- }
- }
- // add children to the objects array at the end.
- parsed.Objects = append(parsed.Objects, children...)
- parsed.PodSelectors = selectors
- }
- // GetLabelRel is generates relationships between objects connected by selector-label.
- // It supports both Equality-based and Set-based operators with MatchLabels and MatchExpressions, respectively.
- func (parsed *ParsedObjs) GetLabelRel() {
- for i, o := range parsed.Objects {
- // Skip Pods
- yaml := o.RawYAML
- matchLabels, matchExpressions := aggregateLabelSelectors(yaml)
- // Find ID's of targets that match the label selector
- targetID := parsed.findLabelsBySelector(o.ID, matchLabels, matchExpressions)
- lrels := o.Relations.LabelRels
- for _, tid := range targetID {
- newrel := LabelRel{
- Relation{
- Source: o.ID,
- Target: tid,
- },
- }
- lrels = append(lrels, newrel)
- }
- parsed.Objects[i].Relations.LabelRels = lrels
- }
- }
- // GetSpecRel draws relationships between two objects that are tied via various fields in their spec.
- func (parsed *ParsedObjs) GetSpecRel() {
- for i, o := range parsed.Objects {
- tid := []int{}
- switch o.Kind {
- case "ClusterRoleBinding", "RoleBinding":
- tid = parsed.findRBACTargets(o.ID, o.RawYAML)
- case "Ingress":
- rules := getField(o.RawYAML, "spec", "rules")
- if rules == nil {
- rules = []interface{}{}
- }
- for _, r := range rules.([]interface{}) {
- paths := getField(r.(map[string]interface{}), "http", "paths")
- if paths == nil {
- paths = []interface{}{}
- }
- for _, p := range paths.([]interface{}) {
- // service and resource are mutually exclusive backend types.
- name := getField(p.(map[string]interface{}), "backend", "serviceName")
- kind := "Service"
- if name == nil {
- name = getField(p.(map[string]interface{}), "backend", "service", "name")
- }
- if name == nil {
- name = getField(p.(map[string]interface{}), "backend", "resource", "name")
- kind = getField(p.(map[string]interface{}), "backend", "resource", "kind").(string)
- }
- tid = parsed.findObjectByNameAndKind(o.ID, name, kind)
- }
- }
- case "StatefulSet":
- serviceName := getField(o.RawYAML, "spec", "serviceName")
- tid = append(tid, parsed.findObjectByNameAndKind(o.ID, serviceName, "Service")...)
- case "Pod":
- volume := getField(o.RawYAML, "spec", "volumes")
- imageSecrets := getField(o.RawYAML, "spec", "ImagePullSecrets")
- serviceAccount := getField(o.RawYAML, "spec", "serviceAccountName")
- if imageSecrets == nil {
- imageSecrets = []interface{}{}
- }
- if volume == nil {
- volume = []interface{}{}
- }
- for _, sec := range imageSecrets.([]interface{}) {
- tid = append(tid, parsed.findObjectByNameAndKind(o.ID, sec, "Secret")...)
- }
- tid = append(tid, parsed.findObjectByNameAndKind(o.ID, serviceAccount, "ServiceAccount")...)
- for _, v := range volume.([]interface{}) {
- vt := v.(map[string]interface{})
- configMap := getField(vt, "configMap", "name")
- pvc := getField(vt, "persistentVolumeClaim", "claimName")
- secret := getField(vt, "secret", "secretName")
- tid = append(tid, parsed.findObjectByNameAndKind(o.ID, configMap, "ConfigMap")...)
- tid = append(tid, parsed.findObjectByNameAndKind(o.ID, pvc, "PersistentVolumeClaim")...)
- tid = append(tid, parsed.findObjectByNameAndKind(o.ID, secret, "Secret")...)
- }
- }
- // Add edges to parent
- rels := o.Relations.SpecRels
- for _, id := range tid {
- newrel := SpecRel{
- Relation{
- Source: o.ID,
- Target: id,
- },
- }
- rels = append(rels, newrel)
- }
- parsed.Objects[i].Relations.SpecRels = rels
- }
- }
- // ControlRel helpers
- func appendIfNotDuplicate(selectors []string, selector string) []string {
- for _, e := range selectors {
- if selector == e {
- return selectors
- }
- }
- selectors = append(selectors, selector)
- return selectors
- }
- // LabelRel helpers
- func aggregateLabelSelectors(yaml map[string]interface{}) ([]MatchLabel, []MatchExpression) {
- matchLabels := []MatchLabel{}
- matchExpressions := []MatchExpression{}
- // First check for the outdated syntax (matchLabels were added in recent k8s version)
- 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{}))
- }
- }
- return matchLabels, matchExpressions
- }
- // SpecRel helpers
- func (parsed *ParsedObjs) findObjectByNameAndKind(parentID int, name interface{}, kind string) []int {
- targets := []int{}
- if name == nil {
- return targets
- }
- name = name.(string)
- for i, o := range parsed.Objects {
- newrel := SpecRel{
- Relation{
- Source: parentID,
- Target: o.ID,
- },
- }
- if o.Name == name && o.Kind == kind {
- // Add bidirectional link from children as well.
- parsed.Objects[i].Relations.SpecRels = append(parsed.Objects[i].Relations.SpecRels, newrel)
- targets = append(targets, o.ID)
- return targets
- }
- }
- return targets
- }
- func (parsed *ParsedObjs) findRBACTargets(parentID int, yaml map[string]interface{}) []int {
- roleRef := getField(yaml, "roleRef")
- subjects := getField(yaml, "subjects")
- rules := append(subjects.([]interface{}), roleRef)
- targets := []int{}
- for i, o := range parsed.Objects {
- for _, r := range rules {
- tr := r.(map[string]interface{})
- newrel := SpecRel{
- Relation{
- Source: parentID,
- Target: o.ID,
- },
- }
- // first consider case of targets added via subjects, which are namespace scoped.
- if tr["namespace"] != nil && o.Kind == tr["kind"] && o.Name == tr["name"] &&
- (o.Namespace == tr["namespace"] || o.Namespace == "default") {
- // Add bidirectional link from children as well.
- parsed.Objects[i].Relations.SpecRels = append(parsed.Objects[i].Relations.SpecRels, newrel)
- targets = append(targets, o.ID)
- } else if tr["namespace"] == nil && o.Kind == tr["kind"] && o.Name == tr["name"] {
- parsed.Objects[i].Relations.SpecRels = append(parsed.Objects[i].Relations.SpecRels, newrel)
- targets = append(targets, o.ID)
- }
- }
- }
- return targets
- }
- 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
- }
- // TODO: Implement MatchExpression for set based operations.
- func (parsed *ParsedObjs) findLabelsBySelector(parentID int, ml []MatchLabel, me []MatchExpression) []int {
- matchedObjs := []int{}
- for i, o := range parsed.Objects {
- // Only Pods can be selected by spec.selector
- if o.Kind != "Pod" {
- continue
- }
- // find Pods 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++
- }
- }
- // Returns only if labels meet all conditions of the selector.
- if match == len(ml) && match > 0 {
- newrel := LabelRel{
- Relation{
- Source: parentID,
- Target: o.ID,
- },
- }
- // Add bidirectional link from children as well.
- parsed.Objects[i].Relations.LabelRels = append(parsed.Objects[i].Relations.LabelRels, newrel)
- matchedObjs = append(matchedObjs, o.ID)
- }
- }
- return matchedObjs
- }
|