-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
View the SVG using a web server instead of PNG on the local FS (#4)
This requires zero setup for the user, because everyone has a browser ;) Added 3 flags: ```console $ flux-graph --help Processes a Flux Kustomization tree and generates a graph Usage: flux-graph [flags] Flags: -d, --direction string Specify direction of graph (TB,BT,LR,RL) (default "TB") -f, --file string Specify input file -h, --help help for flux-graph -n, --no-serve Don't serve the graph on a web server -o, --output string Specify output file (default "graph.svg") -p, --port string Specify web server port (default "9000") ``` Also refactored the code by making it a bit more modular and testable (still not there yet)
- Loading branch information
1 parent
230a178
commit 9aa0651
Showing
10 changed files
with
381 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,80 +1,58 @@ | ||
package main | ||
|
||
import ( | ||
"io" | ||
"log" | ||
"os" | ||
|
||
graphviz "github.com/goccy/go-graphviz" | ||
"github.com/rishinair11/flux-ks-graph/pkg/graph" | ||
"github.com/rishinair11/flux-ks-graph/pkg/resource" | ||
"github.com/rishinair11/flux-ks-graph/pkg/serve" | ||
"github.com/rishinair11/flux-ks-graph/pkg/util" | ||
"github.com/spf13/cobra" | ||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
func main() { | ||
var inputFile string | ||
var outputFile string | ||
var ( | ||
inputFile string | ||
outputFile string | ||
serverPort string | ||
graphDirection string | ||
noServe bool | ||
) | ||
|
||
rootCmd := &cobra.Command{ | ||
Use: "flux-graph", | ||
Short: "Processes a Flux Kustomization tree and generates a graph", | ||
Run: func(cmd *cobra.Command, _ []string) { | ||
var yamlBytes []byte | ||
var err error | ||
|
||
// Read YAML input | ||
yamlBytes, err = readInput(inputFile) | ||
rt, err := resource.NewResourceTree(inputFile) | ||
if err != nil { | ||
log.Fatalf("Failed to read YAML: %v", err) | ||
} | ||
|
||
// Unmarshal YAML into ResourceTree | ||
t := &resource.ResourceTree{} | ||
if err := yaml.Unmarshal(yamlBytes, t); err != nil { | ||
log.Fatalf("Failed to unmarshal YAML: %v", err) | ||
log.Fatalf("Failed to initialize ResourceTree: %v", err) | ||
} | ||
|
||
// Process the graph | ||
graph, err := graph.ProcessGraph(t) | ||
graph, err := graph.ProcessGraph(rt, graphDirection) | ||
if err != nil { | ||
log.Fatalf("Failed to construct graph: %v", err) | ||
} | ||
|
||
gvGraph, err := graphviz.ParseBytes([]byte(graph.String())) | ||
if err != nil { | ||
log.Fatalf("Failed to parse graph dot string: %v", err) | ||
if err := util.GenerateGraphSVG(graph, outputFile); err != nil { | ||
log.Fatalf("Failed to generate graph SVG: %v", err) | ||
} | ||
defer gvGraph.Close() | ||
|
||
f, err := os.Create(outputFile) | ||
if err != nil { | ||
log.Fatalf("Failed to create output file: %v", err) | ||
} | ||
defer f.Close() | ||
log.Println("Generated graph:", outputFile) | ||
|
||
if err := graphviz.New().RenderFilename(gvGraph, graphviz.PNG, f.Name()); err != nil { | ||
log.Fatalf("Failed to write output graph image: %v", err) | ||
if !noServe { | ||
serve.ServeAssets(outputFile, serverPort) | ||
} | ||
|
||
log.Println("Generated graph:", outputFile) | ||
}, | ||
} | ||
|
||
rootCmd.Flags().StringVarP(&inputFile, "file", "f", "", "Specify input file") | ||
rootCmd.Flags().StringVarP(&outputFile, "output", "o", "graph.png", "Specify output file") | ||
rootCmd.Flags().StringVarP(&graphDirection, "direction", "d", "TB", "Specify direction of graph (https://graphviz.gitlab.io/docs/attrs/rankdir)") | ||
rootCmd.Flags().StringVarP(&outputFile, "output", "o", "graph.svg", "Specify output file") | ||
rootCmd.Flags().StringVarP(&serverPort, "port", "p", "9000", "Specify web server port") | ||
rootCmd.Flags().BoolVarP(&noServe, "no-serve", "n", false, "Don't serve the graph on a web server") | ||
|
||
if err := rootCmd.Execute(); err != nil { | ||
log.Fatal(err) | ||
} | ||
} | ||
|
||
// readInput reads the YAML input from a file or stdin | ||
func readInput(inputFile string) ([]byte, error) { | ||
if inputFile != "" { | ||
log.Println("Reading from file:", inputFile) | ||
return os.ReadFile(inputFile) | ||
} | ||
log.Println("Reading from STDIN...") | ||
return io.ReadAll(os.Stdin) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
resource: | ||
GroupKind: | ||
Group: parent-group | ||
Kind: parent-kind | ||
Name: parent-name | ||
Namespace: parent-namespace | ||
resources: | ||
- resource: | ||
GroupKind: | ||
Group: child-group | ||
Kind: child-kind | ||
Name: child-name | ||
Namespace: child-namespace | ||
resources: | ||
- resource: | ||
GroupKind: | ||
Group: grandchild-group | ||
Kind: grandchild-kind | ||
Name: grandchild-name | ||
Namespace: grandchild-namespace |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package serve | ||
|
||
import ( | ||
"embed" | ||
"log" | ||
"net/http" | ||
"path/filepath" | ||
) | ||
|
||
//go:embed static/*.html | ||
var staticFS embed.FS | ||
|
||
// ServeAssets starts a web server to serve the generated graph.html containing the SVG | ||
func ServeAssets(filePath string, port string) { | ||
http.HandleFunc("/", handleGraphSVG) | ||
|
||
http.HandleFunc("/svg", func(w http.ResponseWriter, r *http.Request) { | ||
http.ServeFile(w, r, filePath) | ||
}) | ||
|
||
log.Println("Serving your graph at http://localhost:" + port) | ||
if err := http.ListenAndServe(":"+port, nil); err != nil { | ||
log.Fatalf("Failed to start server: %v", err) | ||
} | ||
} | ||
|
||
// handleGraphSVG serves graph.html, which contains the graph SVG, for "/" path. | ||
func handleGraphSVG(w http.ResponseWriter, r *http.Request) { | ||
data, err := staticFS.ReadFile(filepath.Join("static", "graph.html")) | ||
if err != nil { | ||
http.Error(w, "File not found", http.StatusNotFound) | ||
return | ||
} | ||
|
||
w.Header().Set("Content-Type", "text/html") | ||
w.Write(data) //nolint:errcheck | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package serve | ||
|
||
import ( | ||
"io" | ||
"net/http" | ||
"net/http/httptest" | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestHandleGraphSVG(t *testing.T) { | ||
// test server | ||
req := httptest.NewRequest(http.MethodGet, "/graph.html", nil) | ||
w := httptest.NewRecorder() | ||
|
||
handleGraphSVG(w, req) | ||
|
||
resp := w.Result() | ||
assert.Equal(t, http.StatusOK, resp.StatusCode) | ||
|
||
// response body should match the static file | ||
expectedBody, err := os.ReadFile(filepath.Join("static", "graph.html")) | ||
assert.NoError(t, err) | ||
|
||
body, err := io.ReadAll(resp.Body) | ||
assert.NoError(t, err) | ||
|
||
assert.Equal(t, string(expectedBody), string(body)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
<title>Flux Graph</title> | ||
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet"> | ||
<style> | ||
body, html { | ||
height: 100%; | ||
margin: 0; | ||
font-size: calc(1vw + 1vh + 0.5vmin); | ||
overflow: hidden; | ||
} | ||
.container-fluid { | ||
display: flex; | ||
flex-direction: column; | ||
height: 100vh; | ||
margin: 0; | ||
} | ||
#svg-container { | ||
flex: 1; | ||
margin: 10px; | ||
border: 1px solid #ccc; | ||
overflow: hidden; | ||
position: relative; | ||
} | ||
#svg-object { | ||
width: 100%; | ||
height: 100%; | ||
display: block; | ||
} | ||
#svg-controls { | ||
margin: 10px; | ||
text-align: center; | ||
} | ||
.btn { | ||
font-size: 0.5rem; | ||
margin: 5px; | ||
padding: 0.25rem 0.5rem; | ||
} | ||
.pan-cursor{ | ||
cursor: move !important; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<div class="container-fluid"> | ||
<div id="svg-container" class="pan-cursor"> | ||
<object id="svg-object" type="image/svg+xml" data="/svg"></object> | ||
</div> | ||
<div id="svg-controls"> | ||
<button id="zoom-in" class="btn btn-dark">Zoom +</button> | ||
<button id="zoom-out" class="btn btn-dark">Zoom -</button> | ||
<button id="reset" class="btn btn-dark">Reset</button> | ||
</div> | ||
</div> | ||
|
||
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js" crossorigin="anonymous"></script> | ||
<script> | ||
document.addEventListener("DOMContentLoaded", function() { | ||
var svgPanZoomInstance = null; | ||
var svgObject = document.getElementById('svg-object'); | ||
var svgContainer = document.getElementById('svg-container'); | ||
|
||
svgObject.addEventListener('load', function() { | ||
var svg = svgObject.contentDocument.querySelector('svg'); | ||
|
||
if (svg) { | ||
svgPanZoomInstance = svgPanZoom(svg, { | ||
zoomEnabled: true, | ||
fit: true, | ||
center: true, | ||
minZoom: 0.1, | ||
maxZoom: 100, | ||
zoomScaleSensitivity: 0.5, | ||
controlIconsEnabled: false, | ||
customControls: true | ||
}); | ||
|
||
document.getElementById('zoom-in').addEventListener('click', function() { | ||
if (svgPanZoomInstance) svgPanZoomInstance.zoomIn(); | ||
}); | ||
|
||
document.getElementById('zoom-out').addEventListener('click', function() { | ||
if (svgPanZoomInstance) svgPanZoomInstance.zoomOut(); | ||
}); | ||
|
||
document.getElementById('reset').addEventListener('click', function() { | ||
if (svgPanZoomInstance) { | ||
svgPanZoomInstance.reset(); | ||
svgPanZoomInstance.center(); | ||
} | ||
}); | ||
} else { | ||
console.error("SVG element not found."); | ||
} | ||
}); | ||
}); | ||
</script> | ||
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script> | ||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js"></script> | ||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script> | ||
</body> | ||
</html> |
Oops, something went wrong.