Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ dockerfilegraph
- `--output svg|png|pdf` - Choose your output format
- `--legend` - Add a legend explaining the notation
- `--layers` - Show all Docker layers
- `--separate ubuntu,alpine` - Display selected external images as separate nodes per usage, reducing edge clutter

**All Available Options:**

Expand All @@ -131,6 +132,7 @@ Flags:
-o, --output output file format, one of: canon, dot, pdf, png, raw, svg (default pdf)
-r, --ranksep float minimum separation between ranks (default 0.5)
--scratch how to handle scratch images, one of: collapsed, hidden, separated (default collapsed)
--separate strings external images to display as separate nodes per usage (e.g. --separate ubuntu,alpine)
-u, --unflatten uint stagger length of leaf edges between [1,u] (default 0)
--version display the version of dockerfilegraph
```
Expand Down
27 changes: 24 additions & 3 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type cliFlags struct {
output enum
ranksep float64
scratch enum
separate []string
unflatten uint
version bool
}
Expand Down Expand Up @@ -67,14 +68,15 @@ It creates a visual graph representation of the build process.`,
}

// Determine scratch mode from flag
scratchMode := f.scratch.String()
scratchMode := scratchModeFromString(f.scratch.String())

// Load and parse the Dockerfile.
dockerfile, err := dockerfile2dot.LoadAndParseDockerfile(
inputFS,
f.filename,
int(f.maxLabelLength),
scratchMode,
f.separate,
)
if err != nil {
return
Expand All @@ -93,8 +95,8 @@ It creates a visual graph representation of the build process.`,
f.layers,
f.legend,
int(f.maxLabelLength),
fmt.Sprintf("%.2f", f.nodesep),
fmt.Sprintf("%.2f", f.ranksep),
f.nodesep,
f.ranksep,
)
if err != nil {
return
Expand Down Expand Up @@ -248,6 +250,13 @@ It creates a visual graph representation of the build process.`,
"how to handle scratch images, one of: "+strings.Join(f.scratch.AllowedValues(), ", "),
)

rootCmd.Flags().StringSliceVar(
&f.separate,
"separate",
nil,
"external images to display as separate nodes per usage (e.g. --separate ubuntu,alpine)",
)

rootCmd.Flags().UintVarP(
&f.unflatten,
"unflatten",
Expand Down Expand Up @@ -329,3 +338,15 @@ func checkFlags(maxLabelLength uint) error {
}
return nil
}

// scratchModeFromString converts a validated enum string to a ScratchMode constant.
func scratchModeFromString(s string) dockerfile2dot.ScratchMode {
switch s {
case "separated":
return dockerfile2dot.ScratchSeparated
case "hidden":
return dockerfile2dot.ScratchHidden
default:
return dockerfile2dot.ScratchCollapsed
}
}
60 changes: 60 additions & 0 deletions internal/cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Flags:
-o, --output output file format, one of: canon, dot, pdf, png, raw, svg (default pdf)
-r, --ranksep float minimum separation between ranks (default 0.5)
--scratch how to handle scratch images, one of: collapsed, hidden, separated (default collapsed)
--separate strings external images to display as separate nodes per usage (e.g. --separate ubuntu,alpine)
-u, --unflatten uint stagger length of leaf edges between [1,u] (default 0)
--version display the version of dockerfilegraph
`
Expand Down Expand Up @@ -558,6 +559,65 @@ It creates a visual graph representation of the build process.
wantErr: true,
wantOutRegex: `invalid argument "invalid" for "--scratch" flag: invalid value: invalid`,
},
{
name: "separate flag single image",
cliArgs: []string{"--separate", "ubuntu:latest", "-o", "raw"},
dockerfileContent: "FROM ubuntu:latest AS base\nRUN echo base\n\n" +
"FROM ubuntu:latest AS other\nRUN echo other\n\n" +
"FROM alpine AS final\nCOPY --from=base . .\nCOPY --from=other . .\n",
wantOut: "Successfully created Dockerfile.raw\n",
wantOutFile: "Dockerfile.raw",
wantOutFileContent: `digraph G {
compound=true;
nodesep=1.00;
rankdir=LR;
ranksep=0.50;
external_image_0->stage_0;
external_image_1->stage_1;
external_image_2->stage_2;
stage_0->stage_2[ arrowhead=empty, style=dashed ];
stage_1->stage_2[ arrowhead=empty, style=dashed ];
external_image_0 [ color=grey20, fontcolor=grey20, label="ubuntu:latest", shape=box, style="dashed,rounded", width=2 ];
external_image_1 [ color=grey20, fontcolor=grey20, label="ubuntu:latest", shape=box, style="dashed,rounded", width=2 ];
external_image_2 [ color=grey20, fontcolor=grey20, label="alpine", shape=box, style="dashed,rounded", width=2 ];
stage_0 [ label="base", shape=box, style=rounded, width=2 ];
stage_1 [ label="other", shape=box, style=rounded, width=2 ];
stage_2 [ fillcolor=grey90, label="final", shape=box, style="filled,rounded", width=2 ];

}
`,
},
{
name: "separate flag multiple images",
cliArgs: []string{"--separate", "ubuntu:latest,alpine", "-o", "raw"},
dockerfileContent: "FROM ubuntu:latest AS base\nRUN echo base\n\n" +
"FROM alpine AS mid\nRUN echo mid\n\n" +
"FROM ubuntu:latest AS other\nRUN echo other\n\n" +
"FROM alpine AS final\nCOPY --from=base . .\n",
wantOut: "Successfully created Dockerfile.raw\n",
wantOutFile: "Dockerfile.raw",
wantOutFileContent: `digraph G {
compound=true;
nodesep=1.00;
rankdir=LR;
ranksep=0.50;
external_image_0->stage_0;
external_image_1->stage_1;
external_image_2->stage_2;
external_image_3->stage_3;
stage_0->stage_3[ arrowhead=empty, style=dashed ];
external_image_0 [ color=grey20, fontcolor=grey20, label="ubuntu:latest", shape=box, style="dashed,rounded", width=2 ];
external_image_1 [ color=grey20, fontcolor=grey20, label="alpine", shape=box, style="dashed,rounded", width=2 ];
external_image_2 [ color=grey20, fontcolor=grey20, label="ubuntu:latest", shape=box, style="dashed,rounded", width=2 ];
external_image_3 [ color=grey20, fontcolor=grey20, label="alpine", shape=box, style="dashed,rounded", width=2 ];
stage_0 [ label="base", shape=box, style=rounded, width=2 ];
stage_1 [ label="mid", shape=box, style=rounded, width=2 ];
stage_2 [ label="other", shape=box, style=rounded, width=2 ];
stage_3 [ fillcolor=grey90, label="final", shape=box, style="filled,rounded", width=2 ];

}
`,
},
}

for _, tt := range tests {
Expand Down
8 changes: 4 additions & 4 deletions internal/dockerfile2dot/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ func BuildDotFile(
layers bool,
legend bool,
maxLabelLength int,
nodesep string,
ranksep string,
nodesep float64,
ranksep float64,
) (string, error) {
// Create a new graph
graph := gographviz.NewEscape()
Expand All @@ -33,9 +33,9 @@ func BuildDotFile(
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", "nodesep", fmt.Sprintf("%.2f", nodesep)))
set(graph.AddAttr("G", "rankdir", "LR"))
set(graph.AddAttr("G", "ranksep", ranksep))
set(graph.AddAttr("G", "ranksep", fmt.Sprintf("%.2f", ranksep)))
if concentrate {
set(graph.AddAttr("G", "concentrate", "true"))
}
Expand Down
18 changes: 9 additions & 9 deletions internal/dockerfile2dot/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func TestBuildDotFileErrors(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := BuildDotFile(
tt.simplifiedDockerfile, false, "default", tt.layers, false, 20, "0.5", "0.5",
tt.simplifiedDockerfile, false, "default", tt.layers, false, 20, 0.5, 0.5,
)
if err == nil {
t.Error("BuildDotFile() expected an error, got nil")
Expand All @@ -75,8 +75,8 @@ func TestBuildDotFile(t *testing.T) {
layers bool
legend bool
maxLabelLength int
nodesep string
ranksep string
nodesep float64
ranksep float64
}
tests := []struct {
name string
Expand Down Expand Up @@ -113,8 +113,8 @@ func TestBuildDotFile(t *testing.T) {
edgestyle: "default",
legend: true,
maxLabelLength: 20,
nodesep: "0.5",
ranksep: "0.5",
nodesep: 0.5,
ranksep: 0.5,
},
wantContains: "release",
},
Expand Down Expand Up @@ -148,8 +148,8 @@ func TestBuildDotFile(t *testing.T) {
edgestyle: "default",
layers: true,
maxLabelLength: 20,
nodesep: "0.5",
ranksep: "0.5",
nodesep: 0.5,
ranksep: 0.5,
},
wantContains: "release",
},
Expand Down Expand Up @@ -193,8 +193,8 @@ func TestBuildDotFile(t *testing.T) {
layers: false,
legend: false,
maxLabelLength: 20,
nodesep: "0.5",
ranksep: "0.5",
nodesep: 0.5,
ranksep: 0.5,
},
wantContains: `external_image_0 [ color=grey20, fontcolor=grey20, label="scratch", shape=box, ` +
`style="dashed,rounded", width=2 ];
Expand Down
Loading
Loading