github.com/w3security/vervet/v5@v5.3.1-0.20230618081846-5bd9b5d799dc/internal/backstage/backstage.go (about)

     1  // Package backstage supports vervet's integration with Backstage to
     2  // automatically populate API definitions in the catalog info from compiled
     3  // versions.
     4  package backstage
     5  
     6  import (
     7  	"errors"
     8  	"io"
     9  	"io/fs"
    10  	"os"
    11  	"path/filepath"
    12  	"sort"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath"
    17  	"gopkg.in/yaml.v3"
    18  
    19  	"github.com/w3security/vervet/v5"
    20  )
    21  
    22  const (
    23  	backstageVersion         = "backstage.io/v1alpha1"
    24  	w3securityApiVersionDate = "api.w3security.io/version-date"
    25  	w3securityApiStability   = "api.w3security.io/version-stability"
    26  	w3securityApiLifecycle   = "api.w3security.io/version-lifecycle"
    27  	w3securityApiGeneratedBy = "api.w3security.io/generated-by"
    28  )
    29  
    30  // Component represents a Backstage Component entity document.
    31  type Component struct {
    32  	APIVersion string        `json:"apiVersion" yaml:"apiVersion"`
    33  	Kind       string        `json:"kind" yaml:"kind"`
    34  	Metadata   Metadata      `json:"metadata" yaml:"metadata"`
    35  	Spec       ComponentSpec `json:"spec" yaml:"spec"`
    36  }
    37  
    38  // ComponentSpec represents a Backstage Component entity spec.
    39  type ComponentSpec struct {
    40  	Type         string   `json:"type" yaml:"type"`
    41  	Owner        string   `json:"owner" yaml:"owner"`
    42  	ProvidesAPIs []string `json:"providesApis" yaml:"providesApis"`
    43  }
    44  
    45  // API represents a Backstage API entity document.
    46  type API struct {
    47  	APIVersion string   `json:"apiVersion" yaml:"apiVersion"`
    48  	Kind       string   `json:"kind" yaml:"kind"`
    49  	Metadata   Metadata `json:"metadata" yaml:"metadata"`
    50  	Spec       APISpec  `json:"spec" yaml:"spec"`
    51  }
    52  
    53  // Metadata represents Backstage entity metadata.
    54  type Metadata struct {
    55  	Name        string            `json:"name,omitempty" yaml:"name,omitempty"`
    56  	Namespace   string            `json:"namespace,omitempty" yaml:"namespace,omitempty"`
    57  	Title       string            `json:"title,omitempty" yaml:"title,omitempty"`
    58  	Description string            `json:"description,omitempty" yaml:"description,omitempty"`
    59  	Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
    60  	Labels      map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
    61  	Tags        []string          `json:"tags,omitempty" yaml:"tags,omitempty"`
    62  }
    63  
    64  // APISpec represents a Backstage API entity spec.
    65  type APISpec struct {
    66  	Type       string        `json:"type" yaml:"type"`
    67  	Lifecycle  string        `json:"lifecycle" yaml:"lifecycle"`
    68  	Owner      string        `json:"owner" yaml:"owner"`
    69  	System     string        `json:"system,omitempty" yaml:"system,omitempty"`
    70  	Definition DefinitionRef `json:"definition" yaml:"definition"`
    71  }
    72  
    73  // DefinitionRef represents a reference to a local file in the project.
    74  type DefinitionRef struct {
    75  	Text string `json:"$text" yaml:"$text"`
    76  }
    77  
    78  // CatalogInfo models the Backstage catalog-info.yaml file at the top-level of
    79  // a project.
    80  type CatalogInfo struct {
    81  	service          *yaml.Node
    82  	serviceComponent Component
    83  	components       []*yaml.Node
    84  	VervetAPIs       []*API
    85  }
    86  
    87  // Save writes the catalog info to a writer.
    88  func (c *CatalogInfo) Save(w io.Writer) error {
    89  	enc := yaml.NewEncoder(w)
    90  	enc.SetIndent(2)
    91  	docs := []*yaml.Node{}
    92  	if c.service != nil {
    93  		docs = append(docs, c.service)
    94  	}
    95  	docs = append(docs, c.components...)
    96  	sort.Sort(vervetAPIs(c.VervetAPIs))
    97  	for _, vervetAPI := range c.VervetAPIs {
    98  		var doc yaml.Node
    99  		if err := doc.Encode(vervetAPI); err != nil {
   100  			return err
   101  		}
   102  		doc.HeadComment = "Generated by vervet, DO NOT EDIT"
   103  		docs = append(docs, &doc)
   104  	}
   105  	for _, doc := range docs {
   106  		if err := enc.Encode(doc); err != nil {
   107  			return err
   108  		}
   109  	}
   110  	return nil
   111  }
   112  
   113  type vervetAPIs []*API
   114  
   115  // Len implements sort.Interface.
   116  func (v vervetAPIs) Len() int { return len(v) }
   117  
   118  // Less implements sort.Interface.
   119  func (v vervetAPIs) Less(i, j int) bool { return v[i].Metadata.Name < v[j].Metadata.Name }
   120  
   121  // Swap implements sort.Interface.
   122  func (v vervetAPIs) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
   123  
   124  // LoadCatalogInfo loads a catalog info from a reader.
   125  func LoadCatalogInfo(r io.Reader) (*CatalogInfo, error) {
   126  	dec := yaml.NewDecoder(r)
   127  	var nodes []*yaml.Node
   128  	for {
   129  		var node yaml.Node
   130  		err := dec.Decode(&node)
   131  		if err == io.EOF {
   132  			break
   133  		} else if err != nil {
   134  			return nil, err
   135  		}
   136  		nodes = append(nodes, &node)
   137  	}
   138  	catalog := &CatalogInfo{}
   139  	vervetAPINames := map[string]struct{}{}
   140  	for _, node := range nodes {
   141  		if ok, err := isServiceComponent(node); err != nil {
   142  			return nil, err
   143  		} else if ok {
   144  			catalog.service = node
   145  			if err := node.Decode(&catalog.serviceComponent); err != nil {
   146  				return nil, err
   147  			}
   148  			continue
   149  		}
   150  		if ok, err := isVervetGenerated(node); err != nil {
   151  			return nil, err
   152  		} else {
   153  			if !ok {
   154  				catalog.components = append(catalog.components, node)
   155  			} else {
   156  				// Remove prior vervet API names from the service component so we can rebuild them
   157  				var api API
   158  				if err := node.Decode(&api); err != nil {
   159  					return nil, err
   160  				}
   161  				if api.Kind == "API" {
   162  					vervetAPINames[api.Metadata.Name] = struct{}{}
   163  				}
   164  			}
   165  		}
   166  	}
   167  	if catalog.service != nil {
   168  		var apiNames []string
   169  		for _, apiName := range catalog.serviceComponent.Spec.ProvidesAPIs {
   170  			if _, ok := vervetAPINames[apiName]; !ok {
   171  				apiNames = append(apiNames, apiName)
   172  			}
   173  		}
   174  		catalog.serviceComponent.Spec.ProvidesAPIs = apiNames
   175  	}
   176  	return catalog, nil
   177  }
   178  
   179  // LoadVervetAPIs loads all the compiled versioned OpenAPI specs and adds them
   180  // to the catalog as API components.
   181  func (c *CatalogInfo) LoadVervetAPIs(root, versions string) error {
   182  	root, err := filepath.Abs(root)
   183  	if err != nil {
   184  		return err
   185  	}
   186  	versions, err = filepath.Abs(versions)
   187  	if err != nil {
   188  		return err
   189  	}
   190  	specFiles, err := fs.Glob(os.DirFS(versions), "*/spec.json")
   191  	if err != nil {
   192  		return err
   193  	}
   194  
   195  	// Determine API names, combining existing + generated API entities.
   196  	apiUniqueNames := map[string]struct{}{}
   197  	for _, name := range c.serviceComponent.Spec.ProvidesAPIs {
   198  		apiUniqueNames[name] = struct{}{}
   199  	}
   200  	for _, specFile := range specFiles {
   201  		doc, err := vervet.NewDocumentFile(filepath.Join(versions, specFile))
   202  		if err != nil {
   203  			return err
   204  		}
   205  		api, err := c.vervetAPI(doc, root)
   206  		if err != nil {
   207  			return err
   208  		}
   209  		c.VervetAPIs = append(c.VervetAPIs, api)
   210  		apiUniqueNames[api.Metadata.Name] = struct{}{}
   211  	}
   212  	apiNames := []string{}
   213  	for name := range apiUniqueNames {
   214  		apiNames = append(apiNames, name)
   215  	}
   216  	sort.Strings(apiNames)
   217  
   218  	// Update the existing component providesApis with combined list of API
   219  	// names.
   220  	specPath, err := yamlpath.NewPath("$..spec")
   221  	if err != nil {
   222  		return err
   223  	}
   224  	specNodes, err := specPath.Find(c.service)
   225  	if err != nil {
   226  		return err
   227  	}
   228  	if len(specNodes) == 0 {
   229  		return errors.New("missing spec in Backstage service component")
   230  	}
   231  	providesApisPath, err := yamlpath.NewPath("$.providesApis")
   232  	if err != nil {
   233  		return err
   234  	}
   235  	providesApisNodes, err := providesApisPath.Find(specNodes[0])
   236  	if err != nil {
   237  		return err
   238  	}
   239  	if len(providesApisNodes) == 0 {
   240  		providesApisNodes = []*yaml.Node{{Kind: yaml.SequenceNode}}
   241  		specNodes[0].Content = append(specNodes[0].Content,
   242  			&yaml.Node{Kind: yaml.ScalarNode, Value: "providesApis"},
   243  			providesApisNodes[0],
   244  		)
   245  	}
   246  	err = providesApisNodes[0].Encode(apiNames)
   247  	if err != nil {
   248  		return err
   249  	}
   250  	c.serviceComponent.Spec.ProvidesAPIs = apiNames
   251  	return nil
   252  }
   253  
   254  // vervetAPI adds an OpenAPI spec document to the catalog.
   255  func (c *CatalogInfo) vervetAPI(doc *vervet.Document, root string) (*API, error) {
   256  	version, err := doc.Version()
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  	lifecycle := version.LifecycleAt(time.Time{})
   261  	ref, err := filepath.Rel(root, doc.Location().String())
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  	var backstageLifecycle string
   266  	if lifecycle == vervet.LifecycleReleased {
   267  		backstageLifecycle = version.Stability.String()
   268  	} else {
   269  		backstageLifecycle = lifecycle.String()
   270  	}
   271  	return &API{
   272  		APIVersion: backstageVersion,
   273  		Kind:       "API",
   274  		Metadata: Metadata{
   275  			Name:        toBackstageName(doc.Info.Title) + "_" + version.DateString() + "_" + version.Stability.String(),
   276  			Title:       doc.Info.Title + " " + version.DateString() + " " + version.Stability.String(),
   277  			Description: doc.Info.Description,
   278  			Labels: map[string]string{
   279  				w3securityApiVersionDate: version.DateString(),
   280  				w3securityApiStability:   version.Stability.String(),
   281  				w3securityApiLifecycle:   lifecycle.String(),
   282  			},
   283  			Tags: []string{
   284  				version.Date.Format("2006-01"),
   285  				version.Stability.String(),
   286  				lifecycle.String(),
   287  			},
   288  			Annotations: map[string]string{
   289  				w3securityApiGeneratedBy: "vervet",
   290  			},
   291  		},
   292  		Spec: APISpec{
   293  			Type:      "openapi",
   294  			Lifecycle: backstageLifecycle,
   295  			Owner:     c.serviceComponent.Spec.Owner,
   296  			Definition: DefinitionRef{
   297  				Text: ref,
   298  			},
   299  		},
   300  	}, nil
   301  }
   302  
   303  func toBackstageName(s string) string {
   304  	return strings.Map(func(r rune) rune {
   305  		if r >= '0' && r <= '9' {
   306  			return r
   307  		}
   308  		if r >= 'A' && r <= 'Z' {
   309  			return r
   310  		}
   311  		if r >= 'a' && r <= 'z' {
   312  			return r
   313  		}
   314  		if r == ' ' || r == '_' || r == '-' {
   315  			return '-'
   316  		}
   317  		return -1
   318  	}, strings.TrimSpace(s))
   319  }
   320  
   321  // isServiceComponent returns whether the YAML node is a Backstage component
   322  // document for a service.
   323  func isServiceComponent(node *yaml.Node) (bool, error) {
   324  	var doc Component
   325  	if err := node.Decode(&doc); err != nil {
   326  		return false, err
   327  	}
   328  	return doc.Kind == "Component", nil
   329  }
   330  
   331  // isVervetGenerated returns whether the YAML node is a Backstage entity
   332  // document that was generated by Vervet.
   333  func isVervetGenerated(node *yaml.Node) (bool, error) {
   334  	var comp Component
   335  	if err := node.Decode(&comp); err != nil {
   336  		return false, err
   337  	}
   338  	return comp.Metadata.Annotations[w3securityApiGeneratedBy] == "vervet", nil
   339  }