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 }