github.com/mweagle/Sparta@v1.15.0/describe.go (about) 1 // +build !lambdabinary 2 3 package sparta 4 5 import ( 6 "bytes" 7 "encoding/json" 8 "fmt" 9 "io" 10 "text/template" 11 "time" 12 13 "github.com/pkg/errors" 14 "github.com/sirupsen/logrus" 15 ) 16 17 // workflowHooksDescriptionNodes returns the set of []*DescriptionInfo 18 // entries that summarizes the WorkflowNodes 19 func workflowHooksDescriptionNodes(serviceName string, hooks *WorkflowHooks) ([]*DescriptionInfo, error) { 20 if hooks == nil { 21 return nil, nil 22 } 23 workflowDescInfo := make([]*DescriptionInfo, 0) 24 for _, eachServiceDecorator := range hooks.ServiceDecorators { 25 describable, isDescribable := eachServiceDecorator.(Describable) 26 if isDescribable { 27 descInfos, descInfosErr := describable.Describe(serviceName) 28 if descInfosErr != nil { 29 return nil, descInfosErr 30 } 31 workflowDescInfo = append(workflowDescInfo, descInfos) 32 } 33 } 34 return workflowDescInfo, nil 35 } 36 37 // Describe produces a graphical representation of a service's Lambda and data sources. Typically 38 // automatically called as part of a compiled golang binary via the `describe` command 39 // line option. 40 func Describe(serviceName string, 41 serviceDescription string, 42 lambdaAWSInfos []*LambdaAWSInfo, 43 api APIGateway, 44 s3Site *S3Site, 45 s3BucketName string, 46 buildTags string, 47 linkFlags string, 48 outputWriter io.Writer, 49 workflowHooks *WorkflowHooks, 50 logger *logrus.Logger) error { 51 52 validationErr := validateSpartaPreconditions(lambdaAWSInfos, logger) 53 if validationErr != nil { 54 return validationErr 55 } 56 buildID, buildIDErr := provisionBuildID("none", logger) 57 if buildIDErr != nil { 58 buildID = fmt.Sprintf("%d", time.Now().Unix()) 59 } 60 var cloudFormationTemplate bytes.Buffer 61 err := Provision(true, 62 serviceName, 63 serviceDescription, 64 lambdaAWSInfos, 65 api, 66 s3Site, 67 s3BucketName, 68 false, 69 false, 70 buildID, 71 "", 72 buildTags, 73 linkFlags, 74 &cloudFormationTemplate, 75 workflowHooks, 76 logger) 77 if nil != err { 78 return err 79 } 80 81 tmpl, err := template.New("description").Parse(_escFSMustString(false, "/resources/describe/template.html")) 82 if err != nil { 83 return errors.New(err.Error()) 84 } 85 86 // Setup the describer 87 describer := descriptionWriter{ 88 nodes: make([]*cytoscapeNode, 0), 89 logger: logger, 90 } 91 92 // Instead of inline mermaid stuff, we're going to stuff raw 93 // json through. We can also include AWS images in the icon 94 // using base64/encoded: 95 // Example: https://cytoscape.github.io/cytoscape.js-tutorial-demo/datasets/social.json 96 // Use the "fancy" CSS: 97 // https://github.com/cytoscape/cytoscape.js-tutorial-demo/blob/gh-pages/stylesheets/fancy.json 98 // Which is dynamically updated at: https://cytoscape.github.io/cytoscape.js-tutorial-demo/ 99 100 fullIconPath := func(descriptionNode *DescriptionIcon) string { 101 // Use an empty PNG if we don't have an image 102 if descriptionNode == nil { 103 // Because the style uses data(image) we need to ensure that 104 // empty nodes have some sort of image, else the Cytoscape JS 105 // won't render 106 return "AWS-Architecture-Icons_PNG/empty-image.png" 107 } 108 return fmt.Sprintf("AWS-Architecture-Icons_PNG/PNG Light/%s/%s", 109 descriptionNode.Category, 110 descriptionNode.Name) 111 } 112 113 // Setup the root object 114 writeErr := describer.writeNodeWithParent(serviceName, 115 nodeColorService, 116 fullIconPath(&DescriptionIcon{ 117 Category: "Management & Governance", 118 Name: "AWS-CloudFormation_Stack_light-bg@4x.png", 119 }), 120 "", 121 labelWeightBold) 122 if writeErr != nil { 123 return writeErr 124 } 125 126 parentMap := make(map[string]bool) 127 writeNodes := func(parent string, descriptionNodes []*DescriptionTriplet) error { 128 if parent != "" { 129 _, exists := parentMap[parent] 130 if !exists { 131 writeErr = describer.writeNodeWithParent(parent, 132 "#FF0000", 133 fullIconPath(nil), 134 "", 135 labelWeightBold) 136 if writeErr != nil { 137 return writeErr 138 } 139 parentMap[parent] = true 140 } 141 } 142 143 for _, eachDescNode := range descriptionNodes { 144 descDisplayInfo := eachDescNode.DisplayInfo 145 if descDisplayInfo == nil { 146 descDisplayInfo = &DescriptionDisplayInfo{} 147 } 148 writeErr = describer.writeNodeWithParent(eachDescNode.SourceNodeName, 149 descDisplayInfo.SourceNodeColor, 150 fullIconPath(descDisplayInfo.SourceIcon), 151 parent, 152 labelWeightNormal) 153 if writeErr != nil { 154 return writeErr 155 } 156 writeErr = describer.writeEdge(eachDescNode.SourceNodeName, 157 eachDescNode.TargetNodeName, 158 eachDescNode.ArcLabel) 159 if writeErr != nil { 160 return writeErr 161 } 162 } 163 return nil 164 } 165 166 for _, eachLambda := range lambdaAWSInfos { 167 descriptionNodes, descriptionNodesErr := eachLambda.Description(serviceName) 168 if descriptionNodesErr != nil { 169 return descriptionNodesErr 170 } 171 writeErr := writeNodes("Lambdas", descriptionNodes) 172 if writeErr != nil { 173 return writeErr 174 } 175 } 176 // The API needs to know how to describe itself. So for that it needs an object that 177 // encapsulates writing the nodes and links...so let's go ahead 178 // and make that object, then supply it to the Describe interface function 179 180 // API? 181 if nil != api { 182 descriptionInfo, descriptionInfoErr := api.Describe(serviceName) 183 if descriptionInfoErr != nil { 184 return descriptionInfoErr 185 } 186 writeErr := writeNodes(descriptionInfo.Name, descriptionInfo.Nodes) 187 if writeErr != nil { 188 return writeErr 189 } 190 } 191 // What about everything else... 192 workflowDescription, workflowDescriptionErr := workflowHooksDescriptionNodes(serviceName, workflowHooks) 193 if workflowDescriptionErr != nil { 194 return workflowDescriptionErr 195 } 196 for _, eachWorkflowDesc := range workflowDescription { 197 groupName := eachWorkflowDesc.Name 198 if groupName == "" { 199 groupName = "WorkflowHooks" 200 } 201 workflowDescriptionErr = writeNodes(groupName, eachWorkflowDesc.Nodes) 202 if workflowDescriptionErr != nil { 203 return workflowDescriptionErr 204 } 205 } 206 207 // Write it out... 208 cytoscapeBytes, cytoscapeBytesErr := json.MarshalIndent(describer.nodes, "", " ") 209 if cytoscapeBytesErr != nil { 210 return errors.Wrapf(cytoscapeBytesErr, "Failed to marshal cytoscape data") 211 } 212 213 params := struct { 214 SpartaVersion string 215 ServiceName string 216 ServiceDescription string 217 CloudFormationTemplate string 218 CSSFiles []*templateResource 219 JSFiles []*templateResource 220 ImageMap map[string]string 221 CytoscapeData interface{} 222 }{ 223 SpartaGitHash[0:8], 224 serviceName, 225 serviceDescription, 226 cloudFormationTemplate.String(), 227 templateCSSFiles(logger), 228 templateJSFiles(logger), 229 templateImageMap(logger), 230 string(cytoscapeBytes), 231 } 232 return tmpl.Execute(outputWriter, params) 233 }