From 825b82bf9df985ddbc63b79ae1591a36dee5b6b3 Mon Sep 17 00:00:00 2001 From: vkalapov <87693906+vkalapov@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:56:38 +0300 Subject: [PATCH 1/2] Add cf mta-graph command for local mtad.yaml dependency graph --- commands/mta_graph_command.go | 132 ++++++++++++++++++++++++++++++++++ multiapps_plugin.go | 1 + 2 files changed, 133 insertions(+) create mode 100644 commands/mta_graph_command.go diff --git a/commands/mta_graph_command.go b/commands/mta_graph_command.go new file mode 100644 index 0000000..6cedcfc --- /dev/null +++ b/commands/mta_graph_command.go @@ -0,0 +1,132 @@ +package commands + +import ( + "flag" + "fmt" + "os" + + "code.cloudfoundry.org/cli/v8/plugin" + "github.com/cloudfoundry-incubator/multiapps-cli-plugin/ui" + "github.com/cloudfoundry-incubator/multiapps-cli-plugin/util" + "gopkg.in/yaml.v3" +) + +type MtaGraphCommand struct { + name string + cliConnection plugin.CliConnection +} + +func NewMtaGraphCommand() *MtaGraphCommand { + return &MtaGraphCommand{} +} + +func (c *MtaGraphCommand) GetPluginCommand() plugin.Command { + return plugin.Command{ + Name: "mta-graph", + HelpText: "Render the dependency graph of an MTA descriptor as DOT", + UsageDetails: plugin.Usage{ + Usage: "cf mta-graph ", + }, + } +} + +func (c *MtaGraphCommand) Initialize(name string, cliConnection plugin.CliConnection) { + c.name = name + c.cliConnection = cliConnection +} + +func (c *MtaGraphCommand) Execute(args []string) ExecutionStatus { + if len(args) == 0 { + ui.Failed("Usage: cf mta-graph ") + return Failure + } + data, err := os.ReadFile(args[0]) + if err != nil { + ui.Failed("Could not read file %s: %s", args[0], err) + return Failure + } + + descriptor, err := parseDescriptor(data) + if err != nil { + ui.Failed("Could not parse descriptor: %s", err) + return Failure + } + + fmt.Print(buildDot(descriptor)) + return Success +} + +// executeInternal satisfies the Command interface (never called for this command). +func (c *MtaGraphCommand) executeInternal(_ []string, _ string, _ *flag.FlagSet, _ util.CloudFoundryTarget) ExecutionStatus { + return Failure +} + +func (c *MtaGraphCommand) defineCommandOptions(_ *flag.FlagSet) {} + +// ---- descriptor model ---- + +type mtadDescriptor struct { + Modules []mtadModule `yaml:"modules"` + Resources []mtadResource `yaml:"resources"` +} + +type mtadModule struct { + Name string `yaml:"name"` + Provides []mtadProvided `yaml:"provides"` + Requires []mtadRequired `yaml:"requires"` +} + +type mtadProvided struct { + Name string `yaml:"name"` +} + +type mtadRequired struct { + Name string `yaml:"name"` +} + +type mtadResource struct { + Name string `yaml:"name"` +} + +func parseDescriptor(data []byte) (*mtadDescriptor, error) { + var d mtadDescriptor + if err := yaml.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil +} + +// ---- DOT builder ---- + +func buildDot(d *mtadDescriptor) string { + providedDepToModule := make(map[string]string) + for _, m := range d.Modules { + for _, p := range m.Provides { + providedDepToModule[p.Name] = m.Name + } + } + + resourceNames := make(map[string]bool) + for _, r := range d.Resources { + resourceNames[r.Name] = true + } + + dot := "digraph mta {\n rankdir=LR;\n" + for _, m := range d.Modules { + dot += fmt.Sprintf(" %q [shape=box];\n", m.Name) + } + for _, r := range d.Resources { + dot += fmt.Sprintf(" %q [shape=cylinder];\n", r.Name) + } + for _, m := range d.Modules { + for _, req := range m.Requires { + if provider, ok := providedDepToModule[req.Name]; ok { + dot += fmt.Sprintf(" %q -> %q [label=\"provides\"];\n", provider, m.Name) + } else if resourceNames[req.Name] { + dot += fmt.Sprintf(" %q -> %q [label=\"requires-resource\"];\n", m.Name, req.Name) + } + } + } + dot += "}\n" + return dot +} diff --git a/multiapps_plugin.go b/multiapps_plugin.go index 4166bd6..a7ee8a3 100644 --- a/multiapps_plugin.go +++ b/multiapps_plugin.go @@ -31,6 +31,7 @@ var Commands = []commands.Command{ commands.NewMtaOperationsCommand(), commands.NewPurgeConfigCommand(), commands.NewRollbackMtaCommand(), + commands.NewMtaGraphCommand(), } // Run runs this plugin From b0c95b5efb5215d32971d902795ab1cf3b7dadb7 Mon Sep 17 00:00:00 2001 From: vkalapov <87693906+vkalapov@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:39:11 +0200 Subject: [PATCH 2/2] Style mta-graph DOT and label edges with dependency names --- commands/mta_graph_command.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/commands/mta_graph_command.go b/commands/mta_graph_command.go index 6cedcfc..e1cf86f 100644 --- a/commands/mta_graph_command.go +++ b/commands/mta_graph_command.go @@ -111,19 +111,27 @@ func buildDot(d *mtadDescriptor) string { resourceNames[r.Name] = true } - dot := "digraph mta {\n rankdir=LR;\n" + dot := "digraph mta {\n" + dot += " rankdir=LR;\n" + dot += " node [fontname=\"Helvetica\" fontsize=11];\n" + dot += " edge [fontname=\"Helvetica\" fontsize=10];\n" for _, m := range d.Modules { - dot += fmt.Sprintf(" %q [shape=box];\n", m.Name) + dot += fmt.Sprintf(" %q [shape=box style=\"rounded,filled\" fillcolor=\"#DCE9F7\" color=\"#4A7AB5\"];\n", m.Name) } + dot += " { rank=same;" for _, r := range d.Resources { - dot += fmt.Sprintf(" %q [shape=cylinder];\n", r.Name) + dot += fmt.Sprintf(" %q", r.Name) + } + dot += " }\n" + for _, r := range d.Resources { + dot += fmt.Sprintf(" %q [shape=cylinder style=filled fillcolor=\"#EDEDED\" color=\"#8A8A8A\"];\n", r.Name) } for _, m := range d.Modules { for _, req := range m.Requires { if provider, ok := providedDepToModule[req.Name]; ok { - dot += fmt.Sprintf(" %q -> %q [label=\"provides\"];\n", provider, m.Name) + dot += fmt.Sprintf(" %q -> %q [label=\"provides %s\" color=\"#4A7AB5\" fontcolor=\"#4A7AB5\"];\n", provider, m.Name, req.Name) } else if resourceNames[req.Name] { - dot += fmt.Sprintf(" %q -> %q [label=\"requires-resource\"];\n", m.Name, req.Name) + dot += fmt.Sprintf(" %q -> %q [label=\"requires\" style=dashed color=\"#8A8A8A\" fontcolor=\"#8A8A8A\"];\n", m.Name, req.Name) } } }