// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package controller import ( "fmt" "sort" "strings" "github.com/hashicorp/go-multierror" "golang.org/x/exp/maps" "github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/proto-public/pbresource" ) func (m *Manager) ValidateDependencies(registrations []resource.Registration) error { deps := m.CalculateDependencies(registrations) return deps.validate() } type Dependencies map[string][]string func (deps Dependencies) validate() error { var merr error seen := make(map[string]map[string]struct{}) mkErr := func(src, dst string) error { vals := []string{src, dst} sort.Strings(vals) return fmt.Errorf("circular dependency between %q and %q", vals[0], vals[1]) } for src, dsts := range deps { seenDsts := seen[src] if len(seenDsts) == 0 { seen[src] = make(map[string]struct{}) } for _, dst := range dsts { if _, ok := seenDsts[dst]; ok { merr = multierror.Append(merr, mkErr(src, dst)) } if inverseDsts := seen[dst]; len(inverseDsts) > 0 { if _, ok := inverseDsts[src]; ok { merr = multierror.Append(merr, mkErr(src, dst)) } } seen[src][dst] = struct{}{} } } return merr } func (m *Manager) CalculateDependencies(registrations []resource.Registration) Dependencies { typeToString := func(t *pbresource.Type) string { return strings.ToLower(fmt.Sprintf("%s/%s/%s", t.Group, t.GroupVersion, t.Kind)) } out := make(map[string][]string) for _, r := range registrations { out[typeToString(r.Type)] = nil } for _, c := range m.controllers { watches := map[string]struct{}{} // Extend existing watch list if one is present. This is necessary // because there can be multiple controllers for a given type. // ProxyStateTemplate, for example, is controlled by sidecar proxy and // gateway proxy controllers. if existing, ok := out[typeToString(c.managedTypeWatch.watchedType)]; ok { for _, w := range existing { watches[w] = struct{}{} } } for _, w := range c.watches { watches[typeToString(w.watchedType)] = struct{}{} } out[typeToString(c.managedTypeWatch.watchedType)] = maps.Keys(watches) } return out } func (deps Dependencies) ToMermaid() string { depStrings := make([]string, 0, len(deps)) for src, dsts := range deps { if len(dsts) == 0 { depStrings = append(depStrings, fmt.Sprintf(" %s", src)) continue } for _, dst := range dsts { depStrings = append(depStrings, fmt.Sprintf(" %s --> %s", src, dst)) } } sort.Slice(depStrings, func(a, b int) bool { return depStrings[a] < depStrings[b] }) out := "flowchart TD\n" + strings.Join(depStrings, "\n") return out }