server.garden privileged automation agent (mirror of https://git.sequentialread.com/forest/rootsystem)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

453 lines
14 KiB

package main
import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"html"
"io"
"io/ioutil"
"os/exec"
"regexp"
"strings"
errors "git.sequentialread.com/forest/pkg-errors"
)
type XMLAttr struct {
Name string
Value string
Namespace string
Local string
}
type XMLNode struct {
XMLName xml.Name
Attrs []*XMLAttr `xml:"-"`
Content []byte `xml:",innerxml"`
Children []*XMLNode `xml:",any"`
Parent *XMLNode `xml:"-"`
}
type XMLQuery struct {
DirectChild bool
NodeType string
Class string
Attr string
}
func (node *XMLNode) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
attrPointers := []*XMLAttr{}
for _, attr := range start.Attr {
// I dont really give a shit about XML namespaces but I do have to search for xlink:title which is separate from normal title
// hardcoded that this is happening inside the svg namespace so all non svg namespaces attrs will be specified like ns:attr
name := attr.Name.Local
if attr.Name.Space != "" {
// attr.Name.Space is a url like http://www.w3.org/1999/xlink or ttp://www.w3.org/2000/svg so we are just grabbing the last part
split := strings.Split(attr.Name.Space, "/")
ns := split[len(split)-1]
if ns != "svg" {
name = fmt.Sprintf("%s:%s", ns, name)
}
}
myAttr := XMLAttr{
Name: name,
Value: attr.Value,
Namespace: attr.Name.Space,
Local: attr.Name.Local,
}
attrPointers = append(attrPointers, &myAttr)
}
node.Attrs = attrPointers
// this little derived type wont directly have this UnmarshalXML method,
// thus prevent the decoder from thinking it has to invoke our custom UnmarshalXML again...
type dontCauseInfiniteLoop XMLNode
return d.DecodeElement((*dontCauseInfiniteLoop)(node), &start)
}
//https://stackoverflow.com/questions/39780433/golang-xml-customize-output
// // MarshalXML generate XML output for PrecsontructedInfo
// func (preconstructed PreconstructedInfo) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
// if (PreconstructedInfo{} == preconstructed) {
// return nil
// }
// if preconstructed.Decks > 0 {
// start.Attr = []xml.Attr{xml.Attr{Name: xml.Name{Local: "decks"}, Value: strconv.Itoa(preconstructed.Size)}}
// }
// if strings.Compare(preconstructed.Type, "") != 0 {
// start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "type"}, Value: preconstructed.Type})
// }
// err = e.EncodeToken(start)
// e.EncodeElement(preconstructed.Size, xml.StartElement{Name: xml.Name{Local: "size"}})
// return e.EncodeToken(xml.EndElement{Name: start.Name})
// }
func (node XMLNode) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
attrsToEncode := []xml.Attr{}
for _, attr := range node.Attrs {
// this encoder will print some stupid shit if you give it an xmlns attr:
// for example: <svg ... xmlns:_ _xmlns:xlink="http://www.w3.org/1999/xlink">
if attr.Namespace != "xmlns" {
attrsToEncode = append(attrsToEncode, xml.Attr{
Name: xml.Name{
Local: attr.Local,
Space: attr.Namespace,
},
Value: attr.Value,
})
}
}
start.Attr = attrsToEncode
start.Name = node.XMLName
// this little derived type wont directly have this UnmarshalXML method,
// thus prevent the decoder from thinking it has to invoke our custom UnmarshalXML again...
type dontCauseInfiniteLoop XMLNode
return e.EncodeElement((dontCauseInfiniteLoop)(node), start)
}
func main() {
bytes, err := ioutil.ReadFile("test-dot.txt")
if err != nil {
panic(err)
}
dot := string(bytes)
exitCode, dotStdout, dotStderr, err := shellExecInputPipe(".", &dot, "dot", "-Tsvg")
err = errorFromShellExecResult("dot -Tsvg", exitCode, dotStdout, dotStderr, err)
if err != nil {
panic(err)
}
svgString := string(dotStdout)
// the svg output from dot has html comments that contain metadata preceeding each entity.
// for example:
// <!-- param_ssh_private_key_filepath -->
// <g id="node16" class="node">
// We will attach that metadata to the svg elements as a data property
// Note that this will not work perfectly for the "cluster"s (aka modules)
// they are handled separately after the SVG has been inserted.
// Yes parsing XML with regex is "evil" but keep in mind, this XML was generated by a program, not written by hand.
// So it is a lot more predictable. Also, I couldn't figure out how to parse the comments with the go std lib XML parser.
commentRegex := regexp.MustCompile(`<!--\s*(.+)\s*-->\s*\n\s*(<[a-z]+\s)`)
svgString = commentRegex.ReplaceAllStringFunc(svgString, func(matched string) string {
matches := commentRegex.FindStringSubmatch(matched)
return fmt.Sprintf("%s dot=\"%s\" ", matches[2], html.UnescapeString(matches[1]))
})
ioutil.WriteFile("./test-gen.svg", []byte(svgString), 0777)
var svgDoc XMLNode
err = xml.Unmarshal(dotStdout, &svgDoc)
if err != nil {
panic(err)
}
var setParentRecurse func(*XMLNode)
setParentRecurse = func(node *XMLNode) {
for _, child := range node.Children {
child.Parent = node
setParentRecurse(child)
}
}
setParentRecurse(&svgDoc)
// remove the white background from the SVG
svgDoc.WithQuerySelector(
[]XMLQuery{XMLQuery{NodeType: "g", Class: "graph"}, XMLQuery{NodeType: "polygon", DirectChild: true}},
func(node *XMLNode) {
node.SetAttr("fill", "none")
},
)
// remove the white background from the edges
svgDoc.WithQuerySelector(
[]XMLQuery{XMLQuery{NodeType: "g", Class: "edge"}, XMLQuery{NodeType: "path"}},
func(node *XMLNode) {
node.SetAttr("fill", "none")
},
)
// correctly set the dot property for the "cluster"s (aka modules)
svgDoc.WithQuerySelector(
[]XMLQuery{XMLQuery{NodeType: "a", Attr: "xlink:title"}},
func(node *XMLNode) {
if node.Parent != nil && node.Parent.Parent != nil {
title := html.UnescapeString(node.GetAttr("xlink:title"))
title = regexp.MustCompile(`[.-]`).ReplaceAllString(title, "_")
node.Parent.Parent.SetAttr("dot", title)
}
},
)
// correctly set the dot property for resources or "node"s
svgDoc.WithQuerySelector(
[]XMLQuery{XMLQuery{NodeType: "g", Class: "node"}, XMLQuery{NodeType: "title"}},
func(node *XMLNode) {
if node.Parent != nil {
title := html.UnescapeString(string(node.Content))
node.Parent.SetAttr("dot", title)
}
},
)
// correctly set the dot property for edges
svgDoc.WithQuerySelector(
[]XMLQuery{XMLQuery{NodeType: "g", Class: "edge"}, XMLQuery{NodeType: "title"}},
func(node *XMLNode) {
if node.Parent != nil {
title := html.UnescapeString(string(node.Content))
title = strings.Replace(title, "->", "_to_", 1)
node.Parent.SetAttr("dot", title)
}
},
)
// // in order to get dot to render it correctly, the connections are actually between resources,
// // not between modules. So we have to trim the resource name off of the dot attribute
// // on the connections to make them match what we have in the status JSON object.
// const moduleNames = Array.from(svg.querySelectorAll("g.cluster")).map(x => x.dataset['dot']);
// Array.from(svg.querySelectorAll("g.edge")).forEach(edge => {
// const dot = edge.dataset['dot'];
// const fromTo = dot.split('->');
// if(fromTo.length != 2) {
// console.error("malformed dot on edge: fromTo.length != 2", + dot);
// }
// moduleNames.forEach(moduleName => {
// if(fromTo[0].indexOf(moduleName) == 0) {
// edge.dataset['dot'] = `${moduleName}->${fromTo[1]}`;
// }
// if(fromTo[1].indexOf(moduleName) == 0) {
// edge.dataset['dot'] = `${fromTo[0]}->${moduleName}`;
// }
// });
// });
// svgString := strings.ReplaceAll(string(dotStdout), `fill="none"`, `fill="#ffffff"`)
// matchTitleTag := regexp.MustCompile(`<title>[^<]*</title>`)
// svgString = matchTitleTag.ReplaceAllString(svgString, "")
// svgString = strings.ReplaceAll(svgString, `Times,serif`, `-apple-system,system-ui,BlinkMacSystemFont,Ubuntu,Roboto,Segoe UI,sans-serif`)
// testJson, _ := json.MarshalIndent(simpleStatus, "", " ")
// ioutil.WriteFile("./test-status.json", testJson, 0777)
// this encoder doesn't want to include my custom attributes...
// so we will hijack the id attribute! Record the value of the custom attribute so we can
// set the value of Id later.
var cleanUpSvgRecurse func(*XMLNode)
cleanUpSvgRecurse = func(node *XMLNode) {
// We cant serialize cyclical relationships like parent -> child -> parent
node.Parent = nil
// the XML serializer does not like my custom attributes like `data-dot` or `dot`.
// so we will just hijack the id attribute instead, poggers
dot := node.GetAttr("dot")
if dot != "" {
node.SetAttr("id", dot)
}
// if we don't zero out the content for non-leaf nodes, we will get duplicates of everything
if len(node.Children) != 0 {
node.Content = []byte{}
}
for _, child := range node.Children {
cleanUpSvgRecurse(child)
}
}
cleanUpSvgRecurse(&svgDoc)
testJson2, err := json.MarshalIndent(svgDoc, "", " ")
if err != nil {
panic(err)
}
ioutil.WriteFile("./test-processed.json", testJson2, 0777)
svgBytes, err := xml.MarshalIndent(svgDoc, "", " ")
// it goes crazy spamming xmlns everywhere, lets just remove all of them.
svgBytes = regexp.MustCompile(`xmlns[^=]*="[^"]+"`).ReplaceAll(svgBytes, []byte{})
// now we add the xmlns back to just the root element
startOfSvgTagWithXMLNS := []byte(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" `)
svgBytes = regexp.MustCompile(`^\s*<svg\s*`).ReplaceAll(svgBytes, startOfSvgTagWithXMLNS)
svgHeader := []byte(`<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz & server.garden rootsystem -->
`)
svgBytes = append(svgHeader, svgBytes...)
// if err != nil {
// panic(err)
// }
ioutil.WriteFile("./test-processed.svg", svgBytes, 0777)
}
func padStringForDot(str string) string {
pad := strings.Repeat(" ", int(float32(len(str))/4))
return fmt.Sprintf("%s%s%s", pad, str, pad)
}
// TODO which user to run this command as ??
// Dont run as root. (this process probably runs as root)
func shellExec(workingDirectory string, executable string, arguments ...string) (int, []byte, []byte, error) {
return shellExecInputPipe(workingDirectory, nil, executable, arguments...)
}
func shellExecInputPipe(workingDirectory string, input *string, executable string, arguments ...string) (int, []byte, []byte, error) {
process := exec.Command(executable, arguments...)
process.Dir = workingDirectory
if input != nil {
stdin, err := process.StdinPipe()
if err != nil {
return -1, []byte{}, []byte{}, errors.Wrap(err, "process.StdinPipe() returned")
}
go func() {
defer stdin.Close()
io.WriteString(stdin, *input)
}()
}
var processStdoutBuffer, processStderrBuffer bytes.Buffer
process.Stdout = &processStdoutBuffer
process.Stderr = &processStderrBuffer
err := process.Start()
if err != nil {
err = errors.Wrapf(err, "can't ShellExec(%s %s), process.Start() returned", executable, strings.Join(arguments, " "))
return process.ProcessState.ExitCode(), []byte(""), []byte(""), err
}
err = process.Wait()
if err != nil {
err = errors.Wrapf(err, "can't ShellExec(%s %s), process.Wait() returned", executable, strings.Join(arguments, " "))
}
return process.ProcessState.ExitCode(), processStdoutBuffer.Bytes(), processStderrBuffer.Bytes(), err
}
func errorFromShellExecResult(command string, exitCode int, stdout []byte, stderr []byte, err error) error {
if exitCode != 0 || err != nil {
errorString := "nil"
if err != nil {
errorString = err.Error()
}
return fmt.Errorf(
"%s failed with exit code %d, stdout: \n----\n%s\n----\nstderr: \n----\n%s\n----\nerror: %s",
command, exitCode, stdout, stderr, errorString,
)
}
return nil
}
func (doc *XMLNode) QuerySelectorAll(querySteps []XMLQuery) []*XMLNode {
iterate := func(search []*XMLNode, q XMLQuery) []*XMLNode {
toReturn := []*XMLNode{}
var recurse func(*XMLNode, bool)
recurse = func(node *XMLNode, isDirectChild bool) {
match := true
if q.NodeType != "" && node.XMLName.Local != q.NodeType {
match = false
}
if q.Class != "" {
found := false
for _, attr := range node.Attrs {
classes := strings.Split(attr.Value, " ")
if attr.Name == "class" {
for _, class := range classes {
if class == q.Class {
found = true
}
}
}
}
if !found {
match = false
}
}
if q.Attr != "" {
found := false
for _, attr := range node.Attrs {
if attr.Name == q.Attr {
found = true
}
}
if !found {
match = false
}
}
if match && (isDirectChild || !q.DirectChild) {
toReturn = append(toReturn, node)
}
if isDirectChild || !q.DirectChild {
for _, child := range node.Children {
recurse(child, false)
}
}
}
for _, node := range search {
for _, child := range node.Children {
recurse(child, true)
}
}
return toReturn
}
matched := []*XMLNode{doc}
for _, step := range querySteps {
matched = iterate(matched, step)
}
return matched
}
func (doc *XMLNode) WithQuerySelector(querySteps []XMLQuery, withResult func(*XMLNode)) {
allResults := doc.QuerySelectorAll(querySteps)
for _, res := range allResults {
withResult(res)
}
}
func (node *XMLNode) SetAttr(attr string, value string) {
has := false
for _, a := range node.Attrs {
if a.Name == attr {
a.Value = value
has = true
}
}
if !has {
newAttr := XMLAttr{
Name: attr,
Value: value,
}
node.Attrs = append(node.Attrs, &newAttr)
}
}
func (node *XMLNode) GetAttr(attr string) string {
for _, a := range node.Attrs {
if a.Name == attr {
return a.Value
}
}
return ""
}