diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 2cd2b01..52d9c9a 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -86,7 +86,7 @@ It creates a visual graph representation of the build process.`, } defer os.Remove(dotFile.Name()) - dotFileContent := dockerfile2dot.BuildDotFile( + dotFileContent, err := dockerfile2dot.BuildDotFile( dockerfile, f.concentrate, f.edgestyle.String(), @@ -96,6 +96,9 @@ It creates a visual graph representation of the build process.`, fmt.Sprintf("%.2f", f.nodesep), fmt.Sprintf("%.2f", f.ranksep), ) + if err != nil { + return + } _, err = dotFile.Write([]byte(dotFileContent)) if err != nil { diff --git a/internal/cmd/root_test.go b/internal/cmd/root_test.go index ff36ef8..ca1ab81 100644 --- a/internal/cmd/root_test.go +++ b/internal/cmd/root_test.go @@ -174,7 +174,7 @@ It creates a visual graph representation of the build process. stage_0_layer_1->stage_2_layer_1[ arrowhead=empty, ltail=cluster_stage_0, style=dashed ]; stage_1_layer_1->stage_2_layer_2[ arrowhead=empty, ltail=cluster_stage_1, style=dashed ]; subgraph cluster_stage_0 { - label=ubuntu; + label="ubuntu"; margin=16; stage_0_layer_0 [ fillcolor=white, label="FROM ubuntu:lates...", penwidth=0.5, shape=box, style="filled,rounded", width=2 ]; stage_0_layer_1 [ fillcolor=white, label="RUN apt-get updat...", penwidth=0.5, shape=box, style="filled,rounded", width=2 ]; @@ -191,7 +191,7 @@ It creates a visual graph representation of the build process. ; subgraph cluster_stage_2 { fillcolor=grey90; - label=release; + label="release"; margin=16; style=filled; stage_2_layer_0 [ fillcolor=white, label="FROM scratch AS r...", penwidth=0.5, shape=box, style="filled,rounded", width=2 ]; diff --git a/internal/dockerfile2dot/build.go b/internal/dockerfile2dot/build.go index a708eff..d4c1468 100644 --- a/internal/dockerfile2dot/build.go +++ b/internal/dockerfile2dot/build.go @@ -19,25 +19,68 @@ func BuildDotFile( maxLabelLength int, nodesep string, ranksep string, -) string { +) (string, error) { // Create a new graph graph := gographviz.NewEscape() - _ = graph.SetName("G") - _ = graph.SetDir(true) - _ = graph.AddAttr("G", "compound", "true") // allow edges between clusters - _ = graph.AddAttr("G", "nodesep", nodesep) - _ = graph.AddAttr("G", "rankdir", "LR") - _ = graph.AddAttr("G", "ranksep", ranksep) + + var graphErr error + set := func(err error) { + if graphErr == nil { + graphErr = err + } + } + + set(graph.SetName("G")) + set(graph.SetDir(true)) + set(graph.AddAttr("G", "compound", "true")) // allow edges between clusters + set(graph.AddAttr("G", "nodesep", nodesep)) + set(graph.AddAttr("G", "rankdir", "LR")) + set(graph.AddAttr("G", "ranksep", ranksep)) if concentrate { - _ = graph.AddAttr("G", "concentrate", "true") + set(graph.AddAttr("G", "concentrate", "true")) + } + + if graphErr != nil { + return "", graphErr } // Add the legend if requested if legend { - addLegend(graph, edgestyle) + if err := addLegend(graph, edgestyle); err != nil { + return "", err + } + } + + if err := addExternalImagesToGraph(graph, simplifiedDockerfile, maxLabelLength); err != nil { + return "", err + } + + if err := addStages(graph, simplifiedDockerfile, maxLabelLength, layers, edgestyle); err != nil { + return "", err + } + + // Add the ARGS that appear before the first stage, if layers are requested + if layers { + if err := addBeforeFirstStage(graph, simplifiedDockerfile); err != nil { + return "", err + } + } + + return graph.String(), nil +} + +func addExternalImagesToGraph( + graph *gographviz.Escape, + simplifiedDockerfile SimplifiedDockerfile, + maxLabelLength int, +) error { + var graphErr error + set := func(err error) { + if graphErr == nil { + graphErr = err + } } - // Add the external images for externalImageIndex, externalImage := range simplifiedDockerfile.ExternalImages { label := externalImage.Name if len(label) > maxLabelLength { @@ -48,7 +91,7 @@ func BuildDotFile( label = truncate.Truncate(label, maxLabelLength, "...", truncatePosition) } - _ = graph.AddNode( + set(graph.AddNode( "G", fmt.Sprintf("external_image_%d", externalImageIndex), map[string]string{ @@ -59,7 +102,24 @@ func BuildDotFile( "color": "grey20", "fontcolor": "grey20", }, - ) + )) + } + + return graphErr +} + +func addStages( + graph *gographviz.Escape, + simplifiedDockerfile SimplifiedDockerfile, + maxLabelLength int, + layers bool, + edgestyle string, +) error { + var graphErr error + set := func(err error) { + if graphErr == nil { + graphErr = err + } } for stageIndex, stage := range simplifiedDockerfile.Stages { @@ -72,40 +132,8 @@ func BuildDotFile( // Add layers if requested if layers { - cluster := fmt.Sprintf("cluster_stage_%d", stageIndex) - - clusterAttrs := map[string]string{ - "label": getStageLabel(stageIndex, stage, 0), - "margin": "16", - } - - if stageIndex == len(simplifiedDockerfile.Stages)-1 { - clusterAttrs["style"] = "filled" - clusterAttrs["fillcolor"] = "grey90" - } - - _ = graph.AddSubGraph("G", cluster, clusterAttrs) - - for layerIndex, layer := range stage.Layers { - attrs["label"] = "\"" + layer.Label + "\"" - attrs["penwidth"] = "0.5" - attrs["style"] = "\"filled,rounded\"" - attrs["fillcolor"] = "white" - _ = graph.AddNode( - cluster, - fmt.Sprintf("stage_%d_layer_%d", stageIndex, layerIndex), - attrs, - ) - - // Add edges between layers to guarantee the correct order - if layerIndex > 0 { - _ = graph.AddEdge( - fmt.Sprintf("stage_%d_layer_%d", stageIndex, layerIndex-1), - fmt.Sprintf("stage_%d_layer_%d", stageIndex, layerIndex), - true, - nil, - ) - } + if err := addStageWithLayers(graph, simplifiedDockerfile, stageIndex, stage, attrs); err != nil { + return err } } else { // Add the build stages. @@ -115,45 +143,117 @@ func BuildDotFile( attrs["fillcolor"] = "grey90" } - _ = graph.AddNode("G", fmt.Sprintf("stage_%d", stageIndex), attrs) + set(graph.AddNode("G", fmt.Sprintf("stage_%d", stageIndex), attrs)) + } + + if graphErr != nil { + return graphErr } - // Add the egdes for this build stage - addEdgesForStage( + // Add the edges for this build stage + if err := addEdgesForStage( stageIndex, stage, graph, simplifiedDockerfile, layers, edgestyle, - ) + ); err != nil { + return err + } } - // Add the ARGS that appear before the first stage, if layers are requested - if layers { - if len(simplifiedDockerfile.BeforeFirstStage) > 0 { - _ = graph.AddSubGraph( - "G", - "cluster_before_first_stage", - map[string]string{"label": "Before First Stage"}, - ) - for argIndex, arg := range simplifiedDockerfile.BeforeFirstStage { - _ = graph.AddNode( - "cluster_before_first_stage", - fmt.Sprintf("before_first_stage_%d", argIndex), - map[string]string{ - "label": arg.Label, - "shape": "box", - "style": "rounded", - "width": "2", - }, - ) - } + return graphErr +} + +func addStageWithLayers( + graph *gographviz.Escape, + simplifiedDockerfile SimplifiedDockerfile, + stageIndex int, + stage Stage, + attrs map[string]string, +) error { + var graphErr error + set := func(err error) { + if graphErr == nil { + graphErr = err + } + } + + cluster := fmt.Sprintf("cluster_stage_%d", stageIndex) + + clusterAttrs := map[string]string{ + "label": "\"" + getStageLabel(stageIndex, stage, 0) + "\"", + "margin": "16", + } + + if stageIndex == len(simplifiedDockerfile.Stages)-1 { + clusterAttrs["style"] = "filled" + clusterAttrs["fillcolor"] = "grey90" + } + + set(graph.AddSubGraph("G", cluster, clusterAttrs)) + + for layerIndex, layer := range stage.Layers { + attrs["label"] = "\"" + layer.Label + "\"" + attrs["penwidth"] = "0.5" + attrs["style"] = "\"filled,rounded\"" + attrs["fillcolor"] = "white" + set(graph.AddNode( + cluster, + fmt.Sprintf("stage_%d_layer_%d", stageIndex, layerIndex), + attrs, + )) + + // Add edges between layers to guarantee the correct order + if layerIndex > 0 { + set(graph.AddEdge( + fmt.Sprintf("stage_%d_layer_%d", stageIndex, layerIndex-1), + fmt.Sprintf("stage_%d_layer_%d", stageIndex, layerIndex), + true, + nil, + )) + } + } + + return graphErr +} + +func addBeforeFirstStage( + graph *gographviz.Escape, + simplifiedDockerfile SimplifiedDockerfile, +) error { + if len(simplifiedDockerfile.BeforeFirstStage) == 0 { + return nil + } + + var graphErr error + set := func(err error) { + if graphErr == nil { + graphErr = err } } - return graph.String() + set(graph.AddSubGraph( + "G", + "cluster_before_first_stage", + map[string]string{"label": "\"Before First Stage\""}, + )) + for argIndex, arg := range simplifiedDockerfile.BeforeFirstStage { + set(graph.AddNode( + "cluster_before_first_stage", + fmt.Sprintf("before_first_stage_%d", argIndex), + map[string]string{ + "label": "\"" + arg.Label + "\"", + "shape": "box", + "style": "rounded", + "width": "2", + }, + )) + } + + return graphErr } func addEdgesForStage( stageIndex int, stage Stage, graph *gographviz.Escape, simplifiedDockerfile SimplifiedDockerfile, layers bool, edgestyle string, -) { +) error { for layerIndex, layer := range stage.Layers { for _, waitFor := range layer.WaitFors { edgeAttrs := map[string]string{} @@ -169,9 +269,12 @@ func addEdgesForStage( } } - sourceNodeID, additionalEdgeAttrs := getWaitForNodeID( + sourceNodeID, additionalEdgeAttrs, err := getWaitForNodeID( simplifiedDockerfile, waitFor.ID, layers, ) + if err != nil { + return err + } maps.Copy(edgeAttrs, additionalEdgeAttrs) targetNodeID := fmt.Sprintf("stage_%d", stageIndex) @@ -179,15 +282,25 @@ func addEdgesForStage( targetNodeID = targetNodeID + fmt.Sprintf("_layer_%d", layerIndex) } - _ = graph.AddEdge(sourceNodeID, targetNodeID, true, edgeAttrs) + if err := graph.AddEdge(sourceNodeID, targetNodeID, true, edgeAttrs); err != nil { + return err + } } } + return nil } -func addLegend(graph *gographviz.Escape, edgestyle string) { - _ = graph.AddSubGraph("G", "cluster_legend", nil) +func addLegend(graph *gographviz.Escape, edgestyle string) error { + var graphErr error + set := func(err error) { + if graphErr == nil { + graphErr = err + } + } + + set(graph.AddSubGraph("G", "cluster_legend", nil)) - _ = graph.AddNode("cluster_legend", "key", + set(graph.AddNode("cluster_legend", "key", map[string]string{ "shape": "plaintext", "fontname": "monospace", @@ -198,8 +311,8 @@ func addLegend(graph *gographviz.Escape, edgestyle string) {