1+ // Copyright 2025 The Kube Resource Orchestrator Authors
2+ //
3+ // Licensed under the Apache License, Version 2.0 (the "License");
4+ // you may not use this file except in compliance with the License.
5+ // You may obtain a copy of the License at
6+ //
7+ // http://www.apache.org/licenses/LICENSE-2.0
8+ //
9+ // Unless required by applicable law or agreed to in writing, software
10+ // distributed under the License is distributed on an "AS IS" BASIS,
11+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ // See the License for the specific language governing permissions and
13+ // limitations under the License.
14+
115package commands
216
317import (
418 "encoding/json"
519 "fmt"
620 "os"
721
22+ "github.com/go-echarts/go-echarts/v2/charts"
23+ "github.com/go-echarts/go-echarts/v2/opts"
824 "github.com/kro-run/kro/api/v1alpha1"
925 kroclient "github.com/kro-run/kro/pkg/client"
1026 "github.com/kro-run/kro/pkg/graph"
@@ -64,7 +80,7 @@ var generateCRDCmd = &cobra.Command{
6480
6581var generateInstanceCmd = & cobra.Command {
6682 Use : "instance" ,
67- Short : "Generate a ResourceGraphDefinition (RGD) instance " ,
83+ Short : "Generate an instance of the ResourceGraphDefinition " ,
6884 Long : "Generate a ResourceGraphDefinition (RGD) instance from a " +
6985 "ResourceGraphDefinition file. This command reads the " +
7086 "ResourceGraphDefinition and outputs the corresponding RGD instance" ,
@@ -84,7 +100,36 @@ var generateInstanceCmd = &cobra.Command{
84100 }
85101
86102 if err = generateInstance (& rgd ); err != nil {
87- return fmt .Errorf ("failed to generate RGD instance: %w" , err )
103+ return fmt .Errorf ("failed to generate instance: %w" , err )
104+ }
105+
106+ return nil
107+ },
108+ }
109+
110+ var generateDiagramCmd = & cobra.Command {
111+ Use : "diagram" ,
112+ Short : "Generate a diagram from a ResourceGraphDefinition" ,
113+ Long : "Generate a diagram from a ResourceGraphDefinition file. This command reads the " +
114+ "ResourceGraphDefinition and outputs the corresponding diagram " +
115+ "in the specified format." ,
116+ RunE : func (cmd * cobra.Command , args []string ) error {
117+ if config .resourceGraphDefinitionFile == "" {
118+ return fmt .Errorf ("ResourceGraphDefinition file is required" )
119+ }
120+
121+ data , err := os .ReadFile (config .resourceGraphDefinitionFile )
122+ if err != nil {
123+ return fmt .Errorf ("failed to read ResourceGraphDefinition file: %w" , err )
124+ }
125+
126+ var rgd v1alpha1.ResourceGraphDefinition
127+ if err = yaml .Unmarshal (data , & rgd ); err != nil {
128+ return fmt .Errorf ("failed to unmarshal ResourceGraphDefinition: %w" , err )
129+ }
130+
131+ if err = generateDiagram (& rgd ); err != nil {
132+ return fmt .Errorf ("failed to generate diagram: %w" , err )
88133 }
89134
90135 return nil
@@ -110,6 +155,93 @@ func generateCRD(rgd *v1alpha1.ResourceGraphDefinition) error {
110155 return nil
111156}
112157
158+ func generateDiagram (rgd * v1alpha1.ResourceGraphDefinition ) error {
159+ rgdGraph , err := createGraphBuilder (rgd )
160+ if err != nil {
161+ return fmt .Errorf ("failed to setup rgd graph: %w" , err )
162+ }
163+
164+ graph := charts .NewGraph ()
165+
166+ // Graph layout
167+ graph .SetGlobalOptions (
168+ charts .WithInitializationOpts (opts.Initialization {
169+ Width : "70%" ,
170+ Height : "90vh" ,
171+ }),
172+ charts .WithTitleOpts (opts.Title {
173+ Title : fmt .Sprintf ("Resource Graph: %s" , rgd .Name ),
174+ Subtitle : fmt .Sprintf ("Topological Order: %v" , rgdGraph .TopologicalOrder ),
175+ TitleStyle : & opts.TextStyle {
176+ FontSize : 18 ,
177+ FontWeight : "bold" ,
178+ },
179+ SubtitleStyle : & opts.TextStyle {
180+ FontSize : 14 ,
181+ },
182+ }),
183+ charts .WithLegendOpts (opts.Legend {
184+ Show : opts .Bool (false ),
185+ }),
186+ )
187+
188+ resourceOrder := make (map [string ]int )
189+ for i , resourceName := range rgdGraph .TopologicalOrder {
190+ resourceOrder [resourceName ] = i + 1
191+ }
192+
193+ // Graph nodes
194+ nodes := make ([]opts.GraphNode , 0 , len (rgdGraph .Resources ))
195+ for resourceName := range rgdGraph .Resources {
196+ order := resourceOrder [resourceName ]
197+
198+ node := opts.GraphNode {
199+ Name : fmt .Sprintf ("%s\n (%d)" , resourceName , order ),
200+ SymbolSize : 40.0 ,
201+ ItemStyle : & opts.ItemStyle {
202+ Color : "#269103" ,
203+ BorderColor : "#333" ,
204+ BorderWidth : 1 ,
205+ },
206+ }
207+ nodes = append (nodes , node )
208+ }
209+
210+ // Graph links
211+ links := make ([]opts.GraphLink , 0 )
212+ for resourceName , resource := range rgdGraph .Resources {
213+ targetOrder := resourceOrder [resourceName ]
214+
215+ for _ , dependency := range resource .GetDependencies () {
216+ sourceOrder := resourceOrder [dependency ]
217+
218+ link := opts.GraphLink {
219+ Source : fmt .Sprintf ("%s\n (%d)" , dependency , sourceOrder ),
220+ Target : fmt .Sprintf ("%s\n (%d)" , resourceName , targetOrder ),
221+ LineStyle : & opts.LineStyle {
222+ Color : "#000" ,
223+ },
224+ }
225+ links = append (links , link )
226+ }
227+ }
228+
229+ graph .AddSeries ("resource" , nodes , links ).SetSeriesOptions (
230+ charts .WithGraphChartOpts (
231+ opts.GraphChart {
232+ Force : & opts.GraphForce {
233+ Repulsion : 5000 ,
234+ },
235+ Roam : opts .Bool (true ),
236+ EdgeSymbol : "arrow" ,
237+ EdgeSymbolSize : []int {0 , 7 },
238+ },
239+ ),
240+ )
241+
242+ return graph .Render (os .Stdout )
243+ }
244+
113245func generateInstance (rgd * v1alpha1.ResourceGraphDefinition ) error {
114246 rgdGraph , err := createGraphBuilder (rgd )
115247 if err != nil {
@@ -180,6 +312,7 @@ func marshalObject(obj interface{}, outputFormat string) ([]byte, error) {
180312
181313func AddGenerateCommands (rootCmd * cobra.Command ) {
182314 generateCmd .AddCommand (generateCRDCmd )
315+ generateCmd .AddCommand (generateDiagramCmd )
183316 generateCmd .AddCommand (generateInstanceCmd )
184317 rootCmd .AddCommand (generateCmd )
185318}
0 commit comments