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  }