Merge branch 'backstage:master' into Extend-microsoft-auth-provider

This commit is contained in:
Daniel Doberenz
2024-01-17 14:59:00 +01:00
committed by GitHub
2068 changed files with 107446 additions and 21932 deletions
@@ -0,0 +1,429 @@
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="897px" height="646px" viewBox="-0.5 -0.5 897 646" content="&lt;mxfile scale=&quot;1.5&quot; border=&quot;20&quot;&gt;&lt;diagram id=&quot;gKBvn7eBFHudv9VMIRD-&quot; name=&quot;Page-1&quot;&gt;7V1tb6M4EP41kXZPahVwIPCxabe3J3V11fWku/3oECdBS3HOkG2zv/5ssHkzJIQQ0xCyUjcM2JiZeWbG4zEZgfvX998J3Ky/4QXyRvp48T4CDyNdN0xA/zLCLibYUzMmrIi7iElaSnhxfyFOHHPq1l2gIHdhiLEXups80cG+j5wwR4OE4Lf8ZUvs5e+6gSskEV4c6MnUf9xFuI6pYDJO6V+Ru1qLO1v8xBw6P1YEb31+u5EOltEnPv0KRVf8+mANF/gtQwJfRuCeYBzG317f75HHOCu4Frd7rDibDJsgP6zTwOAtfkJvi8SQo4GFO8GL6HEQazAegdnb2g3RywY67OwbFT6lrcNXjx5p9OvS9bx77GEStQVLg/2j9CAk+AfKnDGjD2uB/TBDjz+UzgeGSIjeK59OS3hGNRHhVxSSHb1ENNCNuAnXwhshv7dUptqY09YZeYrrIFejVdJ1ykv6hbOzgrV6r1mrdcjaid1n1t4I49CJ1o57zVrQIWsnVq9Zq5ld8nbab95Ou+TtWGIlWtBoiR/62Ed53qF3N/yXsflWN/jhd8519v3hPXuwEwc+HVbcyhCH30WH7CBtFh2l7RZ3LOCjh3MPOz/+Xrt+TH50PTGiIIQk5FHmJD6bOTpe1jFDGBf2S5QyDW+Jw6/iKknHskL8KlAheII8GLo/892fJEStsRCPkeFFy0JXJQvZNtWVxXQA1AEhThUJUY7uZ3TuR/lAiQ9o4+HdKxu6Pr77WI6ohKWSb9rjiKwuHZHE8kdCnzXm+d1m0ydGFwJVzZD5bJ2JzSWqfay9OqfpyZuaoiHKi5wx3HWgd+e5K9rTw6u7WHjRYDFxf1HxwUT69IB3Yzf0fk2t3gEjN5GNnEhrFOzlCVaPN33GbmSzxCV4uQzoTTPKk17DNRUUNLWogfGj8EZ7+ilMerViP/HzS/1Eypw8cT39tveqMybhGq+wD70vKfWSw1tnS35GiGhm5Rq6ZqNEbRW5ZsOQDNhddAEO14iwQRG4XLpOqRo8wTny8gKH3Ho49IFp+z1mhaDA/QXnUX9MehumrtGTGLOR8bBvNsgz2bzxKMFDls+x5lY7jfGtZml8JtwU+omnz+f39AIcZcvQAIlg/8zyCCReRlz8cZBoqoKiHLPRJnDjsh6msxA56wV2gpF+HzgUk9hbUIBN6WWPv10cOsG4nKm8J+ourCnIgepGawerZt512meAqi7b1E93W2pPfcbr0PVXn+nZvxDlXYDot2eC33eSCC84BTfJxyeWynmPsddMDgm42qbQlE0hMBSZwonZWIqX4d06laIqhyZufjjvc98j65eYtk7SPmAwf+0AZ1oCHKDK/E0G83c+KU5UmT95nbXC/M16ZP6KwZ9S82fujxvKJ8m5meb1rOoBUSuYwYapChvi5hlswI+2KHFqYVIBB4nSZ4AwKcEBaAMH0wEHtXGgyzgwLFU40GUcbKl29AgIyXJcJ0Bosn5xrUAAJQ6hIk/XPhCABAQHhtDDqz5hoQ4UjDNBYdpkAeFaoVCykGyqKj8D8jpAgCBx+uQVimXbSoGwv7xvAEIWCCULYqauCgjyOkq6CNYbKNg1oGCeCwr6AIXaUChJpZuqMoJATqVnFoH7AwapeF8lGkxrQENtNJRkVo2Kkpv20SBnVjd9W0OXlpFUTpuBvP3q2duuXF92vMEabthXigNvNyMswR0e5nUqGHbksUqVZxy4oYtZFYqHlqwPUZzyVDidFKmIchYSP311NcsCBuvkbm1IRy9ujtM0WTqgRDpGG9KpsaVzEEtuwn1AKmYLUjFkF93AnVx9ZfjHqfs7dXdMoQDtiDrxYn23VVDPpnXiRdfQXp24UbanNK1dTFJ59zyTcaFli0ZF6UxStjixRMZ5l+u8faVpICS78/iWHjwj4tIxMzGeWIVAj+TOLiaANizZ5Fiqll+NGtvrkwhi57nUGRBwOHyYx27jaZ4Qkle8/LkNaTdIiIDz+9aYahPd4n+tffyvirUPyeXEKLxYzaCJDROZiEI/UxRud754NeA1xavdJV6PifivGa9SMlUhXJMJ+oDXD4BXMaXL4VXVhndx8wGvh7ZuF96MpRavnS8EDnhN8VpSjqgOr3I54oDXErx2C9cm+bQBrmeCqy7D1VZVLCZuPsD1wGqq1mk4vH8vUld4vRyInVpcUJ4Ult49KKWX20sLm3JV5wDUMqB2OW+1O980M/jVFPQllafK0kxm2RsoBriWpIW7xKspl0VWS+m6l/8TX3dg/b+NvYfTMl9neowDwQb6OfmY/23ZW+tnuW8r9v/XZEndY08WvWZ/IfqhQ4i7iq8V5Dkp7XyOCcXnjRNj4C4aGvl0c5Olfy4dRINxp/tZg8rRFnSUCjrM618etHybcxb5nFRbw8r0PV/b0IbageIiEZCNQRKKt17CK9vsJ7xi3KDEBxjCOQyQXLbVX+aXmeKzcd+uEeAyT7epfNQ6ZQfHLlLq43reSJS9nMIBa4gZwqwuyGISlSCyKyqTidaGTI6JEK5ZJvVg0opIalQHXrdIFAjBrmGrzmatOzHONX7q4Uq0rmIO3YFxHmqv6slEoXEeymsOiESFca4RtJw7lJbeVaHSWIuc0qCFdsV7JtUba3soDKgnE3XG2tYHkewXiQIhaONjpv3XnYTWNf1Whse50tDaWPajD7NvL5Tyhx+E0HeuKhuoj6clzG8rHzhiv6ojfn82Xh9Pf+IXfPkf&lt;/diagram&gt;&lt;/mxfile&gt;" style="background-color: rgb(255, 255, 255);">
<defs/>
<g>
<rect x="717" y="279" width="150" height="60" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<rect x="702" y="279" width="150" height="60" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<rect x="499.5" y="279" width="150" height="60" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<rect x="484.5" y="279" width="150" height="60" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<rect x="289.5" y="279" width="150" height="60" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<rect x="274.5" y="279" width="150" height="60" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<path d="M 297 279 L 265.37 241.05" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 260.57 235.29 L 267.3 239.45 L 263.45 242.65 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 334.5 279 L 334.5 243.18" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 334.5 235.68 L 337 243.18 L 332 243.18 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 372 279 L 403.63 241.05" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 408.43 235.29 L 405.55 242.65 L 401.7 239.45 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<rect x="259.5" y="279" width="150" height="60" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 206px; margin-left: 174px;">
<div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Backend Deployment A
</div>
</div>
</div>
</foreignObject>
<text x="223" y="210" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
Backend Deployme...
</text>
</switch>
</g>
<rect x="484.5" y="564" width="120" height="60" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 396px; margin-left: 324px;">
<div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Frontend App
</div>
</div>
</div>
</foreignObject>
<text x="363" y="400" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
Frontend App
</text>
</switch>
</g>
<path d="M 544.5 564 L 544.5 528.18" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 544.5 520.68 L 547 528.18 L 542 528.18 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 507 459 Q 507 399 420.75 399 Q 334.5 399 334.5 348.18" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 334.5 340.68 L 337 348.18 L 332 348.18 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 286px; margin-left: 284px;">
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
All other traffic
</div>
</div>
</div>
</foreignObject>
<text x="284" y="290" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">
All other traffic
</text>
</switch>
</g>
<path d="M 582 459 Q 582 399 668.25 399 Q 754.5 399 754.5 348.18" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 754.5 340.68 L 757 348.18 L 752 348.18 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 286px; margin-left: 468px;">
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
/api/{techdocs,scaffolder}/*
</div>
</div>
</div>
</foreignObject>
<text x="468" y="290" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">
/api/{techdocs,scaffolder}/*
</text>
</switch>
</g>
<rect x="469.5" y="459" width="150" height="60" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 326px; margin-left: 314px;">
<div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
(Authenticating) Reverse Proxy
</div>
</div>
</div>
</foreignObject>
<text x="363" y="330" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
(Authenticating)...
</text>
</switch>
</g>
<path d="M 717 279 L 711.01 243.05" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 709.78 235.65 L 713.47 242.64 L 708.54 243.46 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 792 279 L 823.63 241.05" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 828.43 235.29 L 825.55 242.65 L 821.7 239.45 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<rect x="679.5" y="279" width="150" height="60" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 206px; margin-left: 454px;">
<div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Backend Deployment C
</div>
</div>
</div>
</foreignObject>
<text x="503" y="210" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
Backend Deployme...
</text>
</switch>
</g>
<path d="M 507 279 L 494.9 242.71" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 492.53 235.59 L 497.27 241.92 L 492.53 243.5 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 582 279 L 594.1 242.71" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 596.47 235.59 L 596.47 243.5 L 591.73 241.92 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<rect x="469.5" y="279" width="150" height="60" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 206px; margin-left: 314px;">
<div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Backend Deployment B
</div>
</div>
</div>
</foreignObject>
<text x="363" y="210" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
Backend Deployme...
</text>
</switch>
</g>
<path d="M 259.5 189 Q 259.5 189 259.5 160.68" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 259.5 153.18 L 262 160.68 L 257 160.68 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<rect x="229.5" y="189" width="60" height="45" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 141px; margin-left: 154px;">
<div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
app
</div>
</div>
</div>
</foreignObject>
<text x="173" y="145" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
app
</text>
</switch>
</g>
<path d="M 334.5 189 Q 334.5 189 334.5 160.68" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 334.5 153.18 L 337 160.68 L 332 160.68 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<rect x="304.5" y="189" width="60" height="45" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 141px; margin-left: 204px;">
<div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
auth
</div>
</div>
</div>
</foreignObject>
<text x="223" y="145" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
auth
</text>
</switch>
</g>
<path d="M 492 189 Q 492 189 492 160.68" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 492 153.18 L 494.5 160.68 L 489.5 160.68 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<rect x="454.5" y="189" width="75" height="45" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 141px; margin-left: 304px;">
<div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
catalog
</div>
</div>
</div>
</foreignObject>
<text x="328" y="145" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
catalog
</text>
</switch>
</g>
<path d="M 597 189 Q 597 189 597 160.68" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 597 153.18 L 599.5 160.68 L 594.5 160.68 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<rect x="559.5" y="189" width="75" height="45" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 48px; height: 1px; padding-top: 141px; margin-left: 374px;">
<div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
search
</div>
</div>
</div>
</foreignObject>
<text x="398" y="145" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
search
</text>
</switch>
</g>
<path d="M 709.5 189 Q 709.5 189 709.5 160.68" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 709.5 153.18 L 712 160.68 L 707 160.68 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<rect x="664.5" y="189" width="90" height="45" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 141px; margin-left: 444px;">
<div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
techdocs
</div>
</div>
</div>
</foreignObject>
<text x="473" y="145" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
techdocs
</text>
</switch>
</g>
<path d="M 829.5 189 Q 829.5 189 829.5 160.68" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 829.5 153.18 L 832 160.68 L 827 160.68 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<rect x="784.5" y="189" width="90" height="45" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 141px; margin-left: 524px;">
<div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
scaffolder
</div>
</div>
</div>
</foreignObject>
<text x="553" y="145" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
scaffolder
</text>
</switch>
</g>
<path d="M 409.5 189 Q 409.5 189 409.5 160.68" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 409.5 153.18 L 412 160.68 L 407 160.68 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<rect x="379.5" y="189" width="60" height="45" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 141px; margin-left: 254px;">
<div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
proxy
</div>
</div>
</div>
</foreignObject>
<text x="273" y="145" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
proxy
</text>
</switch>
</g>
<path d="M 229.5 174 L 218.25 174 Q 207 174 207 189 L 207 200.25 Q 207 211.5 195.75 211.5 L 190.13 211.5 Q 184.5 211.5 195.75 211.5 L 201.38 211.5 Q 207 211.5 207 226.5 L 207 237.75 Q 207 249 218.25 249 L 229.5 249" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" stroke-dasharray="4.5 4.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-end; width: 32px; height: 1px; padding-top: 141px; margin-left: 89px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: right;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Plugins
</div>
</div>
</div>
</foreignObject>
<text x="121" y="145" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="end">
Plugi...
</text>
</switch>
</g>
<path d="M 229.5 264 L 218.25 264 Q 207 264 207 279 L 207 294 Q 207 309 195.75 309 L 190.13 309 Q 184.5 309 195.75 309 L 201.38 309 Q 207 309 207 324 L 207 339 Q 207 354 218.25 354 L 229.5 354" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" stroke-dasharray="4.5 4.5" pointer-events="all"/>
<path d="M 544.5 459 Q 544.5 459 544.5 348.18" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 544.5 340.68 L 547 348.18 L 542 348.18 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 247px; margin-left: 362px;">
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
/api/{catalog,search}/*
</div>
</div>
</div>
</foreignObject>
<text x="362" y="250" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">
/api/{catalog,search}/*
</text>
</switch>
</g>
<path d="M 334.5 106.5 Q 334.5 106.5 334.5 78.18" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 334.5 70.68 L 337 78.18 L 332 78.18 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 319.5 113.36 C 319.5 109.57 326.22 106.5 334.5 106.5 C 338.48 106.5 342.29 107.22 345.11 108.51 C 347.92 109.79 349.5 111.54 349.5 113.36 L 349.5 144.64 C 349.5 148.43 342.78 151.5 334.5 151.5 C 326.22 151.5 319.5 148.43 319.5 144.64 Z" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 349.5 113.36 C 349.5 117.14 342.78 120.21 334.5 120.21 C 326.22 120.21 319.5 117.14 319.5 113.36" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 409.5 106.5 Q 409.5 87.78 372 87.78 Q 334.5 87.78 334.5 78.18" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 334.5 70.68 L 337 78.18 L 332 78.18 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 394.5 113.36 C 394.5 109.57 401.22 106.5 409.5 106.5 C 413.48 106.5 417.29 107.22 420.11 108.51 C 422.92 109.79 424.5 111.54 424.5 113.36 L 424.5 144.64 C 424.5 148.43 417.78 151.5 409.5 151.5 C 401.22 151.5 394.5 148.43 394.5 144.64 Z" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 424.5 113.36 C 424.5 117.14 417.78 120.21 409.5 120.21 C 401.22 120.21 394.5 117.14 394.5 113.36" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 492 106.5 Q 492 87.78 518.25 87.78 Q 544.5 87.78 544.5 78.18" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 544.5 70.68 L 547 78.18 L 542 78.18 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 477 113.36 C 477 109.57 483.72 106.5 492 106.5 C 495.98 106.5 499.79 107.22 502.61 108.51 C 505.42 109.79 507 111.54 507 113.36 L 507 144.64 C 507 148.43 500.28 151.5 492 151.5 C 483.72 151.5 477 148.43 477 144.64 Z" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 507 113.36 C 507 117.14 500.28 120.21 492 120.21 C 483.72 120.21 477 117.14 477 113.36" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 597 106.5 Q 597 87.78 570.75 87.78 Q 544.5 87.78 544.5 78.18" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 544.5 70.68 L 547 78.18 L 542 78.18 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 582 113.36 C 582 109.57 588.72 106.5 597 106.5 C 600.98 106.5 604.79 107.22 607.61 108.51 C 610.42 109.79 612 111.54 612 113.36 L 612 144.64 C 612 148.43 605.28 151.5 597 151.5 C 588.72 151.5 582 148.43 582 144.64 Z" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 612 113.36 C 612 117.14 605.28 120.21 597 120.21 C 588.72 120.21 582 117.14 582 113.36" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 709.5 106.5 Q 709.5 87.78 739.5 87.78 Q 769.5 87.78 769.5 78.18" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 769.5 70.68 L 772 78.18 L 767 78.18 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 694.5 113.36 C 694.5 109.57 701.22 106.5 709.5 106.5 C 713.48 106.5 717.29 107.22 720.11 108.51 C 722.92 109.79 724.5 111.54 724.5 113.36 L 724.5 144.64 C 724.5 148.43 717.78 151.5 709.5 151.5 C 701.22 151.5 694.5 148.43 694.5 144.64 Z" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 724.5 113.36 C 724.5 117.14 717.78 120.21 709.5 120.21 C 701.22 120.21 694.5 117.14 694.5 113.36" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 829.5 106.5 Q 829.5 87.78 799.5 87.78 Q 769.5 87.78 769.5 78.18" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 769.5 70.68 L 772 78.18 L 767 78.18 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 814.5 113.36 C 814.5 109.57 821.22 106.5 829.5 106.5 C 833.48 106.5 837.29 107.22 840.11 108.51 C 842.92 109.79 844.5 111.54 844.5 113.36 L 844.5 144.64 C 844.5 148.43 837.78 151.5 829.5 151.5 C 821.22 151.5 814.5 148.43 814.5 144.64 Z" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 844.5 113.36 C 844.5 117.14 837.78 120.21 829.5 120.21 C 821.22 120.21 814.5 117.14 814.5 113.36" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 259.5 106.5 Q 259.5 87.78 297 87.78 Q 334.5 87.78 334.5 78.18" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 334.5 70.68 L 337 78.18 L 332 78.18 Z" fill="#666666" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 244.5 113.36 C 244.5 109.57 251.22 106.5 259.5 106.5 C 263.48 106.5 267.29 107.22 270.11 108.51 C 272.92 109.79 274.5 111.54 274.5 113.36 L 274.5 144.64 C 274.5 148.43 267.78 151.5 259.5 151.5 C 251.22 151.5 244.5 148.43 244.5 144.64 Z" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 274.5 113.36 C 274.5 117.14 267.78 120.21 259.5 120.21 C 251.22 120.21 244.5 117.14 244.5 113.36" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 229.5 99 L 218.25 99 Q 207 99 207 114 L 207 121.5 Q 207 129 195.75 129 L 190.13 129 Q 184.5 129 195.75 129 L 201.38 129 Q 207 129 207 144 L 207 151.5 Q 207 159 218.25 159 L 229.5 159" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" stroke-dasharray="4.5 4.5" pointer-events="all"/>
<rect x="19.5" y="286.5" width="165" height="45" fill="none" stroke="none" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-end; width: 108px; height: 1px; padding-top: 206px; margin-left: 13px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: right;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
<span style="">
Horizontally Scaled
</span>
<br style="border-color: var(--border-color);"/>
<span style="">
Deployments
</span>
</div>
</div>
</div>
</foreignObject>
<text x="121" y="210" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="end">
Horizontally Scale...
</text>
</switch>
</g>
<rect x="19.5" y="106.5" width="165" height="45" fill="none" stroke="none" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-end; width: 108px; height: 1px; padding-top: 86px; margin-left: 13px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: right;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Logical Databases
</div>
</div>
</div>
</foreignObject>
<text x="121" y="90" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="end">
Logical Databases
</text>
</switch>
</g>
<path d="M 319.5 53.36 C 319.5 49.57 326.22 46.5 334.5 46.5 C 338.48 46.5 342.29 47.22 345.11 48.51 C 347.92 49.79 349.5 51.54 349.5 53.36 L 349.5 62.14 C 349.5 65.93 342.78 69 334.5 69 C 326.22 69 319.5 65.93 319.5 62.14 Z" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 349.5 53.36 C 349.5 57.14 342.78 60.21 334.5 60.21 C 326.22 60.21 319.5 57.14 319.5 53.36" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 319.5 45.86 C 319.5 42.07 326.22 39 334.5 39 C 338.48 39 342.29 39.72 345.11 41.01 C 347.92 42.29 349.5 44.04 349.5 45.86 L 349.5 54.64 C 349.5 58.43 342.78 61.5 334.5 61.5 C 326.22 61.5 319.5 58.43 319.5 54.64 Z" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 349.5 45.86 C 349.5 49.64 342.78 52.71 334.5 52.71 C 326.22 52.71 319.5 49.64 319.5 45.86" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 319.5 38.36 C 319.5 34.57 326.22 31.5 334.5 31.5 C 338.48 31.5 342.29 32.22 345.11 33.51 C 347.92 34.79 349.5 36.54 349.5 38.36 L 349.5 47.14 C 349.5 50.93 342.78 54 334.5 54 C 326.22 54 319.5 50.93 319.5 47.14 Z" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 349.5 38.36 C 349.5 42.14 342.78 45.21 334.5 45.21 C 326.22 45.21 319.5 42.14 319.5 38.36" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 529.5 53.36 C 529.5 49.57 536.22 46.5 544.5 46.5 C 548.48 46.5 552.29 47.22 555.11 48.51 C 557.92 49.79 559.5 51.54 559.5 53.36 L 559.5 62.14 C 559.5 65.93 552.78 69 544.5 69 C 536.22 69 529.5 65.93 529.5 62.14 Z" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 559.5 53.36 C 559.5 57.14 552.78 60.21 544.5 60.21 C 536.22 60.21 529.5 57.14 529.5 53.36" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 529.5 45.86 C 529.5 42.07 536.22 39 544.5 39 C 548.48 39 552.29 39.72 555.11 41.01 C 557.92 42.29 559.5 44.04 559.5 45.86 L 559.5 54.64 C 559.5 58.43 552.78 61.5 544.5 61.5 C 536.22 61.5 529.5 58.43 529.5 54.64 Z" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 559.5 45.86 C 559.5 49.64 552.78 52.71 544.5 52.71 C 536.22 52.71 529.5 49.64 529.5 45.86" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 529.5 38.36 C 529.5 34.57 536.22 31.5 544.5 31.5 C 548.48 31.5 552.29 32.22 555.11 33.51 C 557.92 34.79 559.5 36.54 559.5 38.36 L 559.5 47.14 C 559.5 50.93 552.78 54 544.5 54 C 536.22 54 529.5 50.93 529.5 47.14 Z" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 559.5 38.36 C 559.5 42.14 552.78 45.21 544.5 45.21 C 536.22 45.21 529.5 42.14 529.5 38.36" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 754.5 53.36 C 754.5 49.57 761.22 46.5 769.5 46.5 C 773.48 46.5 777.29 47.22 780.11 48.51 C 782.92 49.79 784.5 51.54 784.5 53.36 L 784.5 62.14 C 784.5 65.93 777.78 69 769.5 69 C 761.22 69 754.5 65.93 754.5 62.14 Z" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 784.5 53.36 C 784.5 57.14 777.78 60.21 769.5 60.21 C 761.22 60.21 754.5 57.14 754.5 53.36" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 754.5 45.86 C 754.5 42.07 761.22 39 769.5 39 C 773.48 39 777.29 39.72 780.11 41.01 C 782.92 42.29 784.5 44.04 784.5 45.86 L 784.5 54.64 C 784.5 58.43 777.78 61.5 769.5 61.5 C 761.22 61.5 754.5 58.43 754.5 54.64 Z" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 784.5 45.86 C 784.5 49.64 777.78 52.71 769.5 52.71 C 761.22 52.71 754.5 49.64 754.5 45.86" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 754.5 38.36 C 754.5 34.57 761.22 31.5 769.5 31.5 C 773.48 31.5 777.29 32.22 780.11 33.51 C 782.92 34.79 784.5 36.54 784.5 38.36 L 784.5 47.14 C 784.5 50.93 777.78 54 769.5 54 C 761.22 54 754.5 50.93 754.5 47.14 Z" fill="#f5f5f5" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 784.5 38.36 C 784.5 42.14 777.78 45.21 769.5 45.21 C 761.22 45.21 754.5 42.14 754.5 38.36" fill="none" stroke="#666666" stroke-width="1.5" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 229.5 20.25 L 218.25 20.25 Q 207 20.25 207 35.25 L 207 42.75 Q 207 50.25 195.75 50.25 L 190.13 50.25 Q 184.5 50.25 195.75 50.25 L 201.38 50.25 Q 207 50.25 207 65.25 L 207 72.75 Q 207 80.25 218.25 80.25 L 229.5 80.25" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" stroke-dasharray="4.5 4.5" pointer-events="none"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-end; width: 108px; height: 1px; padding-top: 34px; margin-left: 13px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: right;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: none; white-space: normal; overflow-wrap: normal;">
DBMS Instances
</div>
</div>
</div>
</foreignObject>
<text x="121" y="37" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="end">
DBMS Instances
</text>
</switch>
</g>
</g>
<switch>
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
Text is not SVG - cannot display
</text>
</a>
</switch>
</svg>

After

Width:  |  Height:  |  Size: 46 KiB

@@ -0,0 +1,314 @@
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="732px" height="462px" viewBox="-0.5 -0.5 732 462" content="&lt;mxfile scale=&quot;1.5&quot; border=&quot;20&quot;&gt;&lt;diagram id=&quot;gKBvn7eBFHudv9VMIRD-&quot; name=&quot;Page-1&quot;&gt;7VtZc6M4EP41foxL3PBoO8nMVu2RqtmtnTwqINuqYMTK8jW/fgVIgBC+wUlm4qmaQg1qpD6+PkQG1mSx/UJhOv+DRCgemCDaDqz7gWnajsP/zwi7guAaQUGYURwVJKMifMM/kCACQV3hCC2VBxkhMcOpSgxJkqCQKTRIKdmoj01JrL41hTOkEb6FMNap/+KIzcW2fFDRvyI8m8s3O+LGCwxfZ5SsEvG6gWlN819xewElK/H8cg4jsqmRrIeBNaGEsOJqsZ2gOJOslFox73HP3XLZFCXslAlmMWEN45XY+ShNxdLYTkoj3xDKpoCBNd7MMUPfUhhmdzdc/Zw2Z4uYjwx+OcVxPCExoflc68HN/nH6klHyimp33PyXzSAJq9GLH6frmxH7WyPK0LZGEpv7gsgCMbrjj2xVe9qpw02lU1Oqbl7Tpy1oUJjRrGRcyZJfCHG2i9bVZasJFkXc6sQwIQlqSJLL5REucJyt/iuK14jhEIobwmf8Q3JakhUNxats4UWQzpDcpfDJbBUHhUlRDBleq75xlWxsTTYpJevc59tk9Dt84fiiCAfGeJbw65BvGnHLGWdGwcUTj8SNBY6ijMeYoiX+AV9yfpn9pgQnLF+8Mx4497o898pdF3Sp5KZFltAkXqz4d5ul3oEhAJ6tmOudYHWy/AX3p2yHNdaOytVTGZDpdMlNoqm+coknaVRX6AQyGJMZJz7FqxlOBqYbc6mNXyi/mrFcnAUlk7eic/e/FZE37pa5Ykb8AQOk22KauJ8hOwzVORNu85gbhAn+RJv6o8U702It1iiUyyvWwPdXLEMurUMIBMCHALRBIACOl99pQKCIGZ1AoNOAQE/HwKAvCASaHA8jHt8l3X3PhDw0Azl+5uPMOyxJuN8KNRSjnRidApdGOe0JUcw3lEFHPvskELXcW6BoPnVEKdzVHhCotdfJNT0boKGqguOlDu5Kw6uHs/P819fct3A2itKYu9JSRwi8SGO04BrhgiMHEOSY9942hhgHrbEliIAOg4gdeGYnUcNoRA3D7iFsSIeqWdXDlqFkmasb/MVFQluzgo+bkjb8NLghHFstLvyGGamng6kUzhuDaRkkhZJMr2MwdTRNfND8V5pUJ/lvINldm/BaKnS5PSCXtN6aCv8mEXmv6S7L1/aL5brl+AbgavuaPYxh+LpkWafJBI+U7xolUX4JF2hD6OtPFNRM/2ijxehL8p5zZhRDW8y+166f84LDEaOqusgGsrhoDUvHQ1zQRfTS8M1ygqHv2jZwPS9wXRCoqRqX9NBwTMc2Lc/0fcNsFPxFwBU8rwPBoCWjuBrlCliq+geTGKOc2T8Mx5hlAhw9/XYUYM/FSphivqQCMYeiPTBcIrrGnM8+6OwfUY0XaCCzHVHdh9Fjv4hqguPtg5LWtWdL+O7DvESwvqFJZSF4mFE+bWl/eO7PlvRK88ywoaTFquJbekwixvCs1q+FGWMIDseZbNBsTVVdsXpLTESsAw2xvSpT2letka37kKWq3mlotAiaWkjS2DSbXMA/KbSdW/9Z7a/Zt6rm4x1Xi46eWn6a7qAqwW5su7Kzeq3xlnx7Nl73LOOV3cjOjNf8NN620sDU7Vk2Q9+4+bXH3DsziLYu5LUHCXpmVvWttKwMJ7wqj+PlqRnXW58eNG1et6/SybpowxlG4CkW0NExdPNAwXRUDp205Vy9LddZyfCIWDi/Uc0QEoqG0+KFv2i5YDU+3mk5KTGsnqoFt62xcXnUOnBUUgUsGaCeB1Vrak+wqo7Jy1j0rISiKwKTbGTWI5N7q0wraOYuF6ZadrNQaDLqrgfm6U2KD3qWI23+PX/LZDRCUx+H0l7bpw43dH5wkfMbXTi/dJsbfOCiO7+vJgj+hb7f5KPVax36vl7cRChFScRdH2RfMHwk//f2Rej35P9yhqxW+3D/m30mcclBVM393cAYKIVpEBwrTS//9E2iwFukBeURX7NxfC40aIxAPy0Yu3lqEnRbQ0vf+jlwp9NvSG6DO3dGo/F8CfLwYfWXHsXj1R/TWA//Aw==&lt;/diagram&gt;&lt;/mxfile&gt;" style="background-color: rgb(255, 255, 255);">
<defs/>
<g>
<rect x="19.5" y="19.5" width="375" height="60" fill="#e6e6e6" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 248px; height: 1px; padding-top: 33px; margin-left: 14px;">
<div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
App
</div>
</div>
</div>
</foreignObject>
<text x="138" y="37" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
App
</text>
</switch>
</g>
<path d="M 214.5 289.5 L 294.95 289.5" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 302.82 289.5 L 292.32 294.75 L 294.95 289.5 L 292.32 284.25 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 187px; margin-left: 168px;">
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 8px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
provides
</div>
</div>
</div>
</foreignObject>
<text x="168" y="190" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="8px" text-anchor="middle">
provides
</text>
</switch>
</g>
<rect x="79.5" y="259.5" width="135" height="60" fill="#008a00" stroke="#005700" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 193px; margin-left: 54px;">
<div data-drawio-colors="color: #ffffff; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(255, 255, 255); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Catalog Plugin
<br/>
<font face="Courier New" style="font-size: 10px;">
plugin:catalog
</font>
</div>
</div>
</div>
</foreignObject>
<text x="98" y="197" fill="#ffffff" font-family="Helvetica" font-size="12px" text-anchor="middle">
Catalog Plugin...
</text>
</switch>
</g>
<path d="M 214.5 169.5 L 364.5 169.5 Q 379.5 169.5 379.32 184.5 L 378.56 248.15" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 378.47 256.02 L 373.35 245.46 L 378.56 248.15 L 383.84 245.59 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 99px; margin-left: 201px;">
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 10px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
<font style="font-size: 8px;">
replaces
<br/>
implementation
<br/>
</font>
</div>
</div>
</div>
</foreignObject>
<text x="201" y="102" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="10px" text-anchor="middle">
replaces...
</text>
</switch>
</g>
<rect x="79.5" y="139.5" width="135" height="60" fill="#e6e6e6" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 113px; margin-left: 54px;">
<div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Extension Overrides
</div>
</div>
</div>
</foreignObject>
<text x="98" y="117" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
Extension Overr...
</text>
</switch>
</g>
<path d="M 214.5 409.5 L 244.5 409.5 Q 259.5 409.5 274.5 409.5 L 294.95 409.5" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 302.82 409.5 L 292.32 414.75 L 294.95 409.5 L 292.32 404.25 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 267px; margin-left: 167px;">
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 8px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
provides
</div>
</div>
</div>
</foreignObject>
<text x="167" y="270" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="8px" text-anchor="middle">
provides
</text>
</switch>
</g>
<rect x="79.5" y="379.5" width="135" height="60" fill="#008a00" stroke="#005700" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 273px; margin-left: 54px;">
<div data-drawio-colors="color: #ffffff; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(255, 255, 255); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Todo Plugin
<br/>
<font face="Courier New" style="font-size: 10px;">
plugin:todo
</font>
</div>
</div>
</div>
</foreignObject>
<text x="98" y="277" fill="#ffffff" font-family="Helvetica" font-size="12px" text-anchor="middle">
Todo Plugin...
</text>
</switch>
</g>
<rect x="424.5" y="19.5" width="285" height="60" fill="#e6e6e6" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 188px; height: 1px; padding-top: 33px; margin-left: 284px;">
<div data-drawio-colors="color: #333333; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(51, 51, 51); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Backstage Frontend Framework
</div>
</div>
</div>
</foreignObject>
<text x="378" y="37" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
Backstage Frontend Framework
</text>
</switch>
</g>
<path d="M 559.5 289.5 L 553.85 289.59" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 545.97 289.7 L 556.39 284.3 L 553.85 289.59 L 556.55 294.79 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<rect x="304.5" y="259.5" width="255" height="60" fill="#1ba1e2" stroke="#006eaf" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 168px; height: 1px; padding-top: 193px; margin-left: 204px;">
<div data-drawio-colors="color: #ffffff; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(255, 255, 255); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
<font style="font-size: 10px;">
Catalog Client Utility API
<br/>
<font face="Courier New">
api:plugin.catalog.service
</font>
</font>
</div>
</div>
</div>
</foreignObject>
<text x="288" y="197" fill="#ffffff" font-family="Helvetica" font-size="12px" text-anchor="middle">
Catalog Client Utility API...
</text>
</switch>
</g>
<rect x="304.5" y="379.5" width="255" height="60" fill="#1ba1e2" stroke="#006eaf" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 168px; height: 1px; padding-top: 273px; margin-left: 204px;">
<div data-drawio-colors="color: #ffffff; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(255, 255, 255); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
<font style="font-size: 10px;">
Todo Utility API
<br/>
<font face="Courier New">
api:plugin.todo.api
</font>
</font>
</div>
</div>
</div>
</foreignObject>
<text x="288" y="277" fill="#ffffff" font-family="Helvetica" font-size="12px" text-anchor="middle">
Todo Utility API...
</text>
</switch>
</g>
<path d="M 49.5 79.5 L 49.5 151.5 Q 49.5 166.5 49.5 181.5 L 49.5 244.5 Q 49.5 259.5 60.11 270.11 L 72.75 282.75" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 78.31 288.31 L 67.18 284.6 L 72.75 282.75 L 74.6 277.18 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 49.5 169.5 L 49.5 241.5 Q 49.5 256.5 49.5 271.5 L 49.5 379.5 Q 49.5 394.5 60.23 399.86 L 70.96 405.23" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 78 408.75 L 66.26 408.75 L 70.96 405.23 L 70.96 399.36 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 49.5 82.5 L 49.5 154.5 Q 49.5 169.5 59.72 169.5 L 69.95 169.5" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 77.82 169.5 L 67.32 174.75 L 69.95 169.5 L 67.32 164.25 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 64px; margin-left: 47px;">
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 10px; font-family: &quot;Courier New&quot;; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
<font face="Helvetica" style="font-size: 8px;">
installs
</font>
</div>
</div>
</div>
</foreignObject>
<text x="47" y="67" fill="rgb(0, 0, 0)" font-family="Courier New" font-size="10px" text-anchor="middle">
installs
</text>
</switch>
</g>
<rect x="469.5" y="139.5" width="195" height="60" fill="#1ba1e2" stroke="#006eaf" stroke-width="1.5" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 128px; height: 1px; padding-top: 113px; margin-left: 314px;">
<div data-drawio-colors="color: #ffffff; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(255, 255, 255); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
<font style="font-size: 10px;">
Fetch Utility API
<br/>
<font face="Courier New">
api:core.fetch
</font>
</font>
</div>
</div>
</div>
</foreignObject>
<text x="378" y="117" fill="#ffffff" font-family="Helvetica" font-size="12px" text-anchor="middle">
Fetch Utility API...
</text>
</switch>
</g>
<path d="M 567 79.5 L 567 129.95" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 567 137.82 L 561.75 127.32 L 567 129.95 L 572.25 127.32 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 69px; margin-left: 394px;">
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 8px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
provides
</div>
</div>
</div>
</foreignObject>
<text x="394" y="72" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="8px" text-anchor="middle">
provides
</text>
</switch>
</g>
<path d="M 432 379.5 L 432 329.05" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 432 321.18 L 437.25 331.68 L 432 329.05 L 426.75 331.68 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 235px; margin-left: 310px;">
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 8px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
depends on
</div>
</div>
</div>
</foreignObject>
<text x="310" y="238" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="8px" text-anchor="middle">
depends on
</text>
</switch>
</g>
<path d="M 559.5 289.5 L 589.5 289.5 Q 604.5 289.5 604.46 274.5 L 604.27 208.75" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 604.25 200.88 L 609.53 211.36 L 604.27 208.75 L 599.03 211.39 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)scale(1.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 160px; margin-left: 425px;">
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 8px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
depends on
</div>
</div>
</div>
</foreignObject>
<text x="425" y="163" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="8px" text-anchor="middle">
depends on
</text>
</switch>
</g>
</g>
<switch>
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
Text is not SVG - cannot display
</text>
</a>
</switch>
</svg>

After

Width:  |  Height:  |  Size: 27 KiB

+6
View File
@@ -236,6 +236,12 @@ export default async function createPlugin(
// an entity you will need to replace this step as well.
//
// You might also replace it if you for example want to filter out certain groups.
//
// Note that `getDefaultOwnershipEntityRefs` only includes groups to which the
// user has a direct MEMBER_OF relationship. It's perfectly fine to include
// groups that the user is transitively part of in the claims array, but the
// catalog doesn't currently provide a direct way of accessing this list of
// groups.
const ownershipRefs = getDefaultOwnershipEntityRefs(entity);
// The last step is to issue the token, where we might provide more options in the future.
+1
View File
@@ -371,6 +371,7 @@ createApiFactory({
configApi,
discoveryApi,
oauthRequestApi,
provider: { id: 'ghe', title: 'GitHub Enterprise', icon: () => null },
defaultScopes: ['read:user'],
environment: configApi.getOptionalString('auth.environment'),
}),
+6
View File
@@ -27,6 +27,8 @@ To add Okta authentication, you must create an Application from Okta:
- `Controlled access`: (select as appropriate)
- Click Save
The configuration examples provided above are suitable for local development. For a production deployment, substitute `http://localhost:7007` with the url that your Backstage instance is available at.
# Configuration
The provider configuration can then be added to your `app-config.yaml` under the
@@ -43,6 +45,8 @@ auth:
audience: ${AUTH_OKTA_DOMAIN}
authServerId: ${AUTH_OKTA_AUTH_SERVER_ID} # Optional
idp: ${AUTH_OKTA_IDP} # Optional
# https://developer.okta.com/docs/reference/api/oidc/#scope-dependent-claims-not-always-returned
additionalScopes: ${AUTH_OKTA_ADDITIONAL_SCOPES} # Optional
```
The values referenced are found on the Application page on your Okta site.
@@ -55,6 +59,8 @@ The values referenced are found on the Application page on your Okta site.
- `authServerId`: The authorization server ID for the Application
- `idp`: The identity provider for the application, e.g. `0oaulob4BFVa4zQvt0g3`
`additionalScopes` is an optional value, a string of space separated scopes, that will be combined with the default `scope` value of `openid profile email offline_access` to adjust the `scope` sent to Okta during OAuth. This will have an impact on [the dependent claims returned](https://developer.okta.com/docs/reference/api/oidc/#scope-dependent-claims-not-always-returned). For example, setting the `additionalScopes` value to `groups` will result in the claim returning a list of the groups that the user is a member of that also match the ID token group filter of the client app.
## Adding the provider to the Backstage frontend
To add the provider to the frontend, add the `oktaAuthApi` reference and
+1 -3
View File
@@ -6,8 +6,6 @@ sidebar_label: Overview
description: The structure and architecture of the new Backend System and its component parts
---
> **NOTE: The new backend system is in alpha, and some plugins do not yet fully implement it.**
## Building Blocks
This section introduces the high-level building blocks upon which this new
@@ -59,7 +57,7 @@ Just like plugins, modules also have access to services and can depend on their
A detailed explanation of the package architecture can be found in the
[Backstage Architecture
Overview](../../overview/architecture-overview/#package-architecture). The
Overview](../../overview/architecture-overview.md#package-architecture). The
most important packages to consider for this system are the following:
- `plugin-<pluginId>-backend` houses the implementation of the backend plugins
@@ -6,8 +6,6 @@ sidebar_label: Backend
description: Backend instances
---
> **NOTE: The new backend system is in alpha, and some plugins do not yet fully implement it.**
## The Backend Instance
This is the main entry point for creating a backend. It does not have any functionality in and of itself, but is simply responsible for wiring things together.
@@ -6,8 +6,6 @@ sidebar_label: Services
description: Services for backend plugins
---
> **NOTE: The new backend system is in alpha, and some plugins do not yet fully implement it.**
Backend services provide shared functionality available to all backend plugins and modules. They are made available through service references that embed a type that represents the service interface, similar to how [Utility APIs](../../api/utility-apis.md) work in the Backstage frontend system. To use a service in your plugin or module you request an implementation of that service using the service reference.
The system surrounding services exists to provide a level of indirection between the service interfaces and their implementation. It is an implementation of dependency injection, where each backend instance is the dependency injection container. The implementation for each service is provided by a service factory, which encapsulates the logic for how each service instance is created.
@@ -6,8 +6,6 @@ sidebar_label: Plugins
description: Backend plugins
---
> **NOTE: The new backend system is in alpha, and some plugins do not yet fully implement it.**
Plugins provide the actual base features of a Backstage backend. Each plugin operates completely independently of all other plugins and they only communicate with each other through network calls. This means that there is a strong degree of isolation between plugins, and that each plugin can be considered a separate microservice. While a default Backstage project has all plugins installed within a single backend, it is also possible to split this setup into multiple backends, with each backend housing one or more plugins.
## Defining a Plugin
@@ -6,8 +6,6 @@ sidebar_label: Extension Points
description: Extension points of backend plugins
---
> **NOTE: The new backend system is in alpha, and some plugins do not yet fully implement it.**
While plugins are able to use static configuration for lightweight forms of customization, you can quickly hit a limit where you need something more powerful to allow users to extend your plugin. For this purpose, the backend system provides a mechanism for plugins to provide extension points, which can be used to expose deeper customizations for your plugin. Extension points are used by modules, which are installed in the backend adjacent to plugins. Modules are covered more in-depth in the [next section](./06-modules.md).
Extension points are quite similar to services, in that they both encapsulate an interface in a reference object. The key difference is that extension points are registered and provided by plugins themselves, and do not have any factory associated with them. Extension points for a given plugin are also only accessible to modules that extend that same plugin.
@@ -70,7 +68,7 @@ Note that we create a closure that adds to a shared `actions` structure when `ad
## Module Extension Points
Just like plugins, modules can also provide their own extension points. The API for registering and using extension points is the same as for plugins. However, modules should typically only use extension points to allow for complex internal customizations by users of the plugin module. It is therefore preferred to export the extension point directly from the module package, rather than creating a separate node library for that purpose.
Just like plugins, modules can also provide their own extension points. The API for registering and using extension points is the same as for plugins. However, modules should typically only use extension points to allow for complex internal customizations by users of the plugin module. It is therefore preferred to export the extension point directly from the module package, rather than creating a separate node library for that purpose. Extension points exported by a module are used the same way as extension points exported by a plugin, you create your own separate module and declare a dependency on the extension point that you want to interact with.
## Extension Point Design
@@ -6,8 +6,6 @@ sidebar_label: Modules
description: Modules for backend plugins
---
> **NOTE: The new backend system is in alpha, and some plugins do not yet fully implement it.**
Backend modules are used to extend [plugins](./04-plugins.md) or sometimes other modules with additional features or change existing behavior. They must always be installed in the same backend instance as the plugin that they extend, and may only extend a single plugin. Modules interact with their target plugin using the [extension points](./05-extension-points.md) registered by the plugin, while also being able to depend on the [services](./03-services.md) of that plugin.
Both modules and plugins register an `init` method that is called during startup. In order to ensure that modules have registered all their extensions before the plugin starts up, all modules for each plugin are completely initialized before the plugin itself is initialized. In practice this means that all promises returned by each `init` method of the modules need to resolve before the plugin `init` method is called. This also means that it is not possible to further interact with the extension points once the `init` method has resolved.
@@ -25,8 +23,8 @@ import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'
import { MyCustomProcessor } from './MyCustomProcessor';
export const catalogModuleExampleCustomProcessor = createBackendModule({
moduleId: 'exampleCustomProcessor',
pluginId: 'catalog',
moduleId: 'example-custom-processor',
register(env) {
env.registerInit({
deps: {
@@ -6,22 +6,22 @@ sidebar_label: Naming Patterns
description: Naming patterns in the backend system
---
> **NOTE: The new backend system is in alpha, and some plugins do not yet fully implement it.**
These are the naming patterns to adhere to within the backend system. They help us keep exports consistent across packages and make it easier to understand the usage and intent of exports.
As a rule, all names should be camel case, with the exceptions of plugin and module IDs, which should be kebab case.
### Plugins
| Description | Pattern | Examples |
| ----------- | ------------ | ----------------------------------- |
| export | `<id>Plugin` | `catalogPlugin`, `scaffolderPlugin` |
| ID | `'<id>'` | `'catalog'`, `'scaffolder'` |
| Description | Pattern | Examples |
| ----------- | ----------------- | ------------------------------------- |
| export | `<camelId>Plugin` | `catalogPlugin`, `userSettingsPlugin` |
| ID | `'<kebab-id>'` | `'catalog'`, `'user-settings'` |
Example:
```ts
export const catalogPlugin = createBackendPlugin({
pluginId: 'catalog',
export const userSettingsPlugin = createBackendPlugin({
pluginId: 'user-settings',
...
})
```
@@ -31,14 +31,14 @@ export const catalogPlugin = createBackendPlugin({
| Description | Pattern | Examples |
| ----------- | ---------------------------- | ----------------------------------- |
| export | `<pluginId>Module<ModuleId>` | `catalogModuleGithubEntityProvider` |
| ID | `'<moduleId>'` | `'githubEntityProvider'` |
| ID | `'<module-id>'` | `'github-entity-provider'` |
Example:
```ts
export const catalogModuleGithubEntityProvider = createBackendModule({
pluginId: 'catalog',
moduleId: 'githubEntityProvider',
moduleId: 'github-entity-provider',
...
})
```
@@ -6,8 +6,6 @@ sidebar_label: Overview
description: Building backends using the new backend system
---
> **NOTE: The new backend system is in alpha, and some plugins do not yet fully implement it.**
> NOTE: If you have an existing backend that is not yet using the new backend
> system, see [migrating](./08-migrating.md).
@@ -142,3 +140,13 @@ backend.start();
```
We've now split the backend into two separate deployments, but we still need to make sure that they can communicate with each other. This is the hard and somewhat tedious part, as Backstage currently doesn't provide an out of the box solution that solves this. You'll need to manually configure the two backends with custom implementations of the `DiscoveryService` and have them return the correct URLs for each other. Likewise, you'll also need to provide a custom implementation of the `DiscoveryApi` in the frontend, unless you surface the two backends via a proxy that handles the routing instead.
### Split backend deployments architecture example
Below is an example of a more elaborate setup where we have three different backend deployments, each with their own set of plugins and modules. Between our frontend app and the backend instances we have a reverse proxy that routes the traffic to the appropriate instance. As an option for securing the Backstage deployment the proxy can be set up as an authenticating reverse proxy as well, denying unauthenticated users access to the backend instances.
![diagram of a backstage deployment with three separate horizontally scaled backend systems](../../assets/backend-system/scaled-deployments.drawio.svg)
In this example we have split out the Catalog and Search plugins into one backend deployment. The proxy routes all traffic for `/api/catalog/` and `/api/search/` to this instance. With this separation we're able to scale and deploy these two plugins independently, and they are also isolated from both a performance and security perspective. Likewise the TechDocs and Scaffolder plugins are split out as well, and then we route the rest of the traffic to our instance that contains the App, Auth, and Proxy plugins.
We also see how each of the plugins have their own logical database, but are often set up to share the actual Database Management System (DBMS) instance. This is of course not a requirement, and you can choose to further divide or consolidate the databases as you see fit.
@@ -6,8 +6,6 @@ sidebar_label: Migration Guide
description: How to migrate existing backends to the new backend system
---
> **NOTE: The new backend system is in alpha, and some plugins do not yet fully implement it.**
## Overview
This section describes how to migrate an existing Backstage backend service
@@ -21,8 +19,7 @@ internal plugins and support classes themselves to the backend system first -
the migration here will mostly deal with wiring and using compatibility wrappers
where possible in the backend package itself. We hope that you will find that
you end up with a much smaller, easier to understand, and easier to maintain
package as a result of these steps, and then being able to [migrate
plugins](../building-plugins-and-modules/08-migrating.md) as a separate
package as a result of these steps, and then being able to [migrate plugins](../building-plugins-and-modules/08-migrating.md) as a separate
endeavour later.
## Overall Structure
@@ -262,6 +259,317 @@ If you have other customizations made to `plugins/catalog.ts`, such as adding
custom processors or entity providers, read on. Otherwise, you should be able to
just delete that file at this point.
#### Amazon Web Services
`AwsEksClusterProcessor` and `AwsOrganizationCloudAccountProcessor` have not yet been migrated to the new backend system.
See [Other Catalog Extensions](#other-catalog-extensions) for how to use these in the new backend system.
For `AwsS3DiscoveryProcessor`, first migrate to `AwsS3EntityProvider`.
To migrate `AwsS3EntityProvider` to the new backend system, add a reference to the `@backstage/plugin-catalog-backend-module-aws` module.
```ts title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
/* highlight-add-start */
backend.add(import('@backstage/plugin-catalog-backend-module-aws/alpha'));
/* highlight-add-end */
```
If you were providing a `schedule` in code, this now needs to be set via configuration.
All other AWS configuration in `app-config.yaml` remains the same.
```yaml title="app-config.yaml"
catalog:
providers:
awsS3:
yourProviderId:
# ...
/* highlight-add-start */
schedule:
frequency: PT1H
timeout: PT50M
/* highlight-add-end */
```
#### Azure DevOps
For `AzureDevOpsDiscoveryProcessor`, first migrate to `AzureDevOpsEntityProvider`.
To migrate `AzureDevOpsEntityProvider` to the new backend system, add a reference to the `@backstage/plugin-catalog-backend-module-azure` module.
```ts title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
/* highlight-add-start */
backend.add(import('@backstage/plugin-catalog-backend-module-azure/alpha'));
/* highlight-add-end */
```
If you were providing a `schedule` in code, this now needs to be set via configuration.
All other Azure DevOps configuration in `app-config.yaml` remains the same.
```yaml title="app-config.yaml"
catalog:
providers:
azureDevOps:
yourProviderId:
# ...
/* highlight-add-start */
schedule:
frequency: PT1H
timeout: PT50M
/* highlight-add-end */
```
#### Open API
`InternalOpenApiDocumentationProvider` has not yet been migrated to the new backend system.
See [Other Catalog Extensions](#other-catalog-extensions) for how to use this in the new backend system.
#### Bitbucket
For `BitbucketDiscoveryProcessor`, migrate to `BitbucketCloudEntityProvider` or `BitbucketServerEntityProvider`
To migrate `BitbucketCloudEntityProvider` to the new backend system, add a reference to the `@backstage/plugin-catalog-backend-module-bitbucket-cloud` module.
```ts title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
/* highlight-add-start */
backend.add(
import('@backstage/plugin-catalog-backend-module-bitbucket-cloud/alpha'),
);
/* highlight-add-end */
```
If you were providing a `schedule` in code, this now needs to be set via configuration.
All other Bitbucket Cloud configuration in `app-config.yaml` remains the same.
```yaml title="app-config.yaml"
catalog:
providers:
bitbucketCloud:
yourProviderId:
# ...
/* highlight-add-start */
schedule:
frequency: PT30M
timeout: PT3M
/* highlight-add-end */
```
To migrate `BitbucketServerEntityProvider` to the new backend system, add a reference to `@backstage/plugin-catalog-backend-module-bitbucket-server`.
```ts title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
/* highlight-add-start */
backend.add(
import('@backstage/plugin-catalog-backend-module-bitbucket-server/alpha'),
);
/* highlight-add-end */
```
If you were providing a `schedule` in code, this now needs to be set via configuration.
All other Bitbucket Server configuration in `app-config.yaml` remains the same.
```yaml title="app-config.yaml"
catalog:
providers:
bitbucketServer:
yourProviderId:
# ...
/* highlight-add-start */
schedule:
frequency: PT30M
timeout: PT3M
/* highlight-add-end */
```
#### Google Cloud Platform
To migrate `GkeEntityProvider` to the new backend system, add a reference to `@backstage/plugin-catalog-backend-module-gcp`.
```ts title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
/* highlight-add-start */
backend.add(import('@backstage/plugin-catalog-backend-module-gcp'));
/* highlight-add-end */
```
Configuration in app-config.yaml remains the same.
#### Gerrit
To migrate `GerritEntityProvider` to the new backend system, add a reference to `@backstage/plugin-catalog-backend-module-gerrit`.
```ts title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
/* highlight-add-start */
backend.add(import('@backstage/plugin-catalog-backend-module-gerrit/alpha'));
/* highlight-add-end */
```
If you were providing a `schedule` in code, this now needs to be set via configuration.
All other Gerrit configuration in `app-config.yaml` remains the same.
```yaml title="app-config.yaml"
catalog:
providers:
gerrit:
yourProviderId:
# ...
/* highlight-add-start */
schedule:
frequency: PT30M
timeout: PT3M
/* highlight-add-end */
```
#### Github
For `GithubDiscoveryProcessor`, `GithubMultiOrgReaderProcessor` and `GithubOrgReaderProcessor`, first migrate to the equivalent Entity Provider.
To migrate `GithubEntityProvider` to the new backend system, add a reference to `@backstage/plugin-catalog-backend-module-github`.
```ts title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
/* highlight-add-start */
backend.add(import('@backstage/plugin-catalog-backend-module-github/alpha'));
/* highlight-add-end */
```
If you were providing a `schedule` in code, this now needs to be set via configuration.
All other Github configuration in `app-config.yaml` remains the same.
```yaml title="app-config.yaml"
catalog:
providers:
github:
yourProviderId:
# ...
/* highlight-add-start */
schedule:
frequency: PT30M
timeout: PT3M
/* highlight-add-end */
```
To migrate `GithubMultiOrgEntityProvider` and `GithubOrgEntityProvider` to the new backend system, add a reference to `@backstage/plugin-catalog-backend-module-github-org`.
```ts title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
/* highlight-add-start */
backend.add(import('@backstage/plugin-catalog-backend-module-github-org'));
/* highlight-add-end */
```
If you were providing a `schedule` in code, this now needs to be set via configuration.
All other Github configuration in `app-config.yaml` remains the same.
```yaml title="app-config.yaml"
catalog:
providers:
githubOrg:
yourProviderId:
# ...
/* highlight-add-start */
schedule:
frequency: PT30M
timeout: PT3M
/* highlight-add-end */
```
If you were providing transformers, these can be configured by extending `githubOrgEntityProviderTransformsExtensionPoint`
```ts title="packages/backend/src/index.ts"
import { createBackendModule } from '@backstage/backend-plugin-api';
import { githubOrgEntityProviderTransformsExtensionPoint } from '@backstage/plugin-catalog-backend-module-github-org';
backend.add(
createBackendModule({
pluginId: 'catalog',
moduleId: 'githubOrgTransformers',
register(env) {
env.registerInit({
deps: {
/* highlight-add-start */
githubOrgTransformers:
githubOrgEntityProviderTransformsExtensionPoint,
/* highlight-add-end */
},
async init({ githubOrgTransformers }) {
/* highlight-add-start */
githubOrgTransformers.setUserTransformer(myUserTransformer);
githubOrgTransformers.setTeamTransformer(myTeamTransformer);
/* highlight-add-end */
},
});
},
}),
);
```
#### Microsoft Graph
For `MicrosoftGraphOrgReaderProcessor`, first migrate to `MicrosoftGraphOrgEntityProvider`
To migrate `MicrosoftGraphOrgEntityProvider` to the new backend system, add a reference to `@backstage/plugin-catalog-backend-module-msgraph`.
```ts title="packages/backend/src/index.ts"
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
/* highlight-add-start */
backend.add(import('@backstage/plugin-catalog-backend-module-msgraph/alpha'));
/* highlight-add-end */
```
If you were providing a `schedule` in code, this now needs to be set via configuration.
All other Microsoft Graph configuration in `app-config.yaml` remains the same.
```yaml title="app-config.yaml"
catalog:
providers:
microsoftGraphOrg:
provider:
/* highlight-add-start */
schedule:
frequency: PT4H
timeout: PT30M
/* highlight-add-end */
```
If you were providing transformers, these can be configured by extending `microsoftGraphOrgEntityProviderTransformExtensionPoint`
```ts title="packages/backend/src/index.ts"
import { createBackendModule } from '@backstage/backend-plugin-api';
import { microsoftGraphOrgEntityProviderTransformExtensionPoint } from '@backstage/plugin-catalog-backend-module-msgraph/alpha';
backend.add(
createBackendModule({
pluginId: 'catalog',
moduleId: 'microsoft-graph-extensions',
register(env) {
env.registerInit({
deps: {
/* highlight-add-start */
microsoftGraphTransformers:
microsoftGraphOrgEntityProviderTransformExtensionPoint,
/* highlight-add-end */
},
async init({ microsoftGraphTransformers }) {
/* highlight-add-start */
microsoftGraphTransformers.setUserTransformer(myUserTransformer);
microsoftGraphTransformers.setGroupTransformer(myGroupTransformer);
microsoftGraphTransformers.setOrganizationTransformer(
myOrganizationTransformer,
);
/* highlight-add-end */
},
});
},
}),
);
```
#### Other Catalog Extensions
You will use the [extension points](../architecture/05-extension-points.md)
mechanism to extend or tweak the functionality of the plugin. To do that,
you'll make your own bespoke [module](../architecture/06-modules.md) which
@@ -269,21 +577,21 @@ depends on the appropriate extension point and interacts with it.
```ts title="packages/backend/src/index.ts"
/* highlight-add-start */
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { createBackendModule } from '@backstage/backend-plugin-api';
/* highlight-add-end */
/* highlight-add-start */
const catalogModuleCustomExtensions = createBackendModule({
pluginId: 'catalog', // name of the plugin that the module is targeting
moduleId: 'customExtensions',
moduleId: 'custom-extensions',
register(env) {
env.registerInit({
deps: {
catalog: catalogProcessingExtensionPoint,
// ... and other dependencies as needed
},
init({ catalog /* ..., other dependencies */ }) {
async init({ catalog /* ..., other dependencies */ }) {
// Here you have the opportunity to interact with the extension
// point before the plugin itself gets instantiated
catalog.addEntityProvider(new MyEntityProvider()); // just an example
@@ -338,21 +646,21 @@ depends on the appropriate extension point and interacts with it.
```ts title="packages/backend/src/index.ts"
/* highlight-add-start */
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha';
import { createBackendModule } from '@backstage/backend-plugin-api';
/* highlight-add-end */
/* highlight-add-start */
const eventsModuleCustomExtensions = createBackendModule({
pluginId: 'events', // name of the plugin that the module is targeting
moduleId: 'customExtensions',
moduleId: 'custom-extensions',
register(env) {
env.registerInit({
deps: {
events: eventsExtensionPoint,
// ... and other dependencies as needed
},
init({ events /* ..., other dependencies */ }) {
async init({ events /* ..., other dependencies */ }) {
// Here you have the opportunity to interact with the extension
// point before the plugin itself gets instantiated
events.addSubscribers(new MySubscriber()); // just an example
@@ -403,21 +711,21 @@ depends on the appropriate extension point and interacts with it.
```ts title="packages/backend/src/index.ts"
/* highlight-add-start */
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node';
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
import { createBackendModule } from '@backstage/backend-plugin-api';
/* highlight-add-end */
/* highlight-add-start */
const scaffolderModuleCustomExtensions = createBackendModule({
pluginId: 'scaffolder', // name of the plugin that the module is targeting
moduleId: 'customExtensions',
moduleId: 'custom-extensions',
register(env) {
env.registerInit({
deps: {
scaffolder: scaffolderActionsExtensionPoint,
// ... and other dependencies as needed
},
init({ scaffolder /* ..., other dependencies */ }) {
async init({ scaffolder /* ..., other dependencies */ }) {
// Here you have the opportunity to interact with the extension
// point before the plugin itself gets instantiated
scaffolder.addActions(new MyAction()); // just an example
@@ -446,3 +754,255 @@ going easily, but feel free to move it out to where it fits best. As you migrate
your entire plugin flora to the new backend system, you will probably make more
and more of these modules as "first class" things, living right next to the
implementations that they represent, and being exported from there.
### The Auth Plugin
A basic installation of the auth plugin with a Microsoft provider will look as follows.
```ts title="packages/backend/src/index.ts"
const backend = createBackend();
/* highlight-add-start */
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-microsoft-provider'));
/* highlight-add-end */
```
An additional step you'll need to take is to add the resolvers to your configuration, here's an example:
```yaml title:"app-config.yaml"
auth:
environment: development
providers:
microsoft:
development:
clientId: ${AZURE_CLIENT_ID}
clientSecret: ${AZURE_CLIENT_SECRET}
tenantId: ${AZURE_TENANT_ID}
signIn:
resolvers:
- resolver: emailMatchingUserEntityAnnotation
- resolver: emailMatchingUserEntityProfileEmail
- resolver: emailLocalPartMatchingUserEntityName
```
> Note: the resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`.
#### Auth Plugin Modules and Their Resolvers
As you may have noticed in the above example you'll need to import the `auth-backend` and an `auth-backend-module`. The following sections outline each of them and their resolvers.
All of the following modules include the following common resolvers:
- [emailMatchingUserEntityProfileEmail](https://github.com/backstage/backstage/blob/5447cffd23cf00772988fb799ced0ec5e54efb2e/plugins/auth-node/src/sign-in/commonSignInResolvers.ts#L29)
- [emailLocalPartMatchingUserEntityName](https://github.com/backstage/backstage/blob/5447cffd23cf00772988fb799ced0ec5e54efb2e/plugins/auth-node/src/sign-in/commonSignInResolvers.ts#L54)
##### Atlassian
Setup:
```ts title="packages/backend/src/index.ts"
const backend = createBackend();
/* highlight-add-start */
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-atlassian-provider'));
/* highlight-add-end */
```
Additional resolvers:
- [usernameMatchingUserEntityName](https://github.com/backstage/backstage/blob/5447cffd23cf00772988fb799ced0ec5e54efb2e/plugins/auth-backend-module-atlassian-provider/src/resolvers.ts#L33C16-L33C46)
##### GCP IAM
Setup:
```ts title="packages/backend/src/index.ts"
const backend = createBackend();
/* highlight-add-start */
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-gcp-iap-provider'));
/* highlight-add-end */
```
Additional resolvers:
- [emailMatchingUserEntityAnnotation](https://github.com/backstage/backstage/blob/5447cffd23cf00772988fb799ced0ec5e54efb2e/plugins/auth-backend-module-gcp-iap-provider/src/resolvers.ts#L32C16-L32C49)
##### GitHub
Setup:
```ts title="packages/backend/src/index.ts"
const backend = createBackend();
/* highlight-add-start */
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-github-provider'));
/* highlight-add-end */
```
Additional resolvers:
- [usernameMatchingUserEntityName](https://github.com/backstage/backstage/blob/5447cffd23cf00772988fb799ced0ec5e54efb2e/plugins/auth-backend-module-github-provider/src/resolvers.ts#L33C16-L33C46)
##### GitLab
Setup:
```ts title="packages/backend/src/index.ts"
const backend = createBackend();
/* highlight-add-start */
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-gitlab-provider'));
/* highlight-add-end */
```
Additional resolvers:
- [usernameMatchingUserEntityName](https://github.com/backstage/backstage/blob/5447cffd23cf00772988fb799ced0ec5e54efb2e/plugins/auth-backend-module-gitlab-provider/src/resolvers.ts#L33C16-L33C46)
##### Google
Setup:
```ts title="packages/backend/src/index.ts"
const backend = createBackend();
/* highlight-add-start */
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-google-provider'));
/* highlight-add-end */
```
Additional resolvers:
- [emailMatchingUserEntityAnnotation](https://github.com/backstage/backstage/blob/5447cffd23cf00772988fb799ced0ec5e54efb2e/plugins/auth-backend-module-google-provider/src/resolvers.ts#L33C16-L33C49)
##### Microsoft
Setup:
```ts title="packages/backend/src/index.ts"
const backend = createBackend();
/* highlight-add-start */
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-microsoft-provider'));
/* highlight-add-end */
```
Additional resolvers:
- [emailMatchingUserEntityAnnotation](https://github.com/backstage/backstage/blob/5447cffd23cf00772988fb799ced0ec5e54efb2e/plugins/auth-backend-module-microsoft-provider/src/resolvers.ts#L33C16-L33C49)
##### oauth2
Setup:
```ts title="packages/backend/src/index.ts"
const backend = createBackend();
/* highlight-add-start */
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-oauth2-provider'));
/* highlight-add-end */
```
Additional resolvers:
- [usernameMatchingUserEntityName](https://github.com/backstage/backstage/blob/5447cffd23cf00772988fb799ced0ec5e54efb2e/plugins/auth-backend-module-oauth2-provider/src/resolvers.ts#L33C16-L33C46)
##### oauth2 Proxy
Setup:
```ts title="packages/backend/src/index.ts"
const backend = createBackend();
/* highlight-add-start */
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(
import('@backstage/plugin-auth-backend-module-oauth2-proxy-provider'),
);
/* highlight-add-end */
```
Additional resolvers:
- [forwardedUserMatchingUserEntityName](https://github.com/backstage/backstage/blob/5447cffd23cf00772988fb799ced0ec5e54efb2e/plugins/auth-backend-module-oauth2-proxy-provider/src/resolvers.ts#L27C16-L27C51)
##### Okta
Setup:
```ts title="packages/backend/src/index.ts"
const backend = createBackend();
/* highlight-add-start */
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-okta-provider'));
/* highlight-add-end */
```
Additional resolvers:
- [emailMatchingUserEntityAnnotation](https://github.com/backstage/backstage/blob/5447cffd23cf00772988fb799ced0ec5e54efb2e/plugins/auth-backend-module-okta-provider/src/resolvers.ts#L34C16-L34C49)
##### Pinniped
Setup:
```ts title="packages/backend/src/index.ts"
const backend = createBackend();
/* highlight-add-start */
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-pinniped-provider'));
/* highlight-add-end */
```
##### VMware Cloud
Setup:
```ts title="packages/backend/src/index.ts"
const backend = createBackend();
/* highlight-add-start */
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(
import('@backstage/plugin-auth-backend-module-vmware-cloud-provider'),
);
/* highlight-add-end */
```
Additional resolvers:
- [vmwareCloudSignInResolvers](https://github.com/backstage/backstage/blob/5447cffd23cf00772988fb799ced0ec5e54efb2e/plugins/auth-backend-module-vmware-cloud-provider/src/resolvers.ts#L29C18-L29C44)
#### Custom Resolver
You may have a case where the common resolvers or the ones that are included with the auth module you use won't work for your needs. In this case you will need to create a custom resolver. Instead of the 2nd import for your auth provider module you would provide your own:
```ts title="packages/backend/src/index.ts"
/* highlight-add-start */
export const authModuleGoogleProvider = createBackendModule({
pluginId: 'auth',
moduleId: 'googleProvider',
register(reg) {
reg.registerInit({
deps: { providers: authProvidersExtensionPoint },
async init({ providers }) {
providers.registerProvider({
providerId: 'google',
factory: createOAuthProviderFactory({
authenticator: googleAuthenticator,
async signInResolver(info, ctx) {
// custom resolver ...
},
}),
});
},
});
},
});
/* highlight-add-end */
const backend = createBackend();
/* highlight-add-start */
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(authModuleGoogleProvider);
/* highlight-add-end */
```
@@ -6,8 +6,6 @@ sidebar_label: Overview
description: Building backend plugins and modules using the new backend system
---
> **NOTE: The new backend system is in alpha, and some plugins do not yet fully implement it.**
> NOTE: If you have an existing backend and/or backend plugins that are not yet
> using the new backend system, see [migrating](./08-migrating.md).
@@ -72,12 +70,12 @@ items.
## Modules
Backend modules are used to extend [plugins](../architecture/04-plugins.md) with
Backend modules are used to extend [plugins](../architecture/04-plugins.md) or other modules with
additional features or change existing behavior. They must always be installed
in the same backend instance as the plugin that they extend, and may only extend
a single plugin. Modules interact with their target plugin using the [extension
in the same backend instance as the plugin or module that they extend, and may only extend a single plugin and modules from that plugin at a time.
Modules interact with their target plugin or module using the [extension
points](../architecture/05-extension-points.md) registered by the plugin, while also being
able to depend on the [services](../architecture/03-services.md) of that plugin.
able to depend on the [services](../architecture/03-services.md) of the target plugin.
That last point is worth reiterating: injected `plugin` scoped services will be
the exact
same ones as the target plugin will receive later, i.e. they will be scoped
@@ -88,6 +86,9 @@ package, for example `@backstage/plugin-catalog-node`, and does not directly
declare a dependency on the plugin package itself. This is to avoid a direct
dependency and potentially cause duplicate installations of the plugin package,
while duplicate installations of library packages should always be supported.
Modules with extension points typically export their extension points from the same
package however, since the extension points are generally only intended for internal
customizations where package versions can be kept in sync.
To create a Backend module, run `yarn new`, select `backend-module`, and fill out the rest of the prompts. This will create a new package at `plugins/<pluginId>-backend-module-<moduleId>`.
@@ -101,8 +102,8 @@ import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'
import { MyCustomProcessor } from './MyCustomProcessor';
export const catalogModuleExampleCustomProcessor = createBackendModule({
moduleId: 'exampleCustomProcessor',
pluginId: 'catalog',
moduleId: 'example-custom-processor',
register(env) {
env.registerInit({
deps: {
@@ -6,8 +6,6 @@ sidebar_label: Testing
description: Learn how to test your backend plugins and modules
---
> **NOTE: The new backend system is in alpha, and some plugins do not yet fully implement it.**
Utilities for testing backend plugins and modules are available in
`@backstage/backend-test-utils`. This section describes those facilities.
@@ -120,7 +118,7 @@ describe('MyDatabaseClass', () => {
// "physical" databases to test against is much costlier than creating the
// "logical" databases within them that the individual tests use.
const databases = TestDatabases.create({
ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3', 'MYSQL_8'],
ids: ['POSTGRES_16', 'POSTGRES_12', 'SQLITE_3', 'MYSQL_8'],
});
// Just an example of how to conveniently bundle up the setup code
@@ -6,8 +6,6 @@ sidebar_label: Migration Guide
description: How to migrate existing backend plugins to the new backend system
---
> **NOTE: The new backend system is in alpha, and some plugins do not yet fully implement it.**
Migrating an existing backend plugin to the new backend system is fairly straightforward. The process is similar across the majority of plugins which just return a `Router` that is then wired up in the `index.ts` file of your backend. The primary thing that we need to do is to make sure that the dependencies that are required by the plugin are available, and then registering the router with the HTTP router service.
Let's look at an example of migrating the Kubernetes backend plugin. In the existing (old) system, the kubernetes backend is structured like this:
@@ -73,30 +71,99 @@ export const kubernetesPlugin = createBackendPlugin({
});
```
Done! Users of this plugin are now able to import the `kubernetesPlugin` and register it in their backend using
Lastly, make sure you re-export the plugin instance as the default export of your package in `src/index.ts`:
```ts
export { kubernetesPlugin as default } from './plugin.ts';
```
Done! Users of this plugin are now able to import your plugin package and register it in their backend using
```ts
// packages/backend/src/index.ts
import { kubernetesPlugin } from '@backstage/plugin-kubernetes-backend';
backend.add(kubernetesPlugin);
backend.add(import('@backstage/plugin-kubernetes-backend'));
```
There's one thing missing that those sharp eyed readers might have noticed: the `clusterSupplier` option is missing from the original plugin. Let's add it and discuss the alternatives.
One alternative is to pass the `ClusterSupplier` in as options to the plugin, which is quick and easy but not very flexible, and also hard to evolve without introducing breaking changes as it changes the public API for the plugin. Having complex types passed in directly to the plugin also clutters the backend setup code and makes it harder to read.
Options are primarily used for simple configuration values that are not complex types. In this case we want to allow users to register their own `ClusterSupplier` implementations to the plugin. This is where the new backend system's [extension points](../architecture/05-extension-points.md) come in handy, but let's look at doing this with options first.
One alternative is to make it possible to build the cluster supplier using static configuration. It could for example be that there is a selection of built-in implementations to choose from, or that the logic for how the `ClusterSupplier` is supposed to function is all determined by configuration, or a combination of the two. Using static configuration for customization is always the preferred option whenever it's possible. In this case, we could for example imagine that we would be able to configure our cluster supplier like this:
```ts
/* omitted imports but they remain the same as above */
export interface KubernetesOptions {
clusterSupplier?: KubernetesClustersSupplier;
}
const kubernetesPlugin = createBackendPlugin((options: KubernetesOptions) => ({
const kubernetesPlugin = createBackendPlugin({
pluginId: 'kubernetes',
register(env) {
env.registerInit({
deps: {
/* omitted dependencies but they remain the same as above */
},
async init({ config, logger, catalogApi, discovery, http }) {
// Note that in a real implementation this would be done by the `KubernetesBuilder` instead,
// but here we've extracted it into a separate call to highlight the example.
const configuredClusterSupplier = readClusterSupplierFromConfig(config);
const { router } = await KubernetesBuilder.createBuilder({
config,
logger,
catalogApi,
discovery,
})
.setClusterSupplier(configuredClusterSupplier)
.build();
http.use(router);
},
});
},
});
```
There are however many types of customizations that are not possible to do with static configuration. In this case we want integrators to be able to create arbitrary implementations of the `ClusterSupplier` interface, which in the end requires an implementation through code. This is where the new backend system's [extension points](../architecture/05-extension-points.md) come in handy.
The new [extension points](../architecture/05-extension-points.md) API allows [modules](../architecture/06-modules.md) to add functionality into the backend plugin itself, in this case an additional `ClusterSupplier`. Let's look at how we could add support for installing custom suppliers using an extension point. This will allow integrators to build their own internal module with a custom `ClusterSupplier` implementation.
First we'll go ahead and create a `@backstage/plugin-kubernetes-node` package where we can define our extension point. A separate package is used to avoid direct dependencies on the plugin package itself. With the new package created, we define the extension point like this:
```ts
import { createExtensionPoint } from '@backstage/backend-plugin-api';
export interface KubernetesClusterSupplierExtensionPoint {
setClusterSupplier(supplier: KubernetesClustersSupplier): void;
}
/**
* An extension point that allows other plugins to set the cluster supplier.
*/
export const kubernetesClustersSupplierExtensionPoint =
createExtensionPoint<KubernetesClusterSupplierExtensionPoint>({
id: 'kubernetes.cluster-supplier',
});
```
For more information on how to design extension points, see the [extension points](../architecture/05-extension-points.md#extension-point-design) documentation.
Next we'll need to add support for this extension point to the Kubernetes backend plugin itself:
```ts
/* omitted other imports but they remain the same as above */
import { kubernetesClustersSupplierExtensionPoint } from '@backstage/plugin-kubernetes-node';
export const kubernetesPlugin = createBackendPlugin({
pluginId: 'kubernetes',
register(env) {
let clusterSupplier: KubernetesClustersSupplier | undefined = undefined;
// We register the extension point with the backend, which allows modules to
// register their own ClusterSupplier.
env.registerExtensionPoint(kubernetesClustersSupplierExtensionPoint, {
setClusterSupplier(supplier) {
if (clusterSupplier) {
throw new Error('ClusterSupplier may only be set once');
}
clusterSupplier = supplier;
},
});
env.registerInit({
deps: {
/* omitted dependencies but they remain the same as above */
@@ -108,103 +175,7 @@ const kubernetesPlugin = createBackendPlugin((options: KubernetesOptions) => ({
catalogApi,
discovery,
})
.setClusterSupplier(options.clusterSupplier)
.build();
http.use(router);
},
});
},
}));
```
The above would allow users to specify their own `ClusterSupplier` implementation to the plugin like this:
```ts
backend.add(
kubernetesPlugin({ clusterSupplier: new MyCustomClusterSupplier() }),
);
```
Just to echo what was said above, this is not a very flexible solution and will for example be problematic to keep backwards compatible if we start evolving the options to for example accept multiple suppliers or tweak the `ClusterSupplier` interface.
The new [extension points](../architecture/05-extension-points.md) API allows [modules](../architecture/06-modules.md) to add functionality into the backend plugin itself, in this case an additional `ClusterSupplier`.
The kubernetes backend plugin only supports one `ClusterSupplier` at this time but let's look at how we could add support for multiple suppliers using extension points. This allows users to install several modules that add their own `ClusterSupplier` implementations to the plugin like this:
```ts
backend.add(kubernetesPlugin());
backend.add(kubernetesGoogleContainerEngineClusterSupplier());
backend.add(kubernetesElasticContainerEngine());
```
Now let's look at how to implement this with extension points. First we need to define the extension point itself. As the extension point will be used by other modules, it's common practice to export these from a shared package so that they can be imported by other modules and plugins.
We'll go ahead and create a `@backstage/plugin-kubernetes-node` package for this and from there we'll export the extension point.
```ts
import { createExtensionPoint } from '@backstage/backend-plugin-api';
export interface KubernetesClusterSupplierExtensionPoint {
addClusterSupplier(supplier: KubernetesClustersSupplier): void;
}
/**
* An extension point that allows other plugins to add cluster suppliers.
* @public
*/
export const kubernetesClustersSupplierExtensionPoint =
createExtensionPoint<KubernetesClusterSupplierExtensionPoint>({
id: 'kubernetes.cluster-supplier',
});
```
Now we can use this extension point in the kubernetes backend plugin to register the extension point for modules to use.
```ts
import { kubernetesClustersSupplierExtensionPoint, KubernetesClusterSupplierExtensionPoint } from '@backstage/plugin-kubernetes-node';
// Our internal implementation of the extension point, should not be exported.
class ClusterSupplier implements KubernetesClusterSupplierExtensionPoint {
private clusterSuppliers: KubernetesClustersSupplier | undefined;
// This method is private and only used internally to retrieve the registered supplier.
getClusterSupplier() {
return this.clusterSuppliers;
}
addClusterSupplier(supplier: KubernetesClustersSupplier) {
// We can remove this check once the plugin support multiple suppliers.
if(this.clusterSuppliers) {
throw new Error('Multiple Kubernetes cluster suppliers is not supported at this time');
}
this.clusterSuppliers = supplier;
}
}
export const kubernetesPlugin = createBackendPlugin({
pluginId: 'kubernetes',
register(env) {
const extensionPoint = new ClusterSupplier();
// We register the extension point with the backend, which allows modules to
// register their own ClusterSupplier.
env.registerExtensionPoint(
kubernetesClustersSupplierExtensionPoint,
extensionPoint,
);
env.registerInit({
deps: {
... omitted ...
},
async init({ config, logger, catalogApi, discovery, http }) {
const { router } = await KubernetesBuilder.createBuilder({
config,
logger,
catalogApi,
discovery,
})
// We pass in the registered supplier from the extension point.
.setClusterSupplier(extensionPoint.getClusterSupplier())
.setClusterSupplier(clusterSupplier)
.build();
http.use(router);
},
@@ -213,24 +184,33 @@ export const kubernetesPlugin = createBackendPlugin({
});
```
And that's it! Modules can now be built that add clusters into to the kubernetes backend plugin, here's an example of a module that adds a `GoogleContainerEngineSupplier` to the kubernetes backend.
And that's it! Modules can now be built that add clusters into to the kubernetes backend plugin, here's an example of a module that adds a `GoogleContainerEngineSupplier` to the kubernetes backend:
```ts
import { kubernetesClustersSupplierExtensionPoint } from '@backstage/plugin-kubernetes-node';
export const kubernetesGoogleContainerEngineClusterSupplier =
createBackendModule({
pluginId: 'kubernetes',
moduleId: 'gke.supplier',
register(env) {
env.registerInit({
deps: {
supplier: kubernetesClustersSupplierExtensionPoint,
},
async init({ supplier }) {
supplier.addClusterSupplier(new GoogleContainerEngineSupplier());
},
});
},
});
// This is a custom implementation of the ClusterSupplier interface.
import { GoogleContainerEngineSupplier } from './GoogleContainerEngineSupplier';
export default createBackendModule({
pluginId: 'kubernetes',
moduleId: 'gke-supplier',
register(env) {
env.registerInit({
deps: {
supplier: kubernetesClustersSupplierExtensionPoint,
},
async init({ supplier }) {
supplier.setClusterSupplier(new GoogleContainerEngineSupplier());
},
});
},
});
```
The above module can then be installed by the integrator alongside the kubernetes backend plugin:
```ts
backend.add(import('@backstage/plugin-kubernetes-backend'));
backend.add(import('@internal/gke-cluster-supplier'));
```
@@ -6,8 +6,6 @@ sidebar_label: Overview
description: Core backend service APIs
---
> **NOTE: The new backend system is in alpha, and some plugins do not yet fully implement it.**
The default backend provides several [core services](https://github.com/backstage/backstage/blob/master/packages/backend-plugin-api/src/services/definitions/coreServices.ts) out of the box which includes access to configuration, logging, URL Readers, databases and more.
All core services are available through the `coreServices` namespace in the `@backstage/backend-plugin-api` package.
+1 -3
View File
@@ -6,10 +6,8 @@ sidebar_label: Introduction
description: The Backend System
---
> **NOTE: The new backend system is in alpha, and some plugins do not yet fully implement it.**
## Status
The new backend system is in alpha, but many plugins have already been migrated. We recommend all plugins to migrate to the new system, and you can also try it out in your own production deployments.
The new backend system is released and ready for production use, and many plugins and modules have already been migrated. We recommend all plugins and deployments to migrate to the new system.
You can find an example backend setup in [the `backend-next` package](https://github.com/backstage/backstage/tree/master/packages/backend-next).
+24 -7
View File
@@ -53,7 +53,7 @@ yarn build:backend --config ../../app-config.yaml
Once the host build is complete, we are ready to build our image. The following
`Dockerfile` is included when creating a new app with `@backstage/create-app`:
```Dockerfile
```dockerfile
FROM node:18-bookworm-slim
# Install isolate-vm dependencies, these are needed by the @backstage/plugin-scaffolder-backend.
@@ -74,8 +74,10 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
USER node
# This should create the app dir as `node`.
# If it is instead created as `root` then the `tar` command below will fail: `can't create directory 'packages/': Permission denied`.
# If this occurs, then ensure BuildKit is enabled (`DOCKER_BUILDKIT=1`) so the app dir is correctly created as `node`.
# If it is instead created as `root` then the `tar` command below will
# fail: `can't create directory 'packages/': Permission denied`.
# If this occurs, then ensure BuildKit is enabled (`DOCKER_BUILDKIT=1`)
# so the app dir is correctly created as `node`.
WORKDIR /app
# This switches many Node.js dependencies to production mode.
@@ -158,7 +160,7 @@ host build.
The following `Dockerfile` executes the multi-stage build and should be added to
the repo root:
```Dockerfile
```dockerfile
# Stage 1 - Create yarn install skeleton layer
FROM node:18-bookworm-slim AS packages
@@ -229,8 +231,10 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
USER node
# This should create the app dir as `node`.
# If it is instead created as `root` then the `tar` command below will fail: `can't create directory 'packages/': Permission denied`.
# If this occurs, then ensure BuildKit is enabled (`DOCKER_BUILDKIT=1`) so the app dir is correctly created as `node`.
# If it is instead created as `root` then the `tar` command below will
# fail: `can't create directory 'packages/': Permission denied`.
# If this occurs, then ensure BuildKit is enabled (`DOCKER_BUILDKIT=1`)
# so the app dir is correctly created as `node`.
WORKDIR /app
# Copy the install dependencies from the build stage and context
@@ -309,7 +313,7 @@ package, which is done as follows:
.addRouter('', await app(appEnv));
```
3. Remove the `@backstage/plugin-app-backend` and the app package dependency
(e.g. `app`) from `packages/backend/packages.json`. If you don't remove the
(e.g. `app`) from `packages/backend/package.json`. If you don't remove the
app package dependency the app will still be built and bundled with the
backend.
@@ -321,3 +325,16 @@ an NGINX image is available in the
Note that if you're building a separate docker build of the frontend you
probably need to adjust `.dockerignore` appropriately. Most likely by making
sure `packages/app/dist` is not ignored.
## Troubleshooting Tips
When building Docker images you may run into problems from time to time, there are two handy flags you can use to help:
- `--progress=plain`: this will give you a more verbose output and not fold the logs into sections. This is very useful when have an error but it just shows you the last command and possibly an exit code. Using this flag you are more likely to see where the error actually is.
- `--no-cache`: this will rebuild all the layers every time. This is helpful when you want to be sure that it's building from scratch.
Here's an example of these flags in use:
```sh
docker image build . -f packages/backend/Dockerfile --tag backstage --progress=plain --no-cache
```
+82 -6
View File
@@ -51,11 +51,13 @@ kubernetes:
This configures how to determine which clusters a component is running in.
Currently, the only valid value is:
Valid values are:
- `multiTenant` - This configuration assumes that all components run on all the
provided clusters.
- `singleTenant` - This configuration assumes that current component run on one cluster in provided clusters.
### `clusterLocatorMethods`
This is an array used to determine where to retrieve cluster configuration from.
@@ -63,20 +65,72 @@ This is an array used to determine where to retrieve cluster configuration from.
Valid cluster locator methods are:
- [`catalog`](#catalog)
- [`localKubectlProxy`](#localkubectlproxy)
- [`config`](#config)
- [`gke`](#gke)
- [`localKubectlProxy`](#localkubectlproxy)
- [custom `KubernetesClustersSupplier`](#custom-kubernetesclusterssupplier)
#### `catalog`
This cluster locator method will read cluster information from the catalog.
This cluster locator method will gather
[Resources](https://backstage.io/docs/features/software-catalog/system-model#resource)
of
[type](https://backstage.io/docs/features/software-catalog/descriptor-format#spectype-required-4)
`kubernetes-cluster` from the catalog and treat them as clusters for the
purposes of the Kubernetes plugin. In order for a resource to be detected by
this method, it must also have the following
[annotations](https://backstage.io/docs/features/software-catalog/descriptor-format#annotations-optional)
(as seen
[here](https://github.com/backstage/backstage/blob/86baccb2d7d378baed74eaebf017c60b410986e5/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts#L51-L61)
in the code):
#### `localKubectlProxy`
- [`kubernetes.io/api-server`](https://backstage.io/docs/reference/plugin-kubernetes-common.annotation_kubernetes_api_server/),
denoting the base URL of the Kubernetes control plane
- [`kubernetes.io/api-server-certificate-authority`](https://backstage.io/docs/reference/plugin-kubernetes-common.annotation_kubernetes_api_server_ca/),
containing a base64-encoded certificate authority bundle in PEM format;
Backstage will check that the control plane presents a certificate signed by
this authority.
- [`kubernetes.io/auth-provider`](https://backstage.io/docs/reference/plugin-kubernetes-common.annotation_kubernetes_auth_provider/),
denoting the strategy to use to authenticate with the control plane.
This cluster locator method will assume a locally running [`kubectl proxy`](https://kubernetes.io/docs/tasks/extend-kubernetes/http-proxy-access-api/#using-kubectl-to-start-a-proxy-server) process using the default port (8001).
There are many other annotations that can be applied to a cluster resource to
configure the way Backstage communicates, documented
[here](https://backstage.io/docs/reference/plugin-kubernetes-common#variables)
in the API reference. Here is a YAML snippet illustrating an example of a
cluster in the catalog:
NOTE: This cluster locator method is for local development only and should not be used in production.
```yaml
apiVersion: backstage.io/v1alpha1
kind: Resource
metadata:
name: my-cluster
annotations:
kubernetes.io/api-server: 'https://127.0.0.1:53725'
kubernetes.io/api-server-certificate-authority: # base64-encoded CA
kubernetes.io/auth-provider: 'oidc'
kubernetes.io/oidc-token-provider: 'microsoft'
kubernetes.io/skip-metrics-lookup: 'true'
spec:
type: kubernetes-cluster
owner: user:guest
```
Note that it is insecure to store a Kubernetes service account token in an
annotation on a catalog entity (where it could easily be accidentally revealed
by the catalog API) -- therefore there is no annotation corresponding to the
[`serviceAccountToken` field](#clustersserviceaccounttoken-optional) used by
the [`config`](#config) cluster locator. Accordingly, the catalog cluster
locator does not support the [`serviceAccount`](#clustersauthprovider) auth
strategy.
This method can be quite helpful when used in combination with an ingestion
procedure like the
[`GkeEntityProvider`](https://backstage.io/docs/reference/plugin-catalog-backend-module-gcp.gkeentityprovider/)
(installation documented
[here](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-gcp#installation))
or the
[`AwsEKSClusterProcessor`](https://backstage.io/docs/reference/plugin-catalog-backend-module-aws.awseksclusterprocessor/)
to automatically update the set of clusters tracked by Backstage.
#### `config`
@@ -386,6 +440,12 @@ Defaults to `false`.
Array of key value labels used to filter out clusters which don't have the matching
[resource labels](https://cloud.google.com/resource-manager/docs/creating-managing-labels).
#### `localKubectlProxy`
This cluster locator method will assume a locally running [`kubectl proxy`](https://kubernetes.io/docs/tasks/extend-kubernetes/http-proxy-access-api/#using-kubectl-to-start-a-proxy-server) process using the default port (8001).
NOTE: This cluster locator method is for local development only and should not be used in production.
#### Custom `KubernetesClustersSupplier`
If the configuration-based cluster locators do not work for your use-case,
@@ -577,6 +637,22 @@ for more info.
'backstage.io/kubernetes-label-selector': 'app=my-app,component=front-end'
```
### Cluster Selection annotation
This is applicable only for `singleTenant` serviceLocatorMethod.
You can now select `single` kubernetes cluster that the entity is part-of from all your defined kubernetes clusters. To apply this use the following annotation.
SingleTenant Cluster:
```yaml
'backstage.io/kubernetes-cluster': dice-cluster
```
In the example above, we configured the "backstage.io/kubernetes-cluster" annotation on the entity `catalog-info.yaml` file to specify that the current component is running in a single cluster called "dice-cluster", so this cluster must have been specified in the `app-config.yaml`, under the Kubernetes clusters configuration (for more details, see [`Configuring Kubernetes clusters`](#configuring-kubernetes-clusters)).
If you do not specify the annotation by `default Backstage fetches all` defined Kubernetes cluster.
[1]: https://cloud.google.com/kubernetes-engine
[2]: https://cloud.google.com/docs/authentication/production#linux-or-macos
[3]: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/
+1 -1
View File
@@ -130,7 +130,7 @@ export class CustomClustersSupplier implements KubernetesClustersSupplier {
static create(refreshInterval: Duration) {
const clusterSupplier = new CustomClustersSupplier();
// setup refresh, e.g. using a copy of https://github.com/backstage/backstage/blob/master/plugins/search-backend-node/src/runPeriodically.ts
// setup refresh, e.g. using a copy of https://github.com/backstage/backstage/blob/master/plugins/kubernetes-backend/src/service/runPeriodically.ts
runPeriodically(
() => clusterSupplier.refreshClusters(),
refreshInterval.toMillis(),
@@ -51,18 +51,19 @@ _Example disabling the search page extension_
# app-config.yaml
app:
extensions:
- plugin.search.page: false # ✨
- page:search: false # ✨
- nav-item:search: false # ✨
```
_Example setting the search sidebar item label_
_Example setting the search sidebar item title_
```yaml
# app-config.yaml
app:
extensions:
- plugin.search.nav.index: # ✨
- nav-item:search: # ✨
config:
label: 'Search Page'
title: 'Search Page'
```
> **Known limitations:**
-2
View File
@@ -391,8 +391,6 @@ There are other more specific search results layout components that also accept
## How to migrate your backend installation to use Search together with the new backend system
> DISCLAIMER: The new backend system is in alpha, and so are the search backend support for the new backend system. We don't recommend you to migrate your backend installations to the new system yet. But if you want to experiment, this is the guide for you!
Recently, the Backstage maintainers [announced the new Backend System](https://backstage.io/blog/2023/02/15/backend-system-alpha). The search plugins are now migrated to support the new backend system. In this guide you will learn how to update your backend set up.
In "packages/backend-next/index.ts", install the search plugin [1], the search engine [2], and the search collators/decorators modules [3]:
+180 -3
View File
@@ -31,13 +31,190 @@ with a `Bearer` token, which should then be the Backstage token returned by the
These are the endpoints that deal with reading of entities directly. What it
exposes are final entities - i.e. the output of all processing and the stitching
process, not the raw originally ingested entity data. See [The Life of an
Entity](life-of-an-entity.md) for more details about this process and
process, not the raw originally ingested entity data. See
[The Life of an Entity](./life-of-an-entity.md) for more details about this process and
distinction.
### `GET /entities/by-query`
Query entities. Supports the following query parameters, described in the section below:
- [`filter`](#filtering), for selecting only a subset of all entities
- [`fields`](#field-selection), for selecting only parts of the full data
structure of each entity
- `limit` for limiting the number of entities returned (20 is the default)
- [`orderField`](#ordering), for deciding the order of the entities
- [`fullTextFilter`](#full-text-filtering), for filtering the entities by text
- [`cursor`](#pagination), for retrieving the next or previous batch of entities
The return type is JSON, on the following form
```json
{
"items": [{ "kind": "Component", "metadata": { "name": "foo" } }],
"totalItems": 4,
"pageInfo": {
"nextCursor": "a-cursor",
"prevCursor": "another-cursor"
}
}
```
#### Filtering
You can pass in one or more filter sets that get matched against each entity.
Each filter set is a number of conditions that all have to match for the
condition to be true (conditions effectively have an AND between them). At least
one filter set has to be true for the entity to be part of the result set
(filter sets effectively have an OR between them).
Example:
```text
/entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type
Return entities that match
Filter set 1:
Condition 1: kind = user
AND
Condition 2: metadata.namespace = default
OR
Filter set 2:
Condition 1: kind = group
AND
Condition 2: spec.type exists
```
Each condition is either on the form `<key>`, or on the form `<key>=<value>`.
The first form asserts on the existence of a certain key (with any value), and
the second asserts that the key exists and has a certain value. All checks are
always case _insensitive_.
In all cases, the key is a simplified JSON path in a given piece of entity data.
Each part of the path is a key of an object, and the traversal also descends
through arrays. There are two special forms:
- Array items that are simple value types (such as strings) match on a key-value
pair where the key is the item as a string, and the value is the string `true`
- Relations can be matched on a `relations.<type>=<targetRef>` form
Let's look at a simplified example to illustrate the concept:
```json
{
"a": {
"b": ["c", { "d": 1 }],
"e": 7
}
}
```
This would match any one of the following conditions:
- `a`
- `a.b`
- `a.b.c`
- `a.b.c=true`
- `a.b.d`
- `a.b.d=1`
- `a.e`
- `a.e=7`
Some more real world usable examples:
- Return all orphaned entities:
`/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true`
- Return all users and groups:
`/entities/by-query?filter=kind=user&filter=kind=group`
- Return all service components:
`/entities/by-query?filter=kind=component,spec.type=service`
- Return all entities with the `java` tag:
`/entities/by-query?filter=metadata.tags.java`
- Return all users who are members of the `ops` group (note that the full
[reference](references.md) of the group is used):
`/entities/by-query?filter=kind=user,relations.memberof=group:default/ops`
#### Full text filtering
TODO
#### Field selection
By default the full entities are returned, but you can pass in a `fields` query
parameter which selects what parts of the entity data to retain. This makes the
response smaller and faster to transfer, and may allow the catalog to perform
more efficient queries.
The query parameter value is a comma separated list of simplified JSON paths
like above. Each path corresponds to the key of either a value, or of a subtree
root that you want to keep in the output. The rest is pruned away. For example,
specifying `?fields=metadata.name,metadata.annotations,spec` retains only the
`name` and `annotations` fields of the `metadata` of each entity (it'll be an
object with at most two keys), keeps the entire `spec` unchanged, and cuts out
all other roots such as `relations`.
Some more real world usable examples:
- Return only enough data to form the full ref of each entity:
`/entities/by-query?fields=kind,metadata.namespace,metadata.name`
#### Ordering
By default the entities are returned ordered by their internal uid. You can
customize the `orderField` query parameters to affect that ordering.
For example, to return entities by their name:
`/entities/by-query?orderField=metadata.name,asc`
Each parameter can be followed by `asc` for ascending lexicographical order or
`desc` for descending (reverse) lexicographical order.
#### Pagination
You may pass the `cursor` query parameters to perform cursor based pagination
through the set of entities. The value of `cursor` will be returned in the response, under the `pageInfo` property:
```json
"pageInfo": {
"nextCursor": "a-cursor",
"prevCursor": "another-cursor"
}
```
If `nextCursor` exists, it can be used to retrieve the next batch of entities. Following the same approach,
if `prevCursor` exists, it can be used to retrieve the previous batch of entities.
- [`filter`](#filtering), for selecting only a subset of all entities
- [`fields`](#field-selection), for selecting only parts of the full data
structure of each entity
- `limit` for limiting the number of entities returned (20 is the default)
- [`orderField`](#ordering), for deciding the order of the entities
- `fullTextFilter`
**NOTE**: [`filter`, `orderField`, `fullTextFilter`] and `cursor` are mutually exclusive. This means that,
it isn't possible to change any of [`filter`, `orderField`, `fullTextFilter`] when passing `cursor` as query parameters,
as changing any of these properties will affect pagination. If any of `filter`, `orderField`, `fullTextFilter` is specified together with `cursor`, only the latter is taken into consideration.
### `GET /entities`
Lists entities. Supports the following query parameters, described in sections
Lists entities.
**NOTE**: This endpoint is deprecated in favor of `GET /entities/by-query`, which provides a more efficient implementation and cursor based pagination.
The endpoint supports the following query parameters, described in sections
below:
- [`filter`](#filtering), for selecting only a subset of all entities
+11 -11
View File
@@ -46,7 +46,7 @@ action is logged for further investigation.
### Local File (`type: file`) Configurations
In addition to url locations, you can use the `file` location type to bring in content from the local file system. You should only use this for local development, test setups and example data, not for production data.
In addition to url locations, you can use the `file` location type to bring in content from the local file system. You should only use this for local development, test setups, and example data, not for production data.
You are also not able to use placeholders in them like `$text`. You can however reference other files relative to the current file. See the full [catalog example data set here](https://github.com/backstage/backstage/tree/master/packages/catalog-model/examples) for an extensive example.
Here is an example pulling in the `all.yaml` file from the examples folder. Note the use of `../../` to go up two levels from the current execution path of the backend. This is typically `packages/backend/`.
@@ -61,7 +61,7 @@ catalog:
### Integration Processors
Integrations may simply provide a mechanism to handle `url` location type for an
external provider, or they may also include additional processors out of the
external provider or they may also include additional processors out of the
box, such as the GitHub [discovery](../../integrations/github/discovery.md)
processor that scans a GitHub organization for
[entity descriptor files](descriptor-format.md).
@@ -78,10 +78,10 @@ Backstage's descriptor format. This is documented in
## Catalog Rules
By default the catalog will only allow ingestion of entities with the kind
`Component`, `API` and `Location`. In order to allow entities of other kinds to
By default, the catalog will only allow the ingestion of entities with the kind
`Component`, `API`, and `Location`. In order to allow entities of other kinds to
be added, you need to add rules to the catalog. Rules are added either in a
separate `catalog.rules` key, or added to statically configured locations.
separate `catalog.rules` key or added to statically configured locations.
For example, given the following configuration:
@@ -99,7 +99,7 @@ catalog:
We are able to add entities of kind `Component`, `API`, `Location`, or
`Template` from any location, and `Group` entities from the `org-data.yaml`,
which will also be read as statically configured location.
which will also be read as a statically configured location.
Note that if the `catalog.rules` key is present it will replace the default
value, meaning that you need to add rules for the default kinds if you want
@@ -115,10 +115,10 @@ catalog:
## Readonly mode
Processors provides a good way to automate ingestion of entities when combined
Processors provide a good way to automate the ingestion of entities when combined
with [Static Location Configuration](#static-location-configuration) or a
discovery processor like
[GitHub Discovery](../../integrations/github/discovery.md). To enforce usage of
[GitHub Discovery](../../integrations/github/discovery.md). To enforce the usage of
processors to locate entities we can configure the catalog into `readonly` mode.
This configuration disables registering and deleting locations with the catalog APIs.
@@ -127,7 +127,7 @@ catalog:
readonly: true
```
> **Note that any plugin relying on the catalog API for creating, updating and
> **Note that any plugin relying on the catalog API for creating, updating, and
> deleting entities will not work in this mode.**
Deleting an entity by UUID, `DELETE /entities/by-uid/:uid`, is allowed when using this mode. It may be rediscovered as noted in [explicit deletion](life-of-an-entity.md#explicit-deletion).
@@ -140,9 +140,9 @@ plugin.
## Clean up orphaned entities
In short entities can become orphaned through multiple means, such as when a catalog-info YAML file is moved from one place to another in the version control system without updating the registration in the catalog. For safety reasons the default behavior is to just tag the orphaned entities, and keep them around. You can read more about orphaned entities [here](life-of-an-entity.md#orphaning).
In short, entities can become orphaned through multiple means, such as when a catalog-info YAML file is moved from one place to another in the version control system without updating the registration in the catalog. For safety reasons, the default behavior is to just tag the orphaned entities, and keep them around. You can read more about orphaned entities [here](life-of-an-entity.md#orphaning).
However, if you do with to automatically remove the orphaned entities, you can use the following configuration, and everything with an orphaned entity tag will be eventually deleted.
However, if you do wish to automatically remove the orphaned entities, you can use the following configuration, and everything with an orphaned entity tag will be eventually deleted.
```yaml
catalog:
@@ -782,7 +782,7 @@ Describes the following entity kind:
An API describes an interface that can be exposed by a component. The API can be
defined in different formats, like [OpenAPI](https://swagger.io/specification/),
[AsyncAPI](https://www.asyncapi.com/docs/specifications/latest/),
[AsyncAPI](https://www.asyncapi.com/docs/reference/specification/latest),
[GraphQL](https://graphql.org/learn/schema/),
[gRPC](https://developers.google.com/protocol-buffers), or other formats.
@@ -838,7 +838,7 @@ The current set of well-known and common values for this field is:
- `openapi` - An API definition in YAML or JSON format based on the
[OpenAPI](https://swagger.io/specification/) version 2 or version 3 spec.
- `asyncapi` - An API definition based on the
[AsyncAPI](https://www.asyncapi.com/docs/specifications/latest/) spec.
[AsyncAPI](https://www.asyncapi.com/docs/reference/specification/latest) spec.
- `graphql` - An API definition based on
[GraphQL schemas](https://spec.graphql.org/) for consuming
[GraphQL](https://graphql.org/) based APIs.
@@ -341,7 +341,7 @@ the three-dots menu option of entity views does offer this option, and the
orphaned status can be seen in an info box at the top of the entity's overview
page.
However, if you were to try to do an explicit depletion on an entity that's
However, if you were to try to do an explicit deletion on an entity that's
being kept actively updated by a parent entity, it would just reappear again
shortly thereafter when the processing loops reconsider the parent entity that's
still in there.
@@ -114,6 +114,6 @@ configured differently should be running on `/catalog-import`.
For information about writing your own templates, you can check out the docs
[here](./writing-templates.md)
If you are looking for a method to discover templates without the need for manual ingestion, there are several options available. One approach is to utilize Discovery providers, such as [GitHub Discovery](https://backstage.io/docs/integrations/github/discover).
If you are looking for a method to discover templates without the need for manual ingestion, there are several options available. One approach is to utilize Discovery providers, such as [GitHub Discovery](https://backstage.io/docs/integrations/github/discovery).
Alternatively, you can choose to set up an external integration. This involves connecting your system to external sources or platforms that may host templates relevant to your needs, as mentioned in [External Integration](https://backstage.io/docs/features/software-catalog/external-integrations/).
@@ -128,10 +128,10 @@ class ExamplePermissionPolicy implements PermissionPolicy {
}
```
With this permission policy, the user `spiderman` won't be able to execute the debug:log action.
With this permission policy, the user `spiderman` won't be able to execute the `debug:log` action.
You can also restrict the input provided to the action by combining multiple rules.
In the example below, `spiderman` won't be able to execute debug:log when passing `{ "message": "not-this!" }` as action input:
In the example below, `spiderman` won't be able to execute `debug:log` when passing `{ "message": "not-this!" }` as action input:
```ts title="packages/backend/src/plugins/permission.ts"
/* highlight-add-start */
@@ -20,6 +20,11 @@ locations like GitHub or GitLab.
> Be sure to have covered
> [Getting Started with Backstage](../../getting-started) before proceeding.
> Note: if you're running Backstage with Node 20 or later, you'll need to pass the flag `--no-node-snapshot` to Node in order to
> use the templates feature.
> One way to do this is to specify the `NODE_OPTIONS` environment variable before starting Backstage:
> `export NODE_OPTIONS=--no-node-snapshot`
The Software Templates are available under `/create`. For local development you
should be able to reach them at `http://localhost:3000/create`.
@@ -0,0 +1,135 @@
---
id: migrating-to-rjsf-v5
title: 'Migrating to react-jsonschema-form@v5'
# prettier-ignore
description: Docs on migrating to `react-jsonschema-form`@v5 and the new designs
---
> Note: If you were previously using the `/alpha` imports to test out the `scaffolder/next` work, those imports have been promoted to the default exports from the respective packages. You should just have to remove the `/alpha` from the import path, and remove the `Next` from the import name. `NextScaffolderPage` -> `ScaffolderPage`, `createNextScaffolderFieldExtension` -> `createScaffolderFieldExtension` etc.
## What's `react-jsonschema-form`?
This library is core to the frontend part of the scaffolder plugin, and is responsible for rendering the form in which developers and end users fill out to meet the `jsonschema` requirement for the parameters section.
Since the initial release of the `scaffolder` plugin, we we're on a pretty old version of `react-jsonschema-form` (v3), which has been pretty outdated as of late. The problem with us just bumping this library was that there are several breaking changes with the new v5 version, which we've tried pretty aggressively not to pass on to our end users for their templates and [Custom Field Extensions](https://backstage.io/docs/features/software-templates/writing-custom-field-extensions/).
We're hoping that by duplicating the types from version 3 of `react-jsonschema-form` and making these the types that we will support even though the underlying library is v5, it should get us through all of the breaking changes without passing that down.
## What's new?
With that in mind, this release has `v5` of `react-jsonschema-form`, and with that comes all the new features and bugfixes in `v4` that we were waiting for - one of the main ones being the ability to use `if / then / else` syntax in the `template.yaml` definitions! 🎉
We've also rebuilt how validation works in the `scaffolder` components, which now means that we've opened the ability to have `async` validation functions in your `Field Extensions`.
Some of the pages have gotten a little bit of an overhaul in terms of UI based on some research and feedback from the community and internally.
- The `TemplateList` page has gotten some new `Card` components which show a little more information than the previous version with a little `material-ui` standards.
- The `WizardPage` has received some new updates with the stepper now running horizontally, and the `Review` step being a dedicated step in the stepper.
- The `OngoingTask` page now does not show the logs by default, and instead has a much cleaner interface for tracking the ongoing steps and the pipeline of actions that are currently showing.
- You can also now provide your own `OutputsComponent` which can be used to render the outputs from an ongoing / completed task in a way that suits your templates the best. For instance, if your template produces `Pull Requests`, it could be useful to render these in an interactive way where you can see the statuses of each of these `Pull Requests` in the `Ongoing Task` page.
There's also a lot of bug fixes, and other things, but these are the main ones that we wanted to highlight.
## How do I upgrade
With the release of [`v1.20.0`](https://github.com/backstage/backstage/releases/tag/v1.20.0) these changes should have been made for you. We're hoping that it should be pretty transparent, and things just work as expected. Please reach out to us on [discord](https://discord.com/invite/MUpMjP2) or in a [issue](https://github.com/backstage/backstage/issues/new?assignees=&labels=bug&projects=&template=bug.yaml&title=%F0%9F%90%9B+Bug+Report%3A+%3Ctitle%3E) if you're having issues.
It's possible that if you have a hard dependency on any of the `@rjsf/*` libraries in your app, you'll need to bump these manually to the version that we currently support: `5.13.6` at the time of writing. There could be breaking changes that you will have to fix here however, which we think should be pretty simple, but they're things like changing imports from `@rjsf/core` to `@rjsf/utils`.
```ts
/* highlight-remove-next-line */
import { FieldValidation } from '@rjsf/core';
/* highlight-add-next-line */
import { FieldValidation } from '@rjsf/utils;
```
## Escape hatch
If for some reason the upgrade to [`v1.20.0`](https://github.com/backstage/backstage/releases/tag/v1.20.0) didn't go as planned, there's an escape hatch for use until the next mainline release in which we will try to get any issues fixed before removing the legacy code.
We've moved some of the older exports to an `/alpha` export so you should be able switch to using the old library just in case.
```tsx
/* highlight-remove-next-line */
import { ScaffolderPage } from '@backstage/plugin-scaffolder';
/* highlight-add-next-line */
import { LegacyScaffolderPage } from '@backstage/plugin-scaffolder/alpha';
```
And this API should be the exact same as the previous Router, so you should be able to make a change like the following further down in this file:
```tsx
<Route
path="/create"
element={
{/* highlight-remove-next-line */}
<ScaffolderPage
{/* highlight-add-next-line */}
<LegacyScaffolderPage
groups={[
{
title: 'Recommended',
filter: entity =>
entity?.metadata?.tags?.includes('recommended') ?? false,
},
]}
/>
}
>
<ScaffolderFieldExtensions>
<LowerCaseValuePickerFieldExtension />
{/* ... other extensions */}
</ScaffolderFieldExtensions>
<ScaffolderLayouts>
<TwoColumnLayout />
{/* ... other layouts */}
</ScaffolderLayouts>
</Route>
```
And you can also update any of your `CustomFieldExtensions` to use the old helper like so:
```ts
/* highlight-remove-next-line */
import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder';
/* highlight-add-next-line */
import { createLegacyScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react/alpha';
export const EntityNamePickerFieldExtension = scaffolderPlugin.provide(
/* highlight-remove-next-line */
createScaffolderFieldExtension({
/* highlight-add-next-line */
createLegacyScaffolderFieldExtension({
component: EntityNamePicker,
name: 'EntityNamePicker',
validation: entityNamePickerValidation,
}),
);
```
And in the component themselves, you might have to do the following:
```tsx
/* highlight-remove-next-line */
import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';
/* highlight-add-next-line */
import { LegacyFieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react/alpha';
export const EntityNamePicker = (
/* highlight-remove-next-line */
props: FieldExtensionComponentProps<string, EntityNamePickerProps>,
/* highlight-add-next-line */
props: LegacyFieldExtensionComponentProps<string, EntityNamePickerProps>,
) => {
const {
onChange,
required,
schema: { title = 'Name', description = 'Unique name of the component' },
rawErrors,
formData,
idSchema,
placeholder,
} = props;
// ..
};
```
@@ -1,276 +0,0 @@
---
id: testing-scaffolder-alpha
title: 'Experimental: Testing out the alpha Scaffolder plugin'
# prettier-ignore
description: Docs on the upcoming breaking release for the scaffolder plugin
---
## What's `scaffolder/next`?
The `alpha` version, or as you might have seen referred to in other places the `scaffolder/next` release, is a new version of the `scaffolder` plugin that will be the first breaking change to the plugin, so you can also think of it as `@backstage/plugin-scaffolder@2.0.0`.
Its mostly a rewrite of a lot of the frontend components and pages that had very limited test coverage, which made adding new features to the `scaffolder` plugin quite hard, and we were lacking in confidence when making changes.
There is of course some other things that have changed when re-writing this, which are essentially what has caused some breaking changes.
Now, this is not like previous scaffolder changes where you would have to change all of your templates as this is only the frontend plugin that is going to have breaking changes. You can read more about the [breaking changes](#breaking-changes) below.
## What's new?
First off, the main dependency that we have for the frontend which is responsible for rendering the `JSONSchema` into `material-ui` components is [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form).
This dependency in the current version of the plugin is 3.x.x, which is now 2 major versions out of date. Long story short, `v4` of this plugin contained some bug fixes, and new features but we we're unable to upgrade due to some issues with having support for `material-ui@v4`, so we had to wait for `v5` to be released, and because of the `FieldExtensions` and how they are very tightly coupled to the `react-jsonschema-form` library, we also wanted to make sure that this release was stable before getting people to migrate their `Field Extensions`.
With that in mind, this release has `v5` of `react-jsonschema-form`, and with that comes all the new features and bugfixes in `v4` that we were waiting for - one of the main ones being the ability to use `if / then / else` syntax in the `template.yaml` definitions! 🎉
We've also rebuilt how validation works in the `scaffolder` components, which now means that we've opened the ability to have `async` validation functions in your `Field Extensions`.
Some of the pages have gotten a little bit of an overhaul in terms of UI based on some research and feedback from the community and internally.
- The `TemplateList` page has gotten some new `Card` components which show a little more information than the previous version with a little `material-ui` standards.
- The `WizardPage` has received some new updates with the stepper now running horizontally, and the `Review` step being a dedicated step in the stepper.
- The `OngoingTask` page now does not show the logs by default, and instead has a much cleaner interface for tracking the ongoing steps and the pipeline of actions that are currently showing.
- You can also now provide your own `OutputsComponent` which can be used to render the outputs from an ongoing / completed task in a way that suits your templates the best. For instance, if your template produces `Pull Requests`, it could be useful to render these in an interactive way where you can see the statuses of each of these `Pull Requests` in the `Ongoing Task` page.
There's also a lot of bug fixes, and other things, but these are the main ones that we wanted to highlight.
## How do I test out the `alpha` version?
With the release of [`v1.11.0`](https://github.com/backstage/backstage/releases/tag/v1.11.0) it's now possible to run the `scaffolder/next` plugin and it be a drop in replacement for the current version that you use today. This means that you can start using the new code, and start testing it out. Once we have collected enough feedback, and squashed any bugs that might block us from releasing, it will be promoted from the `/alpha` exports and replace the existing code leading to breaking changes if you haven't already made these changes as part of this testing pilot. Those that have chosen to opt into this testing pilot means that once we promote it from the `/alpha` exports, you will need to update your code to point to the original exports from the `scaffolder` plugin, just like the code is today but with the [breaking changes](#breaking-changes) that you already made to your `Custom Field Extensions`.
It's also worth calling out that if you do test this out, and find some issues or something not working out as expected, feel free to raise an issue in the [repo](https://github.com/backstage/backstage) or reach out to us on Discord!
### Make the required changes to `App.tsx`
The `ScaffolderPage` router has a completely different export for the `scaffolder/next` work, so you will want to change any import from the old `ScaffolderPage` to the new `NextScaffolderPage`
```tsx
/* highlight-remove-next-line */
import { ScaffolderPage } from '@backstage/plugin-scaffolder';
/* highlight-add-next-line */
import { NextScaffolderPage } from '@backstage/plugin-scaffolder/alpha';
```
And this API should be the exact same as the previous Router, so you should be able to make a change like the following further down in this file:
```tsx
<Route
path="/create"
element={
{/* highlight-remove-next-line */}
<ScaffolderPage
{/* highlight-add-next-line */}
<NextScaffolderPage
groups={[
{
title: 'Recommended',
filter: entity =>
entity?.metadata?.tags?.includes('recommended') ?? false,
},
]}
/>
}
>
<ScaffolderFieldExtensions>
<LowerCaseValuePickerFieldExtension />
{/* ... other extensions */}
</ScaffolderFieldExtensions>
<ScaffolderLayouts>
<TwoColumnLayout />
{/* ... other layouts */}
</ScaffolderLayouts>
</Route>
```
Optionally, you can choose to run the two side by side by using `FeatureFlags` in your `App.tsx` if you wish, but, we would also recommend duplicating any `CustomFieldExtensions` too as the new `CustomFieldExtensions` might not be compatible with the old form.
```tsx
<FeatureFlagged with="scaffolder-next-preview">
<Route path="/create" element={<NextScaffolderPage />}>
<ScaffolderFieldExtensions>
<DelayingComponentFieldExtension />
</ScaffolderFieldExtensions>
</Route>
</FeatureFlagged>
<FeatureFlagged without="scaffolder-next-preview">
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderFieldExtensions>
<DelayingComponentFieldExtension />
</ScaffolderFieldExtensions>
</Route>
</FeatureFlagged>
```
You should then be able to enable the `scaffolder-next-preview` feature flag under `/settings/feature-flags` in Backstage.
### Make the required changes to your `CustomFieldExtensions`
There's differently named function for creating field extensions part of the `/alpha` exports as these are the ones that can contain breaking changes because of the breaking changes that have been applied in `react-jsonschema-form`.
Let's take the following example:
```ts
export const EntityNamePickerFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
component: EntityNamePicker,
name: 'EntityNamePicker',
validation: entityNamePickerValidation,
schema: EntityNamePickerSchema,
}),
);
```
References for `createScaffolderFieldExtension` have an `/alpha` version of `createNextScaffolderFieldExtension`, which should be used instead.
```ts
/* highlight-remove-next-line */
import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder';
/* highlight-add-next-line */
import { createNextScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react/alpha';
export const EntityNamePickerFieldExtension = scaffolderPlugin.provide(
/* highlight-remove-next-line */
createScaffolderFieldExtension({
/* highlight-add-next-line */
createNextScaffolderFieldExtension({
component: EntityNamePicker,
name: 'EntityNamePicker',
validation: entityNamePickerValidation,
}),
);
```
Once you've done this you will find that you will have two squiggly lines under the properties that are passed in. One for the component and one for the validation (if provided.)
Let's take the following code for the `EntityNamePicker` component:
```tsx
export const EntityNamePicker = (
props: FieldExtensionComponentProps<string, EntityNamePickerProps>,
) => {
const {
onChange,
required,
schema: { title = 'Name', description = 'Unique name of the component' },
rawErrors,
formData,
uiSchema: { 'ui:autofocus': autoFocus },
idSchema,
placeholder,
} = props;
// ..
};
```
There's another `/alpha` export that you need to replace `FieldExtensionComponentProps` with which is the `NextFieldExtensionComponentProps`.
```tsx
/* highlight-remove-next-line */
import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';
/* highlight-add-next-line */
import { NextFieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react/alpha';
export const EntityNamePicker = (
/* highlight-remove-next-line */
props: FieldExtensionComponentProps<string, EntityNamePickerProps>,
/* highlight-add-next-line */
props: NextFieldExtensionComponentProps<string, EntityNamePickerProps>,
) => {
const {
onChange,
required,
schema: { title = 'Name', description = 'Unique name of the component' },
rawErrors,
formData,
/* highlight-remove-next-line */
uiSchema: { 'ui:autofocus': autoFocus },
/* highlight-add-next-line */
uiSchema: { 'ui:autofocus': autoFocus } = {},
idSchema,
placeholder,
} = props;
// ..
};
```
You'll notice that there's an additional change here, which is that we're now defaulting the `uiSchema` to an empty object. This is because the `uiSchema` is now optional, and if you don't provide it, it will be `undefined` instead of an empty object. There's more around this in the [breaking changes](#breaking-changes) section.
To fix the previous validation error, you will need to change the import for the `FieldValidation` type that is used in the `validation` function.
Let's take the following example of the validation function:
```ts
import { FieldValidation } from '@rjsf/utils';
import { KubernetesValidatorFunctions } from '@backstage/catalog-model';
export const entityNamePickerValidation = (
value: string,
validation: FieldValidation,
) => {
if (!KubernetesValidatorFunctions.isValidObjectName(value)) {
validation.addError(
'Must start and end with an alphanumeric character, and contain only alphanumeric characters, hyphens, underscores, and periods. Maximum length is 63 characters.',
);
}
};
```
You will need to change the import for `FieldValidation` to point at the new `react-jsonschema-form` dependency.
> Note: you will probably need to install this dependency too, by using `yarn add @rjsf/utils` in the package where you define these validation functions, this could also be in the `packages/app` folder, so you can install it there if needed.
```ts
/* highlight-remove-next-line */
import { FieldValidation } from '@rjsf/core';
/* highlight-add-next-line */
import { FieldValidation } from '@rjsf/utils;
import { KubernetesValidatorFunctions } from '@backstage/catalog-model';
export const entityNamePickerValidation = (
value: string,
validation: FieldValidation,
) => {
```
## Breaking Changes
Once we fully release the code that is in the `/alpha` exports right now onto the current API and release v2.0.0 of `@backstage/plugin-scaffolder` the breaking changes will be as follows:
### `uiSchema` is now optional
Later releases of `react-jsonschema-form` have made the `uiSchema` optional, and if you don't provide it, it will be `undefined` instead of an empty object. This means that you will need to make sure that you're defaulting the `uiSchema` to an empty object if you're using it in your code.
```tsx
const {
onChange,
required,
schema: { title = 'Name', description = 'Unique name of the component' },
rawErrors,
formData,
/* highlight-remove-next-line */
uiSchema: { 'ui:autofocus': autoFocus },
/* highlight-add-next-line */
uiSchema: { 'ui:autofocus': autoFocus } = {},
idSchema,
placeholder,
} = props;
// ..
```
### `formData` can also be `undefined`
If you were using the `formData` and assuming that it was set to an empty object when building `Field Extensions` that return objects, then this will be `undefined` now due to a change in the `react-jsonschema-form` library.
```tsx
const {
onChange,
required,
schema: { title = 'Name', description = 'Unique name of the component' },
rawErrors,
/* highlight-remove-next-line */
formData,
/* highlight-add-next-line */
formData = {}, // or maybe some other default value that you would prefer
uiSchema: { 'ui:autofocus': autoFocus } = {},
idSchema,
placeholder,
} = props;
// ..
```
@@ -231,8 +231,53 @@ spec:
inputType: tel
```
### Using Secrets
You may want to mark things as secret and make sure that these values are protected and not available through REST endpoints. You can do this by using the built in `ui:field: Secret`.
You can define this property as any normal parameter, however the consumption of this parameter will not be available through `${{ parameters.myKey }}` you will instead need to use `${{ secrets.myKey }}` in your `template.yaml`.
Parameters will be automatically masked in the review step.
```yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: v1beta3-demo
title: Test Action template
description: scaffolder v1beta3 template demo
spec:
owner: backstage/techdocs-core
type: service
parameters:
- title: Authenticaion
description: Provide authentication for the resource
required:
- username
- password
properties:
username:
type: string
# use the built in Secret field extension
ui:field: Secret
password:
type: string
ui:field: Secret
steps:
- id: setupAuthentication
action: auth:create
input:
# make sure to use ${{ secrets.parameterName }} to reference these values
username: ${{ secrets.username }}
password: ${{ secrets.password }}
```
### Hide or mask sensitive data on Review step
> Note: this approach is soon to be deprecated, please mark things as secret by using the `Secret` field extension instead as mentioned above.
Sometimes, specially in custom fields, you collect some data on Create form that
must not be shown to the user on Review step. To hide or mask this data, you can
use `ui:widget: password` or set some properties of `ui:backstage`:
+1
View File
@@ -68,6 +68,7 @@ See [TechDocs Architecture](architecture.md) to get an overview of where the bel
| Gerrit | Yes ✅ |
| GitLab | Yes ✅ |
| GitLab Enterprise | Yes ✅ |
| Gitea | Yes ✅ |
### File storage providers
+4
View File
@@ -91,6 +91,9 @@ Options:
--docker-option <DOCKER_OPTION...> Extra options to pass to the docker run command, e.g. "--add-host=internal.host:192.168.11.12"
(can be added multiple times).
--no-docker Do not use Docker, use MkDocs executable in current user environment.
--mkdocs-parameter-clean Pass "--clean" parameter to mkdocs server running in containerized environment.
--mkdocs-parameter-dirtyreload Pass "--dirtyreload" parameter to mkdocs server running in containerized environment.
--mkdocs-parameter-strict Pass "--strict" parameter to mkdocs server running in containerized environment.
--mkdocs-port <PORT> Port for MkDocs server to use (default: "8000")
--preview-app-bundle-path <PATH_TO_BUNDLE> Preview documentation using a web app other than the included one.
--preview-app-port <PORT> Port where the preview will be served.
@@ -147,6 +150,7 @@ Options:
Defaults to false, which means that the techdocs-core plugin is always added to the mkdocs file.
--legacyCopyReadmeMdToIndexMd Attempt to ensure an index.md exists falling back to using <docs-dir>/README.md or README.md
in case a default <docs-dir>/index.md is not provided. (default: false)
--runAsDefaultUser Bypass setting the container user as the same user and group id as host for Linux and MacOS (default: false)
-v --verbose Enable verbose output. (default: false)
-h, --help display help for command
```
@@ -9,8 +9,8 @@ This section will guide you through how to:
- [Create a basic documentation setup](#create-a-basic-documentation-setup)
- [Use any software template](#use-any-software-template)
- [Use the documentation template](#use-the-documentation-template)
- [Enable documentation for an already existing entity](#enable-documentation-for-an-already-existing-entity)
- [Create a standalone documentation](#create-a-standalone-documentation)
- [Writing and previewing your documentation](#writing-and-previewing-your-documentation)
## Prerequisites
@@ -41,23 +41,6 @@ default, we highly recommend you to set that up. Follow our how-to guide
[How to add documentation setup to your software templates](./how-to-guides.md#how-to-add-the-documentation-setup-to-your-software-templates)
to get started.
### Use the documentation template
There could be _some_ situations where you don't want to keep your docs close to
your code, but still want to publish documentation - for example, an onboarding
tutorial. For this use case, we have put together a documentation template. Your
Backstage instance should by default have a documentation template added. If
not, copy the catalog locations from the
[create-app template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/app-config.yaml.hbs)
to add the documentation template. The template creates a component with
**only** TechDocs configuration and default markdown files, and is otherwise
empty.
![Documentation Template](../../assets/techdocs/documentation-template.png)
Create an entity from the documentation template and you will get the needed
setup for free.
### Enable documentation for an already existing entity
Prerequisites:
@@ -115,6 +98,56 @@ This is a basic example of documentation.
Commit your changes, open a pull request and merge. You will now get your
updated documentation next time you run Backstage!
### Create a standalone documentation
There could be _some_ situations where you don't want to keep your docs close to
your code, but still want to publish documentation - for example, an onboarding
tutorial. For this case, you can create a documentation component, which will be
published as a standalone part of TechDocs.
First, create an entity for your documentation. A minimal example could look like
this:
```yaml title="catalog-info.yaml"
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: a-unique-name-for-your-docs
annotations:
# this could also be `url:<url>` if the documentation isn't in the same location
backstage.io/techdocs-ref: dir:.
spec:
type: documentation
lifecycle: experimental
owner: user-or-team-name
```
Next, create the config file for [mkdocs](https://www.mkdocs.org/), which will be
used to parse your docs:
```yaml title="mkdocs.yml"
site_name: a-unique-name-for-your-docs
site_description: An informative description
plugins:
- techdocs-core
nav:
- Getting Started: index.md
```
Finally, add your index.md Markdown file, in a folder named `docs/` with your desired
documentation in Markdown. Your file structure should now look like this:
```
your-great-documentation/
docs/
index.md
catalog-info.yaml
mkdocs.yml
```
Last but not least, register your component in the software catalog using
[one of several options](../software-catalog/index.md#adding-components-to-the-catalog).
## Writing and previewing your documentation
Using the [techdocs-cli](https://github.com/backstage/backstage/tree/master/packages/techdocs-cli) you can
+12
View File
@@ -504,6 +504,18 @@ folder (/docs) or replace the content in this file.
Done! You now have support for TechDocs in your own software template!
### Prevent download of Google fonts
If your Backstage instance does not have internet access, the generation will fail. TechDocs tries to download the Roboto font from Google. You can disable it by adding the following lines to mkdocs.yaml:
```yaml
theme:
name: material
font: false
```
> Note: The addition `name: material` is necessary. Otherwise it will not work
## How to enable iframes in TechDocs
TechDocs uses the [DOMPurify](https://github.com/cure53/DOMPurify) library to
@@ -6,7 +6,7 @@ sidebar_label: Overview
description: The structure and architecture of the new Frontend System
---
> **NOTE: The new frontend system is in a highly experimental phase**
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
## Building Blocks
+34 -3
View File
@@ -1,12 +1,12 @@
---
id: apps
id: app
title: App Instances
sidebar_label: App
# prettier-ignore
description: App instances
---
> **NOTE: The new frontend system is in a highly experimental phase**
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
## The App Instance
@@ -45,4 +45,35 @@ A common type of data that is shared between extensions is React elements and co
## Feature Discovery
TODO
App feature discovery lets you automatically discover and install features provided by dependencies in your app. In practice, it means that you don't need to manually `import` features in code, but they are instead installed as soon as you add them as a dependency in your `package.json`.
Because feature discovery needs to interact with the compilation process, it is only available when using the `@backstage/cli` to build your app. It is hooked into the WebPack compilation process by scanning your app package for compatible dependencies, which are then made part of the app compilation bundle.
Since the `@backstage/cli` is a more stable component than the new frontend system, feature discovery is currently marked as an experimental feature of the CLI and needs to be enabled manually. To enable it, add the following configuration to your `app-config.yaml`:
```yaml
app:
experimental:
packages: all
```
This will cause all dependencies in your app package to be installed automatically. If this is not desired, you can use include or exclude filters to narrow down the set of packages:
```yaml
app:
experimental:
packages:
# Only the following packages will be included
include:
- '@backstage/plugin-catalog'
- '@backstage/plugin-scaffolder'
---
app:
experimental:
packages:
# All but the following package will be included
exclude:
- '@backstage/plugin-catalog'
```
Note that you do not need to manually exclude packages that you also import explicitly in code, since plugin instances are deduplicated by the app. You will never end up with duplicate plugin installations except if they are in fact two different plugin instances with different IDs.
@@ -6,7 +6,7 @@ sidebar_label: Extensions
description: Frontend extensions
---
> **NOTE: The new frontend system is in a highly experimental phase**
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
As mentioned in the [previous section](./02-app.md), Backstage apps are built up from a tree of extensions. This section will go into more detail about what extensions are, how to create and use them, and how to create your own extensibility patterns.
@@ -18,6 +18,10 @@ Each extensions has a number of different properties that define how it behaves
### ID
<!--
Update this to be 3 different sections: name, kind and namespace
-->
The ID of an extension is used to uniquely identity it, and it should ideally by unique across the entire Backstage ecosystem. For each frontend app instance there can only be a single extension for any given ID. Installing multiple extensions with the same ID will either result in an error or one of the extensions will override the others. The ID is also used to reference the extensions from other extensions, in configuration, and in other places such as developer tools and analytics.
### Output
@@ -56,7 +60,7 @@ Extensions are created using the `createExtension` function from `@backstage/fro
```tsx
const extension = createExtension({
id: 'my-extension',
name: 'my-extension',
// This is the attachment point, `id` is the ID of the parent extension,
// while `input` is the name of the input to attach to.
attachTo: { id: 'my-parent', input: 'content' },
@@ -67,10 +71,10 @@ const extension = createExtension({
element: coreExtensionData.reactElement,
},
// This factory is called to instantiate the extensions and produce its output.
factory({ bind }) {
bind({
factory() {
return {
element: <div>Hello World</div>,
});
};
},
});
```
@@ -81,24 +85,288 @@ Note that while the `createExtension` is public API and used in many places, it
## Extension Data
TODO
Communication between extensions happens in one direction, from one child extension through the attachment point to its parent. The child extension outputs data which is then passed as inputs to the parent extension. This data is called Extension Data, where the shape of each individual piece of data is described by an Extension Data Reference. These references are created separately from the extensions themselves, and can be shared across multiple different kinds of extensions. Each reference consists of an ID and a TypeScript type that the data needs to conform to, and represents one type of data that can be shared between extensions.
### Extension Data References
To create a new extension data reference to represent a type of shared extension data you use the `createExtensionDataRef` function. When defining a new reference you need to provide an ID and a TypeScript type, for example:
```ts
export const reactElementExtensionDataRef =
createExtensionDataRef<React.JSX.Element>('my-plugin.reactElement');
```
The `ExtensionDataRef` can then be used to describe an output property of the extension. This will enforce typing on the return value of the extension factory:
```tsx
const extension = createExtension({
// ...
output: {
element: reactElementExtensionDataRef,
},
factory() {
return {
element: <div>Hello World</div>,
};
},
});
```
### Extension Data Uniqueness
Note that the key used in the output map, in this case `element`, is only used internally within the definition of the extension itself. That actual identifier for the data when consumed by other extensions is the ID of the reference, in this case `core.reactElement`. This means that you can not output multiple different values for the same extension data reference, as they would conflict with each other. That in turn makes overly generic extension data references a bad idea, for example a generic "string" type. Instead create separate references for each type of data that you want to share.
```tsx
const extension = createExtension({
// ...
output: {
// ❌ Bad example - outputting values of same type
element1: reactElementExtensionDataRef,
element2: reactElementExtensionDataRef,
},
factory() {
return {
element1: <div>Hello World</div>,
element2: <div>Hello World</div>,
};
},
});
```
### Core Extension Data
We provide default `coreExtensionData`, which provides commonly used `ExtensionDataRef`s - e.g. for `React.JSX.Element` and `RouteRef`. They can be used when creating your own extension. For example, the React Element extension data that we defined above is already provided as `coreExtensionData.reactElement`.
<!-- For a full list and explanations of all types of core extension data, see the [core extension data reference](#TODO). -->
### Optional Extension Data
By default all extension data is required, meaning that the extension factory must provide a value for each output. However, it is possible to make extension data optional by calling the `.optional()` method. This makes it optional for the factory function to return a value as part of its output. When calling the `.optional()` method you create a new copy of the extension data reference, it does not mutate the existing reference.
```tsx
const extension = createExtension({
// ...
output: {
element: coreExtensionData.reactElement.optional(),
},
factory() {
return {
element:
Math.random() < 0.5 ? <img src="./assets/logo.png" /> : undefined,
};
},
});
```
## Extension Inputs
TODO
The Extension Data can be passed up to other extensions through their extension inputs. Similar to the outputs seen before, let's create an example an extension with a extension input:
## Configuration
```tsx
const navigationExtension = createExtension({
// ...
inputs: {
// [1]: Input
logo: createExtensionInput(
{
element: coreExtensionData.reactElement,
},
{ singleton: true, optional: true },
),
},
factory({ inputs }) {
return {
element: (
<nav>{inputs.logo.output?.element ?? <span>Backstage</span>}</nav>
),
};
},
// ...
});
```
TODO
The input (see [1] above) is an object that we create using `createExtensionInput`. The first argument is the set of extension data that we accept via this input, and works just like the `output` option. The second argument is optional, and it allows us to put constraints on the extensions that are attached to our input. If the `singleton: true` option is set, only a single extension can attached at a time, and unless the `optional: true` option is set it will also be required that there is exactly on attached extension.
## Configuration Schema
So how can we now attach the output to the parent extension's input? If we think about a navigation component, like the Sidebar in Backstage, there might be plugins that want to attach a link to their plugin to this navigation component. In this case the plugin only needs to know the extension `id` and the name of the extension `input` to attach the extension `output` returned by the `factory` to the specified extension:
TODO
```tsx
const navigationItemExtension = createExtension({
// ...
attachTo: { id: 'app/nav', input: 'items' },
factory() {
return {
element: <Link to="/home">Home</Link>,
};
},
});
const navigationExtension = createExtension({
// ...
// [2]: Extension `id` will be `app/nav` following the extension naming pattern
namespace: 'app',
name: 'nav',
output: {
element: coreExtensionData.reactElement,
},
inputs: {
items: createExtensionInput({
element: coreExtensionData.reactElement,
}),
},
factory({ inputs }) {
return {
element: (
<nav>
<ul>
{inputs.items.map(item => {
return <li>{item.output.element}</li>;
})}
</ul>
</nav>
),
};
},
// ...
});
```
In this case the extension input `items` is an array, where each individual item is an extension that attached itself to the extension inputs of this `id`.
With the `inputs` not only the `output` of an extensions item is passed to the extension, but also the `node`. However, it is discouraged to consume the `node` here unless needed. If we are looking at the `factory` function from the example above we could access the `node` like the following:
```tsx
// ...
factory({ inputs }) {
return {
element: (
<nav>
<ul>
{inputs.items.map(({output, node}) => {
const _node: AppNode = node;
return <li>{output.element}</li>;
})}
</ul>
</nav>
),
};
},
```
## Extension Configuration
With the `app-config.yaml` there is already the option to pass configuration to plugins or the app to e.g. define the `baseURL` of your app. For extensions this concept would be limiting as an extension can be independent of the plugin & initiated several times. Therefor we created a possibility to configure each extension individually through config. The extension config schema is created using the [`zod`](https://zod.dev/) library, which in addition to TypeScript type checking also provides runtime validation and coercion. If we continue with the example of the `navigationExtension` and now want it to contain a configurable title, we could make it available like the following:
```tsx
const navigationExtension = createExtension({
// ...
namespace: 'app',
name: 'nav',
// [3]: Extension `id` will be `app/nav` following the extension naming pattern
configSchema: createSchemaFromZod(z =>
z.object({
title: z.string().default('Sidebar Title'),
}),
),
factory({ config }) {
return {
element: (
<nav>
<span>{config.title}</span>
<ul>{/* ... */}</ul>
</nav>
),
};
},
// ...
});
```
To now change the text of the title from "Sidebar Title" to "Backstage" we can look at the `id` of the extension & add the following to the `app-config.yaml`:
```yaml
app:
# ...
extensions:
# ...
- app/nav:
config:
title: 'Backstage'
```
## Extension Creators
TODO
With creating an extension by using `createExtension(...)` you have the advantage that the extension can be anything in your Backstage application. We realised that this comes with the trade-off of having to repeat boilerplate code for similar building blocks. Here extension creators come into play for covering common building blocks in Backstage like pages using `createPageExtension`, themes using the `createThemeExtension` or items for the navigation using `createNavItemExtension`.
If we follow the example from above all items of the navigation have similarities, like they all want to be attached to the same extension with the same input as well as rendering the same navigation item component. Therefor `createExtension` can be abstracted for this use case to `createNavItemExtension` and if we add the extension to the app it will end up in the right place & looks like we expect a navigation item to look.
```tsx
export const HomeNavIcon = createNavItemExtension({
routeRef: routeRefForTheHomePage,
title: 'Home',
icon: HomeIcon,
});
```
### Extension Kind
With the example `HomeNavIcon` will end up on the extension input `items` of the extensions with the id `app/nav`. It raises the question what the `id` of the `HomeNavIcon` itself is. The extension creator for the navigation item has a defined `kind`, which by convention matches the own name. So in this example `createNavItemExtension` sets the kind to `nav-item`.
The `id` of the extension is then build out of `namespace`, `name` & `kind` like the following - where `namespace` & `name` are optional properties that can be passed to the extension creator:
```
id: kind:namespace/name
```
For more information on naming of extension refer to the [naming patterns documentation](./08-naming-patterns.md).
### Extension Creators in libraries
Extension creators should be exported from frontend library packages (e.g. `*-react`) rather than plugin packages.
If an extension is only for in-house tweaks, it's okay to put it in the plugin package. But if you want other open source plugins to use it, or you already have a `-react` package, always put extension creators in the `-react` package.
## Extension Boundary
TODO
The `ExtensionBoundary` wraps extensions with several React contexts for different purposes
### Suspense
All React elements rendered by extension creators should be wrapped in the extension boundary. With `Suspense` the extension can than load resources asynchronously with having a loading fallback. It also allows to lazy load the whole extension similar to how plugins are currently lazy loaded in Backstage.
### Error Boundary
Similar to plugins the `ErrorBoundary` for extension allows to pass in a fallback component in case there is an uncaught error inside of the component. With this the error can be isolated & it would prevent the rest of the plugin to crash.
### Analytics
Analytics information are provided through the `AnalyticsContext`, which will give `extensionId` & `pluginId` as context to analytics event fired inside of the extension. Additionally `RouteTracker` will capture an analytics event for routable extension to inform which extension metadata gets associated with a navigation event when the route navigated to is a gathered `mountPoint`.
The `ExtensionBoundary` can be used like the following in an extension creator:
```tsx
export function createSomeExtension<
TConfig extends {},
TInputs extends AnyExtensionInputMap,
>(options): ExtensionDefinition<TConfig> {
return createExtension({
// ...
factory({ config, inputs, node }) {
const ExtensionComponent = lazy(() =>
options
.loader({ config, inputs })
.then(element => ({ default: () => element })),
);
return {
path: config.path,
routeRef: options.routeRef,
element: (
<ExtensionBoundary node={node} routable>
<ExtensionComponent />
</ExtensionBoundary>
),
};
},
});
}
```
@@ -6,4 +6,88 @@ sidebar_label: Plugins
description: Frontend plugins
---
> **NOTE: The new frontend system is in a highly experimental phase**
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
## Introduction
<!--
Introduce frontend plugins and how each of them is shipped as a separate package. Highlight that each package should only export a single plugin instance.
-->
## Creating a Plugin
<!--
How to create a simple plugin
-->
```ts
export const myPlugin = createPlugin({
id: 'my-plugin',
});
```
<!--
Note that this plugin is useless in itself, and you need to provide extra options to make it useful, in particular extensions.
-->
### Plugin ID
<!--
link to relevant docs
-->
### Plugin Extensions
<!--
- Example of how this option is used in `createPlugin`
link to relevant docs
-->
### Plugin Routes
<!--
- Example of how this option is used in `createPlugin`
link to relevant docs
-->
### Plugin External Routes
<!--
- Example of how this option is used in `createPlugin`
link to relevant docs
-->
### Plugin Feature Flags
<!--
- Example of how this option is used in `createPlugin`
link to relevant docs
-->
## Installing a Plugin in an App
<!--
Quick intro, but link back to app docs for more details
-->
@@ -1,9 +1,47 @@
---
id: extension overrides
id: extension-overrides
title: Frontend Extension Overrides
sidebar_label: Extension Overrides
# prettier-ignore
description: Frontend extension overrides
---
> **NOTE: The new frontend system is in a highly experimental phase**
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
## Introduction
<!--
Introduce extension overrides and how they can be shipped in separate packages but also internally within a project too.
Talk about how extension overrides
-->
## Creating a Extension Override
<!--
How to create an extension override and export it from a package.A
- Example using the default exports
You can also create them directly withing the app for local overrides.
- Example of installing an override directly into an app
Mention that in can still be a good idea to split your overrides out into separate packages in large projects. But it's up to you to decide how to group the extensions into extension overrides.
-->
## Overriding Existing Extensions
<!--
To override an existing extension you need to provide an extension through an extension override that has the same ID as the existing extension. That is, all of kind, namespace and name must match the extension you want to override.
This means that you typically need to provide an explicit namespace when overriding extensions from a plugin.
- Example of how to create an extension override for an existing extension from some plugin
-->
@@ -6,6 +6,25 @@ sidebar_label: Utility APIs
description: Utility APIs
---
> **NOTE: The new frontend system is in a highly experimental phase**
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
See [Utility APIs docs](../../api/utility-apis.md).
## Overview
Utility APIs are pieces of standalone functionality, interfaces that can be requested by plugins to use. They are defined by a TypeScript interface as well as a reference (an "API ref") used to access its implementation. They can be provided both by plugins and the core framework, and are themselves [extensions](../architecture/03-extensions.md) that can have inputs, be replaced, and be declaratively configured in your app-config.
A common example of a utility API is a client interface to interact with the backend part of a plugin, such as the catalog client. Any frontend plugin can then request an implementation of that interface to make requests through.
The following diagram shows a hypothetical application, which depends on two plugins and also provides some extra overrides. Note that both the plugins and the core framework provide utility APIs, and that they depended on each other. The app also chooses to use its overrides mechanism to supply a replacement implementation of one API, which takes precedence over the default one. Thus, all consumers of that API will be sure to get that new implementation provided to them.
![frontend system utility apis diagram](../../assets/frontend-system/architecture-utility-apis.drawio.svg)
## Extension structure
All utility APIs implement the `createApiExtension.factoryDataRef` output data type, and must attach exclusively to the `core` extension's `apis` input no matter who provided them. These defaults are provided out of the box by the `createApiExtension` framework function.
Since utility APIs are extensions, they can also have inputs in advanced use cases. This is occasionally useful for complex APIs that can themselves be extended with additional programmatic functionality by adopters.
## Links
- The [Utility APIs section](../utility-apis/01-index.md) of the plugin docs
- The legacy docs on [utility APIs](../../api/utility-apis.md)
+140 -1
View File
@@ -6,6 +6,145 @@ sidebar_label: Routes
description: Frontend routes
---
> **NOTE: The new frontend system is in a highly experimental phase**
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
See [routing system docs](../../plugins/composability.md#routing-system)
## Introduction
<!--
Routing system of backstage - what's the prupose
Explain the indirection needed to route between plugins. For example what do we do if plugin A want to link to plugin B, but plugin B is not installed? How can we support this in an app?
How can we let an integrator decice whether plugin A should link to plugin B or plugin C? (with some good real-work examples, e.g. catalog -> scaffolder create page / catalog-import plugin)
-->
## Route References
<!--
Explain what a route reference is - then explain how it solves the problem outlined above.
Explain that we use route references to represent a path in an app. Each referenced path is unique for each app, but in different apps the same route ref might point to different paths.
-->
### Creating a Route Reference
<!--
Example using `createRouteRef` + passing it to a page extension
-->
### Using a Route Reference
<!--
Example using `useRouteRef` in a component
-->
### Route Path Parameters
<!--
Example using path parameters, both createRouteRef and useRouteRef
-->
### Providing Route References to Plugins
<!--
Show how to provide routes through createPlugin({ routes: })
-->
## External Router References
<!--
Explain the need for external route refs
Explain all of create, use and provide
-->
### Binding External Route References
<!--
Example using `createApp`
Example using config
-->
### Optional External Route References
<!--
Talk about how external routes must be bound or app will crash, but you can make them optional too
useRouteRef can return undefined for optional external routes
-->
## Sub Route References
<!--
Explain the need for external route refs - both as a tool for routing within a plugin, but also allow external routes to point to sub routes
Talk about how sub routes declare an explicit path - can't be decided by the app/integrator. They are hard-coded in the plugin in parallel to the internal routing structure of the plugin itself.
Explain all of create, use and provide
-->
```ts
/*
Some examples
export const indexPageRouteRef = createRouteRef()
export const catalogPlugin = createPlugin({
id: 'catalog,
routes: {
index: indexPageRouteRef,
},
})
// in catalog plugin
import {indexPageRouteRef} from '../../routes
const link = useRouteRef(indexPageRouteRef)
// scaffolder
export const catalogIndexPageRouteRef = createExternalRouteRef({
defaultTarget: 'catalog/index',
})
export const scaffolderPlugin = createPlugin({
id: 'scaffolder,
externalRoutes: {
catalogIndex: catalogIndexPageRouteRef,
},
})
import {catalogIndexPageRouteRef} from '../../routes
const link = useRouteRef(catalogIndexPageRouteRef)
// app
import {catalogPlugin} from '@backstage/plugin-catalog'
const app = createApp({
bindRoutes({bind}) {
bind(scaffolderPlugin, {
catalogIndex: catalogPlugin.routes.index,
})
},
})
*/
```
@@ -0,0 +1,142 @@
---
id: naming-patterns
title: Frontend System Naming Patterns
sidebar_label: Naming Patterns
# prettier-ignore
description: Naming patterns in the frontend system
---
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
These are the naming patterns to adhere to within the frontend system. They help us keep exports and IDs consistent across packages and make it easier to understand the usage and intent of exports and IDs.
As a rule, all names should be camel case, with the exceptions of plugin and extension IDs, which should use kebab case.
## Plugins
| Description | Pattern | Examples |
| ----------- | ------------ | ------------------------------------- |
| ID | `'<id>'` | `'catalog'`, `'user-settings'` |
| Symbol | `<id>Plugin` | `catalogPlugin`, `userSettingsPlugin` |
Example:
```ts
// This declaration is only for internal usage in tests. This could also be a direct default export.
export const userSettingsPlugin = createPlugin({
id: 'user-settings',
...
})
// The plugin instance should be the default export of the package, typically this is placed in src/index.ts
export { userSettingsPlugin as default } from './plugin';
```
Note that while we use this naming pattern for the plugin instance this is only for internal usage within the package. Plugins are always exported as the default export of the plugin package.
## Extensions
| Description | Pattern | Examples |
| ----------- | ------------------------------- | ------------------------------------------------------------------- |
| Creator | `create<Kind>Extension` | `createPageExtension`, `createEntityCardExtension` |
| ID | `[<kind>:]<namespace>[/<name>]` | `'core.nav'`, `'page:user-settings'`, `'entity-card:catalog/about'` |
| Symbol | `<namespace>[<Name>][<Kind>]` | `coreNav`, `userSettingsPage`, `catalogAboutEntityCard` |
When you create a new extension you never provide the ID directly. Instead, you indirectly or directly provide the kind, namespace, and name parts that make up the ID. The kind is always provided by the extension creator function used to create the extension, the only exception is if you use `createExtension` directly. Any extension that is provided by a plugin will by default have its namespace set to the plugin ID, so you generally only need to provide an explicit namespace if you want to override an existing extension. The name is also optional, and primarily used to distinguish between multiple extensions of the same kind and namespace. If a plugin doesn't need to distinguish between different extensions of the same kind, the name can be omitted.
Example:
```ts
// This is an extension creator that is used to create an extension of the 'page' kind.
export function createPageExtension(options) {
return createExtension({
kind: 'page', // Kinds are kebab-case
// ...options
});
}
// The namespace is inferred from the plugin ID, in this case 'catalog'
// The final ID for this extension will be 'page:catalog/entity'
const catalogEntityPage = createPageExtension({
name: 'entity',
// ...
});
// The name is omitted, because the catalog plugin only provides a single extension of this kind
// The final ID for this extension will be 'search-result-list-item:catalog'
const catalogSearchResultListItem = createSearchResultListItemExtension({
// ...
});
// Note that the extensions themselves are not exported, only the plugin instance
export const catalogPlugin = createPlugin({
id: 'catalog',
extensions: [catalogEntityPage, catalogSearchResultListItem /* ... */],
});
```
## Extension Data
| Description | Pattern | Examples |
| -------------------- | ------------------------------------- | ----------------------------------------------------------------------------- |
| Interface | `<Name>ExtensionData` | `SearchResultItemExtensionData` |
| Standalone Reference | `<name>ExtensionDataRef` | `searchResultItemExtensionDataRef` |
| Standalone ID | `<namespace>.<name>` | `'search.search-result-item'` |
| Grouped Reference | `<group>ExtensionData.<name>` | `coreExtensionData.reactElement`, `catalogFilterExtensionData.functionFilter` |
| Grouped ID | `<group>.<name>` | `'core.react-element'`, `'catalog-filter.function-filter'` |
| Creator Reference | `create<Kind>Extension.<name>DataRef` | `createGraphiQLEndpointExtension.endpointDataRef` |
| Creator ID | `<namespace>.<kind>.<name>` | `'graphiql.graphiql-endpoint.endpoint'` |
Extension data references can be defined in a couple of different ways, depending on the intended usage, all of which are covered below.
#### Standalone Extension Data
The most simple way of defining extension data is a standalone reference. This is useful when you want to export a single reference that isn't closely tied to a specific kind of extension. Because this creates an extra export for each reference, the two other ways of defining extension data are preferred when possible.
```ts
// A separate named type declaration is only needed for bespoke complex extension data types
export interface SearchResultItemExtensionData {
/* ... */
}
export const searchResultItemExtensionDataRef =
createExtensionDataRef<SearchResultItemExtensionData>(
'search.search-result-item',
);
```
#### Grouped Extension Data
This way of defining extension data is similar to the standalone way, but it used when you want to export multiple pieces of grouped extension data for general use. This avoids separate exports and helps make related extension data references easier to discover. The name of the group should generally by the same as the namespace of the exporting package, typically the plugin ID. If the group needs to be more specific it should be prefixed with the namespace.
```ts
export const coreExtensionData = {
reactElement: createExtensionDataRef<ReactElement>('core.react-element'),
routePath: createExtensionDataRef<string>('core.route-path'),
};
```
#### Extension Creator Extension Data
This is a convenient way of defining extension data when that data is only meant to be produced by a specific extension creator. It avoids additional exports and clearly signals that this piece of data belongs to this particular kind of extension.
```ts
export function createGraphiQLEndpointExtension(options) {
/* ... */
}
// Use a TypeScript namespace to merge the extension data references with the extension creator
export namespace createGraphiQLEndpointExtension {
export const endpointDataRef = createExtensionDataRef</* ... */>(
'graphiql.graphiql-endpoint.endpoint',
);
}
```
## Extension Inputs
Extension inputs do not have naming patterns for all types of input, but there are some specific use-cases where we encourage using a recognizable input name.
| Name | Description |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `children` | An extension input that accepts `coreExtensionData.reactElement` data and nothing else, used in a way that is equivalent of the `children` property in React. |
@@ -0,0 +1,17 @@
---
id: references
title: Value References
sidebar_label: Value References
# prettier-ignore
description: Value References
---
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
<!--
Describe the generic concept of references as used for ApiRef, ExtensionDataRef, RouteRef, ComponentRef, etc.
Talk about the usage and how helper functions allows type-safe access to the values that the references represent.
-->
@@ -0,0 +1,235 @@
---
id: testing
title: Frontend System Testing Plugins
sidebar_label: Testing
# prettier-ignore
description: Testing plugins in the frontend system
---
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
# Testing Frontend Plugins
> NOTE: The new frontend system is in alpha, and some plugins do not yet fully implement it.
Utilities for testing frontend features and components are available in `@backstage/frontend-test-utils`.
## Testing React components
A component can be used for more than one extension, and it should be tested independently of an extension environment.
Use the `renderInTestApp` helper to render a given component inside a Backstage test app:
```tsx
import React from 'react';
import { screen } from '@testing-library/react';
import { renderInTestApp } from '@backstage/frontend-test-utils';
import { EntityDetails } from './plugin';
describe('Entity details component', () => {
it('should render the entity name and owner', async () => {
await renderInTestApp(<EntityDetails owner="tools" name="test" />);
await expect(
screen.findByText('The entity "test" is owned by "tools"'),
).resolves.toBeInTheDocument();
});
});
```
To mock [Utility APIs](../architecture/06-utility-apis.md) that are used by your component you can use the `TestApiProvider` to override individual API implementations. In the snippet below, we wrap the component within a `TestApiProvider` in order to mock the catalog client API:
```tsx
import React from 'react';
import { screen } from '@testing-library/react';
import {
renderInTestApp,
TestApiProvider,
} from '@backstage/frontend-test-utils';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { EntityDetails } from './plugin';
describe('Entity details component', () => {
it('should render the entity name and owner', async () => {
const catalogApiMock = {
async getEntityFacets() {
return {
facets: {
'relations.ownedBy': [{ count: 1, value: 'group:default/tools' }],
},
},
}
} satisfies Partial<typeof catalogApiRef.T>;
const entityRef = stringifyEntityRef({
kind: 'Component',
namespace: 'default',
name: 'test',
});
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
<EntityDetails entitRef={entityRef} />
</TestApiProvider>,
);
await expect(
screen.findByText('The entity "test" is owned by "tools"'),
).resolves.toBeInTheDocument();
});
});
```
## Testing extensions
To facilitate testing of frontend extensions, the `@backstage/frontend-test-utils` package provides a tester class which starts up an entire frontend harness, complete with a number of default features. You can then provide overrides for extensions whose behavior you need to adjust for the test run.
A number of features (frontend extensions and overrides) are also accepted by the tester. Here are some examples of how these facilities can be useful:
### Single extension
In order to test an extension in isolation, you simply need to pass it into the tester factory, then call the render method on the returned instance:
```tsx
import { screen } from '@testing-library/react';
import { createExtensionTester } from '@backstage/frontend-test-utils';
import { indexPageExtension } from './plugin';
describe('Index page', () => {
it('should render a the index page', () => {
createExtensionTester(indexPageExtension).render();
expect(screen.getByText('Index Page')).toBeInTheDocument();
});
});
```
### Extension preset
There are some extensions that rely on other extensions existence, such as a page that links to another page. In that case, you can add more than one extension to the preset of features you want to render in the test, as shown below:
```tsx
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createExtensionTester } from '@backstage/frontend-test-utils';
import { indexPageExtension, detailsPageExtension } from './plugin';
describe('Index page', async () => {
it('should link to the details page', () => {
createExtensionTester(indexPageExtension)
// Adding more extensions to the preset being tested
.add(detailsPageExtension)
.render();
await expect(screen.findByText('Index Page')).toBeInTheDocument();
await userEvent.click(screen.getByRole('link', { name: 'See details' }));
await expect(
screen.findByText('Details Page'),
).resolves.toBeInTheDocument();
});
});
```
### Mocking apis
If your extensions requires implementation of APIs that aren't wired up by default, you'll have to add overrides to the preset of features being tested:
```tsx
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createApiFactory } from '@backestage/core-plugin-api';
import {
createExtensionOverrides,
configApiRef,
analyticsApiRef,
} from '@backstage/frontend-plugin-api';
import {
createExtensionTester,
MockConfigApi,
MockAnalyticsApi,
} from '@backstage/frontend-test-utils';
import { indexPageExtension } from './plugin';
describe('Index page', () => {
it('should capture click events in analytics', async () => {
// Mocking the analytics api implementation
const analyticsApiMock = new MockAnalyticsApi();
const analyticsApiOverride = createApiExtension({
factory: createApiFactory({
api: analyticsApiRef,
factory: () => analyticsApiMock,
}),
});
createExtensionTester(indexPageExtension)
// Overriding the analytics api extension
.add(analyticsApiOverride)
.render();
await userEvent.click(
await screen.findByRole('link', { name: 'See details' }),
);
expect(analyticsApiMock.getEvents()[0]).toMatchObject({
action: 'click',
subject: 'See details',
});
});
});
```
### Setting configuration
In the case that your extension can be configured, you can test this capability by passing configuration values as follows:
```tsx
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createExtensionTester } from '@backstage/frontend-test-utils';
import { indexPageExtension, detailsPageExtension } from './plugin';
describe('Index page', () => {
it('should accepts a custom title via config', async () => {
createExtensionTester(indexPageExtension, {
// Configuration specific of index page
config: { title: 'Custom index' },
})
.add(detailsExtensionPage, {
// Configuration specific of details page
config: { title: 'Custom details' },
})
.render({
// Configuration specific of the instance
config: {
app: {
title: 'Custom app',
},
},
});
await expect(
screen.findByRole('heading', { name: 'Custom app' }),
).resolves.toBeInTheDocument();
await expect(
screen.findByRole('heading', { name: 'Custom index' }),
).resolves.toBeInTheDocument();
await userEvent.click(screen.getByRole('link', { name: 'See details' }));
await expect(
screen.findByText('Custom details'),
).resolves.toBeInTheDocument();
});
});
```
That's all for testing features!
## Missing something?
If there's anything else you think needs to be covered in the docs or that you think isn't covered by the test utilities, please create an issue in the Backstage repository. You are always welcome to contribute as well!
+3 -3
View File
@@ -6,10 +6,10 @@ sidebar_label: Introduction
description: The Frontend System
---
> **NOTE: The new frontend system is in a highly experimental phase**
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
## Status
The new frontend system is in an experimental phase and we do not recommend any plugins or apps to migrate.
The new frontend system is in alpha, and only a few plugins support the system so far. We do not yet recommend migrating any apps to the new system. If you add support for the new system to your plugin, please do so under a `/alpha` sub-path export.
You can find an example app setup in [the `app-next` package](https://github.com/backstage/backstage/tree/master/packages/app-next).
You can find an example app setup in the [`app-next` package](https://github.com/backstage/backstage/tree/master/packages/app-next).
@@ -0,0 +1,43 @@
---
id: index
title: Utility APIs
sidebar_label: Overview
# prettier-ignore
description: Working with Utility APIs in the New Frontend System
---
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
As described [in the architecture section](../architecture/06-utility-apis.md), utility APIs are pieces of shared functionality - interfaces that can be requested by plugins to use. They are defined by a TypeScript interface as well as a reference (an "API ref") used to access its implementation. They can be provided both by plugins and the core framework, and are themselves [extensions](../architecture/03-extensions.md) that can accept inputs, be declaratively configured in your app-config, or transparently be replaced entirely with custom implementations that fulfill the same contract.
## Creating utility APIs
> For details, [see the main article](./02-creating.md).
Backstage apps, plugins, and the core Backstage framework can all expose utility APIs for general use.
Some are available out of the box, such as the API for reading app configuration. Some are provided by third party plugins, such as the catalog client API that both the catalog itself and your own code can leverage to talk to the catalog backend. Some, you may create yourself and make available inside your Backstage instance for use within your private ecosystem of plugins.
[The main article](./02-creating.md) describes the process of creating and exposing utility APIs of your own, for sharing functionality or configurability across plugins and apps.
## Consuming utility APIs
> For details, [see the main article](./03-consuming.md).
Once utility APIs are created, there are a few ways that they can be accessed to be consumed.
Some utility APIs in turn depend on other utility APIs. This powerful composability lets you leverage already-written reusable pieces. In particular, you may want to rely on Backstage's framework-provided APIs e.g. for reading app configuration and many other use cases. Sometimes you request utility APIs inside your React components, e.g. for accessing i18n strings, or emitting analytics events.
These are described in detail in [the main article](./03-consuming.md)
## Configuring utility APIs
> For details, [see the main article](./04-configuring.md).
Most utility APIs are usable directly without any configuration. But they are proper extensions, and can therefore have their implementations entirely swapped out by your app for advanced use cases. They can also be built with the ability to configured in your app-config, or to have inputs that extend their functionality.
These cases are all described in [the main article](./04-configuring.md).
## Migrating from the old frontend system
If you want to learn how to migrate your own utility APIs from the old frontend system to the new one, that's described in [a dedicated migration guide](./05-migrating.md).
@@ -0,0 +1,145 @@
---
id: creating
title: Creating Utility APIs
sidebar_label: Creating APIs
# prettier-ignore
description: Creating new utility APIs in your plugins and app
---
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
This section describes how to make a Utility API from scratch, or to add configurability and inputs to an existing one. If you are instead interested in migrating an existing Utility API from the old frontend system, check out [the migrating section](./05-migrating.md).
## Creating the Utility API contract
The first step toward exposing a utility API is to define its TypeScript contract, as well as an API reference for consumers use to access the implementation. If you want your API to be accessible by other plugins this should be done in [your plugin's `-react` package](../../architecture-decisions/adr011-plugin-package-structure.md), so that it can be imported separately. If you just want to use the API within your own plugin it is fine to place the definition within the plugin itself. In this example, we have an Example plugin that wants to expose a utility API for performing some type of work.
```tsx title="in @internal/plugin-example-react"
import { createApiRef } from '@backstage/frontend-plugin-api';
/**
* The work interface for the Example plugin.
* @public
*/
export interface WorkApi {
/**
* Performs some work.
*/
doWork(): Promise<void>;
}
/**
* API Reference for {@link WorkApi}.
* @public
*/
export const workApiRef = createApiRef<WorkApi>({
id: 'plugin.example.work',
});
```
Both of these are properly exported publicly from the package, so that consumers can reach them.
## Providing an extension through your plugin
The plugin itself now wants to provide this API and its default implementation, in the form of an API extension. Doing so means that when users install the Example plugin, an instance of the Work utility API will also be automatically available in their apps - both to the Example plugin itself, and to others. We do this in the main plugin package, not the `-react` package.
```tsx title="in @internal/plugin-example"
import {
createApiExtension,
createApiFactory,
createPlugin,
storageApiRef,
StorageApi,
} from '@backstage/frontend-plugin-api';
import { WorkApi, workApiRef } from '@internal/plugin-example-react';
class WorkImpl implements WorkApi {
constructor(options: { storageApiRef: StorageApi }) {
/* TODO */
}
async doWork() {
/* TODO */
}
}
const exampleWorkApi = createApiExtension({
factory: createApiFactory({
api: workApiRef,
deps: { storageApi: storageApiRef },
factory: ({ storageApi }) => {
return new WorkImpl({ storageApi });
},
}),
});
/**
* The Example plugin.
* @public
*/
export default createPlugin({
id: 'example',
extensions: [exampleWorkApi],
});
```
For illustration we make a skeleton implementation class and the API extension and factory for it, in the same file. These are not exported to the public surface of the plugin package; only the plugin is, as the default export. Users who install the plugin will now get the utility API automatically as well.
The code also illustrates how the API factory declares a dependency on another utility API - the core storage API in this case. An instance of that utility API is then provided to the factory function.
The resulting extension ID of the work API will be the kind `api:` followed by the plugin ID as the namespace, in this case ending up as `api:plugin.example.work`. Check out [the naming patterns doc](../architecture/08-naming-patterns.md) for more information on how this works. You can now use this ID to refer to the API in app-config and elsewhere.
## Adding configurability
Here we will describe how to amend a utility API with the capability of having extension config, which is driven by [your app-config](../../conf/writing.md). You do this by giving an extension config schema to your API extension factory function. Let's make the required additions to our original work example API.
```tsx title="in @internal/plugin-example"
/* highlight-add-next-line */
import { createSchemaFromZod } from '@backstage/frontend-plugin-api';
const exampleWorkApi = createApiExtension({
/* highlight-add-start */
api: workApiRef,
configSchema: createSchemaFromZod(z =>
z.object({
goSlow: z.boolean().default(false),
}),
),
/* highlight-add-end */
/* highlight-remove-next-line */
factory: createApiFactory({
/* highlight-add-next-line */
factory: ({ config }) => createApiFactory({
api: workApiRef,
deps: { storageApi: storageApiRef },
factory: ({ storageApi }) => {
/* highlight-add-start */
if (config.goSlow) {
/* ... */
}
/* highlight-add-end */
},
}),
});
```
We wanted users to be able to set a `goSlow` extension config parameter for our API instances. So we passed in a `configSchema` to `createApiExtension` which matches that interface. This example builds it using [the zod library](https://zod.dev/). The actual extension config values will then be passed in a type safe manner in to the `factory` which is now a callback, wherein we can do what we wish with them. When changing to the callback form, we also had to add a top level `api: workApiRef` under `createApiExtension`.
Note that the expression "extension config" as used here, is _not_ the same thing as the `configApi` which gives you access to the full app-config. The extension config discussed here is instead the particular configuration settings given to your utility API instance. This is discussed more [in the Configuring section](./04-configuring.md).
Note also that the extension config schema contained a default value fo the `goSlow` field. This is an important consideration. You want users of your API to be able to get maximum value out of it, without having to dive deep into how to configure it. For that reason you generally want to provide as many sane defaults as possible, while letting users override them rarely but with purpose, only when called for. If you have an extension config schema without defaults, the framework will refuse to instantiate the utility API on startup unless the user had configured those values explicitly. Since it had a default value, the TypeScript code and interfaces also don't have to defensively allow `undefined` - we know that it'll have either the default value or an overridden value when we start consuming the extension config data.
## Adding inputs
Inputs are added to Utility APIs in the same way as other extension types:
- Declaring a set of `inputs` on your extension
- If needed, create custom extension data types to be used in those inputs
- If needed, export an extension creator function for creating that particular attachment type
This is a power use case and not very commonly used.
<!-- TODO: link to main article -->
## Next steps
See [the Consuming section](./03-consuming.md) to see how to consume this new utility API in various ways. If you wish to configure and add inputs to it, check out [the Configuring section](./04-configuring.md).
@@ -0,0 +1,69 @@
---
id: consuming
title: Consuming Utility APIs
sidebar_label: Consuming APIs
# prettier-ignore
description: Consuming utility APIs
---
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
All of the utility API extensions that were passed into your app through installed plugins, get instantiated and configured in the right order by the framework, and are then made available for consumption. You can interact with these instances in the following ways.
## Via React hooks
The most common consumption pattern for utility APIs is to call the `useApi` hook inside React components to get an implementation via its API ref. This applies whether it was originally provided from the core framework or from a plugin.
```tsx
import { useApi, configApiRef } from '@backstage/frontend-plugin-api';
const MyComponent = () => {
const configApi = useApi(configApiRef);
const title = configApi.getString('app.title');
// ...
};
```
The `useApi` hook always returns a value, or throws an exception if the API ref could not be resolved to a registered implementation. For advanced use cases, where you explicitly want to optionally request a utility API that may or may not have been provided at runtime, you can use the underlying `useApiHolder` hook instead.
```tsx
import { useApiHolder, configApiRef } from '@backstage/frontend-plugin-api';
const MyComponent = () => {
const apis = useApiHolder();
const configApi = apis.get(configApiRef); // may return undefined
if (configApi) {
const title = configApi.getString('app.title');
// ...
}
};
```
## Via dependencies
Your utility APIs can depend on other utility APIs in their factories. You do this by declaring `deps` on your `createApiFactory`, and reading the outcome in your `factory`.
```tsx
import {
configApiRef,
createApiExtension,
createApiFactory,
discoveryApiRef,
} from '@backstage/frontend-plugin-api';
import { MyApiImpl } from './MyApiImpl';
const myApi = createApiExtension({
factory: createApiFactory({
api: myApiRef,
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
},
factory: ({ configApi, discoveryApi }) => {
return new MyApiImpl({ configApi, discoveryApi });
},
}),
});
```
Note how the `deps` section essentially assigns free-form names that you choose, to API refs. Here we for example map `configApiRef` to the name `configApi`, but that's just a convention. The framework will ensure that all of those deps get instantiated and passed into your `factory` function with the same set of names as your `deps`. At that point, `configApi` refers to an actual functioning instance of that API ref.
@@ -0,0 +1,76 @@
---
id: configuring
title: Configuring Utility APIs
sidebar_label: Configuring
# prettier-ignore
description: Configuring, extending, and overriding utility APIs
---
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
Utility APIs are extensions and can therefore optionally be amended with configurability, as well as inputs that other extensions attach themselves to. This section describes how to make use of that as a consumer of such utility APIs.
## Configuring
To configure your Utility API extension, first you'll need to know its ID. That ID is formed from the API ref ID; check [the naming patterns docs](../architecture/08-naming-patterns.md) for details.
Our example work API from [the creating section](./02-creating.md) would have the ID `api:plugin.example.work`. You configure it and all other extensions under the `app.extensions` section of your app-config.
```yaml title="in e.g. app-config.yaml or app-config.production.yaml"
app:
extensions:
- api:plugin.example.work:
config:
goSlow: false
- # ... other extensions
```
It's important to note that the `extensions` are a list (mind the initial `-`), and that the `api:plugin.example.work` entry is an object such that the `config` key needs to be indented below it. If you do not get those two pieces right, the application may not start up correctly.
The extension config schema will tell you what parameters it supports. Here we override the `goSlow` extension config value, which replaces the default.
## Attaching extensions to inputs
Like with other extension types, you add input attachments to a Utility API by declaring the `attachTo` section of that attachment to point to the Utility APIs ID and input name.
Well written input-enabled extension often have extension creator functions that help you make such attachments. Those functions typically set the `attachTo` section correctly on your behalf so that you don't have to figure them out.
## Replacing a Utility API implementation
Like with other extension types, you replace Utility APIs with your own custom implementation using [extension overrides](../architecture/05-extension-overrides.md).
```tsx title="in your app"
/* highlight-add-start */
import { createExtensionOverrides } from '@backstage/frontend-plugin-api';
class CustomWorkImpl implements WorkApi {
/* ... */
}
const myOverrides = createExtensionOverrides({
extensions: [
createApiExtension({
api: workApiRef,
factory: () =>
createApiFactory({
api: workApiRef,
factory: () => new CustomWorkImpl(),
}),
}),
],
});
/* highlight-add-end */
// Remember to pass the overrides to your createApp
export default createApp({
features: [
// ... other features
/* highlight-add-next-line */
myOverrides,
],
});
```
In this example the overriding extension is kept minimal, but just like any other extension it can also have `deps`, configurability, and inputs. Check out [the Creating section](./02-creating.md) for more details about that.
When you create a replacement extension, in general you may want to mimic its extension config schema or input shapes where applicable. This makes it an easier thing to slot in to an app, since it'll be responding to extensibility the same way as the original one did.
@@ -0,0 +1,126 @@
---
id: migrating
title: Migrating Utility APIs from the old frontend system
sidebar_label: Migrating
# prettier-ignore
description: Migrating Utility APIs from the old frontend system
---
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
If you are migrating your plugins or app over from the old frontend system, there are a few things to keep in mind in regards to utility APIs.
## Overview
- Migrate your repo overall to the latest release of Backstage
- Follow the plugin migration guide <!-- TODO: Link -->
- Optionally change your package dependencies and code from `core-*-api` to `frontend-*-api`
- Keep the TypeScript interface and API ref exported as they were, except possibly reconsidering the choice of ID of the latter
- Wrap the old API factory call in an extension using `createApiExtension`
- Make sure that this extension is referenced by your migrated plugin
## Prerequisites
This guide assumes that you first [upgrade your repo](../../getting-started/keeping-backstage-updated.md) to the latest release of Backstage. This ensures that you do not have to fight several types of incompatibilities and updates at the same time.
## Dependency changes
In this article we will discuss some old interfaces that you used to import from the `@backstage/core-plugin-api` package. Those are now generally lifted over to `@backstage/frontend-plugin-api`, next to the new interfaces that are specific to the new frontend system. If you want to, you can already update your `package.json` and code imports to only use the new plugin API, but for the time being you don't have to. The old core exports will continue to work for the foreseeable future.
To at least get access to the new interfaces, you'll need to run the following command. Note that it's just an example! It refers to `plugins/example`, which you'll have to change to the actual folder name that your package to migrate is in.
```bash title="from your repo root"
yarn --cwd plugins/example add @backstage/frontend-plugin-api
```
## React package interface and ref changes
Let's begin with [your `-react` package](../../architecture-decisions/adr011-plugin-package-structure.md). The act of exporting TypeScript interfaces and API refs have not changed from the old system. You can typically keep those as-is. For illustrative purposes, this is an example of an interface and its API ref:
```tsx title="in @internal/plugin-example-react"
import { createApiRef } from '@backstage/frontend-plugin-api';
/**
* Performs some work.
* @public
*/
export interface WorkApi {
doWork(): Promise<void>;
}
/**
* The work interface for the Example plugin.
* @public
*/
export const workApiRef = createApiRef<WorkApi>({
id: 'plugin.example.work',
});
```
In this example, the plugin ID already follows <!-- TODO: Link --> the common naming convention. If it doesn't, you may want to consider renaming that ID at this point. Don't worry, this won't hurt consumers in the old frontend system since the ID is mostly used for debugging purposes there. In the new system, it's much more important and appears in app-config files and similar.
Note at the top of the file that it uses the updated import from `@backstage/frontend-plugin-api` that we migrated in the previous section, instead of the old `@backstage/core-plugin-api`.
## Plugin package changes
Now let's turn to the main plugin package where the plugin itself is exported. You will probably already have a `createPlugin` call in here. Before we changed the `core-plugin-api` imports it'll have looked somewhat similar to the following:
```tsx title="in @internal/plugin-example, NOTE THIS IS LEGACY CODE"
import {
storageApiRef,
createPlugin,
createApiFactory,
} from '@backstage/core-plugin-api';
import { workApiRef } from '@internal/plugin-example-react';
import { WorkImpl } from './WorkImpl';
const exampleWorkApi = createApiFactory({
api: workApiRef,
deps: { storageApi: storageApiRef },
factory: ({ storageApi }) => new WorkImpl({ storageApi }),
});
/** @public */
export const catalogPlugin = createPlugin({
id: 'example',
apis: [exampleWorkApi],
});
```
The major changes we'll make are
- Optionally change the old imports to the new package as per the top section of this guide
- Wrap the existing API factory in a `createApiExtension`
- Change to the new version of `createPlugin` which exports this extension
- Change the plugin export to be the default instead
The end result, after simplifying imports and cleaning up a bit, might look like this:
```tsx title="in @internal/plugin-example"
import {
storageApiRef,
createPlugin,
createApiFactory,
createApiExtension,
} from '@backstage/frontend-plugin-api';
import { workApiRef } from '@internal/plugin-example-react';
import { WorkImpl } from './WorkImpl';
const exampleWorkApi = createApiExtension({
factory: createApiFactory({
api: workApiRef,
deps: { storageApi: storageApiRef },
factory: ({ storageApi }) => new WorkImpl({ storageApi }),
}),
});
/** @public */
export default createPlugin({
id: 'example',
extensions: [exampleWorkApi],
});
```
## Further work
Since utility APIs are now complete extensions, you may want to take a bigger look at how they used to be used, and what the new frontend system offers. You may for example consider [adding configurability or inputs](./02-creating.md) to your API, if that makes sense for your current application.
+4
View File
@@ -484,3 +484,7 @@ You can see more ways to use this in the [Storybook Sidebar examples](https://ba
In addition to a custom theme, a custom logo, you can also customize the
homepage of your app. Read the full guide on the [next page](homepage.md).
## Migrating to Material UI v5
We now support Material UI v5 in Backstage. Check out our [migration guide](../tutorials/migrate-to-mui5.md) to get started.
+11 -6
View File
@@ -134,7 +134,7 @@ links:
There are multiple authentication providers available for you to use with
Backstage, feel free to follow
[the instructions for adding authentication](../auth/).
[the instructions for adding authentication](../auth/index.md).
For this tutorial we choose to use GitHub, a free service most of you might be
familiar with. For other options, see
@@ -205,7 +205,7 @@ Restart Backstage from the terminal, by stopping it with `Control-C`, and starti
To learn more about Authentication in Backstage, here are some docs you
could read:
- [Authentication in Backstage](../auth/)
- [Authentication in Backstage](../auth/index.md)
- [Using organizational data from GitHub](../integrations/github/org.md)
### Setting up a GitHub Integration
@@ -254,7 +254,7 @@ integrations:
Some helpful links, for if you want to learn more about:
- [Other available integrations](../integrations/)
- [Other available integrations](../integrations/index.md)
- [Using GitHub Apps instead of a Personal Access Token](../integrations/github/github-apps.md#docsNav)
### Explore what we've done so far
@@ -275,8 +275,8 @@ otherwise something went terribly wrong.
![Software template main screen, with a blue button to add an existing component](../assets/getting-started/b-existing-1.png)
- As URL use `https://github.com/backstage/demo/blob/master/catalog-info.yaml`.
This is used by our [demo site](https://demo.backstage.io).
- As URL use `https://github.com/backstage/backstage/blob/master/catalog-info.yaml`.
This is used in our [demo site](https://demo.backstage.io) catalog.
![Register a new component wizard, asking for an URL to the existing component YAML file](../assets/getting-started/b-existing-2.png)
@@ -285,11 +285,16 @@ otherwise something went terribly wrong.
![Register a new component wizard, showing the metadata for the component YAML we use in this tutorial](../assets/getting-started/b-existing-3.png)
- You should receive a message that your entities have been added.
- If you go back to `Home`, you should be able to find `demo`. You should be
- If you go back to `Home`, you should be able to find `backstage`. You should be
able to click it and see the details
## Create a new component using a software template
> Note: if you're running Backstage with Node 20 or later, you'll need to pass the flag `--no-node-snapshot` to Node in order to
> use the templates feature.
> One way to do this is to specify the `NODE_OPTIONS` environment variable before starting Backstage:
> `export NODE_OPTIONS=--no-node-snapshot`
- Go to `create` and choose to create a website with the `Example Node.js Template`
- Type in a name, let's use `tutorial` and click `Next Step`
@@ -15,7 +15,7 @@ The following steps assume that you have
to it.
We are using the
[CircleCI](https://github.com/backstage/backstage/blob/master/plugins/circleci/README.md)
[CircleCI](https://github.com/CircleCI-Public/backstage-plugin/tree/main/plugins/circleci)
plugin in this example, which is designed to show CI/CD pipeline information attached
to an entity in the software catalog.
@@ -23,7 +23,7 @@ to an entity in the software catalog.
```bash
# From your Backstage root directory
yarn add --cwd packages/app @backstage/plugin-circleci
yarn add --cwd packages/app @circleci/backstage-plugin
```
Note the plugin is added to the `app` package, rather than the root
@@ -38,7 +38,7 @@ to an entity in the software catalog.
import {
EntityCircleCIContent,
isCircleCIAvailable,
} from '@backstage/plugin-circleci';
} from '@circleci/backstage-plugin';
/* highlight-add-end */
const cicdContent = (
+1 -1
View File
@@ -26,7 +26,7 @@ you to use the [Contributors](https://github.com/backstage/backstage/blob/master
On MacOS, you will want to have run `xcode-select --install` to get the XCode command line build tooling in place.
- An account with elevated rights to install the dependencies
- `curl` or `wget` installed
- Node.js [Active LTS Release](https://nodejs.org/en/blog/release/) installed using one of these
- Node.js [Active LTS Release](https://nodejs.org/en/about/previous-releases) installed using one of these
methods:
- Using `nvm` (recommended)
- [Installing nvm](https://github.com/nvm-sh/nvm#install--update-script)
+2 -8
View File
@@ -32,7 +32,7 @@ catalog:
bucketName: sample-bucket
prefix: prefix/ # optional
region: us-east-2 # optional, uses the default region otherwise
schedule: # optional; same options as in TaskScheduleDefinition
schedule: # same options as in TaskScheduleDefinition
# supports cron, ISO duration, "human duration" as used in code
frequency: { minutes: 30 }
# supports ISO duration, "human duration" as used in code
@@ -52,7 +52,7 @@ catalog:
bucketName: sample-bucket
prefix: prefix/ # optional
region: us-east-2 # optional, uses the default region otherwise
schedule: # optional; same options as in TaskScheduleDefinition
schedule: # same options as in TaskScheduleDefinition
# supports cron, ISO duration, "human duration" as used in code
frequency: { minutes: 30 }
# supports ISO duration, "human duration" as used in code
@@ -79,12 +79,6 @@ const builder = await CatalogBuilder.create(env);
builder.addEntityProvider(
AwsS3EntityProvider.fromConfig(env.config, {
logger: env.logger,
// optional: alternatively, use scheduler with schedule defined in app-config.yaml
schedule: env.scheduler.createScheduledTaskRunner({
frequency: { minutes: 30 },
timeout: { minutes: 3 },
}),
// optional: alternatively, use schedule
scheduler: env.scheduler,
}),
);
+1 -1
View File
@@ -71,7 +71,7 @@ The parameters available are:
- **`repository:`** _(optional)_ The repository name. Wildcards are supported as show on the examples above. If not set, all repositories will be searched.
- **`path:`** _(optional)_ Where to find catalog-info.yaml files. Defaults to /catalog-info.yaml.
- **`branch:`** _(optional)_ The branch name to use.
- **`schedule`** _(optional)_:
- **`schedule`**:
- **`frequency`**:
How often you want the task to run. The system does its best to avoid overlapping invocations.
- **`timeout`**:
+4 -5
View File
@@ -34,6 +34,9 @@ catalog:
securityEnabled eq false
and mailEnabled eq true
and groupTypes/any(c:c+eq+'Unified')
schedule:
frequency: PT1H
timeout: PT50M
```
Finally, register the plugin in `catalog.ts`.
@@ -52,11 +55,7 @@ export default async function createPlugin(
builder.addEntityProvider(
MicrosoftGraphOrgEntityProvider.fromConfig(env.config, {
logger: env.logger,
schedule: env.scheduler.createScheduledTaskRunner({
frequency: { hours: 1 },
timeout: { minutes: 50 },
initialDelay: { seconds: 15 },
}),
scheduler: env.scheduler,
}),
);
/* highlight-add-end */
+2 -15
View File
@@ -49,19 +49,6 @@ export default async function createPlugin(
}
```
Alternatively to the config-based schedule, you can use
```ts
/* highlight-remove-next-line */
scheduler: env.scheduler,
/* highlight-add-start */
schedule: env.scheduler.createScheduledTaskRunner({
frequency: { minutes: 30 },
timeout: { minutes: 3 },
}),
/* highlight-add-end */
```
### Installation with Events Support
Please follow the installation instructions at
@@ -131,7 +118,7 @@ catalog:
filters: # optional
projectKey: '^apis-.*$' # optional; RegExp
repoSlug: '^service-.*$' # optional; RegExp
schedule: # optional; same options as in TaskScheduleDefinition
schedule: # same options as in TaskScheduleDefinition
# supports cron, ISO duration, "human duration" as used in code
frequency: { minutes: 30 }
# supports ISO duration, "human duration" as used in code
@@ -153,7 +140,7 @@ catalog:
Regular expression used to filter results based on the project key.
- **`repoSlug`** _(optional)_:
Regular expression used to filter results based on the repo slug.
- **`schedule`** _(optional)_:
- **`schedule`**:
- **`frequency`**:
How often you want the task to run. The system does its best to avoid overlapping invocations.
- **`timeout`**:
@@ -38,12 +38,6 @@ export default async function createPlugin(
builder.addEntityProvider(
BitbucketServerEntityProvider.fromConfig(env.config, {
logger: env.logger,
// optional: alternatively, use scheduler with schedule defined in app-config.yaml
schedule: env.scheduler.createScheduledTaskRunner({
frequency: { minutes: 30 },
timeout: { minutes: 3 },
}),
// optional: alternatively, use schedule
scheduler: env.scheduler,
}),
);
@@ -69,7 +63,7 @@ catalog:
filters: # optional
projectKey: '^apis-.*$' # optional; RegExp
repoSlug: '^service-.*$' # optional; RegExp
schedule: # optional; same options as in TaskScheduleDefinition
schedule: # same options as in TaskScheduleDefinition
# supports cron, ISO duration, "human duration" as used in code
frequency: { minutes: 30 }
# supports ISO duration, "human duration" as used in code
@@ -87,7 +81,7 @@ catalog:
Regular expression used to filter results based on the project key.
- **`repoSlug`** _(optional)_:
Regular expression used to filter results based on the repo slug.
- **`schedule`** _(optional)_:
- **`schedule`**:
- **`frequency`**:
How often you want the task to run. The system does its best to avoid overlapping invocations.
- **`timeout`**:
+1 -8
View File
@@ -26,18 +26,11 @@ Then add the plugin to the plugin catalog `packages/backend/src/plugins/catalog.
```ts
/* packages/backend/src/plugins/catalog.ts */
import { GerritEntityProvider } from '@backstage/plugin-catalog-backend-module-gerrit';
import { Duration } from 'luxon';
const builder = await CatalogBuilder.create(env);
/** ... other processors and/or providers ... */
builder.addEntityProvider(
GerritEntityProvider.fromConfig(env.config, {
logger: env.logger,
// optional: alternatively, use scheduler with schedule defined in app-config.yaml
schedule: env.scheduler.createScheduledTaskRunner({
frequency: { minutes: 30 },
timeout: { minutes: 3 },
}),
// optional: alternatively, use schedule
scheduler: env.scheduler,
}),
);
@@ -57,7 +50,7 @@ catalog:
host: gerrit-your-company.com
branch: master # Optional
query: 'state=ACTIVE&prefix=webapps'
schedule: # optional; same options as in TaskScheduleDefinition
schedule:
# supports cron, ISO duration, "human duration" as used in code
frequency: { minutes: 30 }
# supports ISO duration, "human duration" as used in code
+7 -20
View File
@@ -40,12 +40,6 @@ export default async function createPlugin(
builder.addEntityProvider(
GithubEntityProvider.fromConfig(env.config, {
logger: env.logger,
// optional: alternatively, use scheduler with schedule defined in app-config.yaml
schedule: env.scheduler.createScheduledTaskRunner({
frequency: { minutes: 30 },
timeout: { minutes: 3 },
}),
// optional: alternatively, use schedule
scheduler: env.scheduler,
}),
);
@@ -85,12 +79,6 @@ export default async function createPlugin(
/* highlight-add-start */
const githubProvider = GithubEntityProvider.fromConfig(env.config, {
logger: env.logger,
// optional: alternatively, use scheduler with schedule defined in app-config.yaml
schedule: env.scheduler.createScheduledTaskRunner({
frequency: { minutes: 30 },
timeout: { minutes: 3 },
}),
// optional: alternatively, use schedule
scheduler: env.scheduler,
});
env.eventBroker.subscribe(githubProvider);
@@ -122,7 +110,7 @@ catalog:
filters:
branch: 'main' # string
repository: '.*' # Regex
schedule: # optional; same options as in TaskScheduleDefinition
schedule: # same options as in TaskScheduleDefinition
# supports cron, ISO duration, "human duration" as used in code
frequency: { minutes: 30 }
# supports ISO duration, "human duration" as used in code
@@ -213,7 +201,7 @@ This provider supports multiple organizations via unique provider IDs.
Defaults to `false`.
Due to limitations in the GitHub API's ability to query for repository objects, this option cannot be used in
conjunction with wildcards in the `catalogPath`.
- **`schedule`** _(optional)_:
- **`schedule`**:
- **`frequency`**:
How often you want the task to run. The system does its best to avoid overlapping invocations.
- **`timeout`**:
@@ -229,13 +217,12 @@ GitHub [rate limits](https://docs.github.com/en/rest/overview/resources-in-the-r
accounts). The snippet below refreshes the Backstage catalog data every 35 minutes, which issues an API request for each discovered location.
If your requests are too frequent then you may get throttled by
rate limiting. You can change the refresh frequency of the catalog in your `packages/backend/src/plugins/catalog.ts` file:
rate limiting. You can change the refresh frequency of the catalog in your `app-config.yaml` file by controlling the `schedule`.
```typescript
schedule: env.scheduler.createScheduledTaskRunner({
frequency: { minutes: 35 },
timeout: { minutes: 30 },
}),
```yaml
schedule:
frequency: { minutes: 35 }
timeout: { minutes: 3 }
```
More information about scheduling can be found on the [TaskScheduleDefinition](https://backstage.io/docs/reference/backend-tasks.taskscheduledefinition) page.
+15 -11
View File
@@ -295,7 +295,8 @@ Webpack configuration itself varies very little between the frontend development
and production bundling, so we'll dive more into the configuration in the
production section below. The main differences are that `process.env.NODE_ENV`
is set to `'development'`, minification is disabled, cheap source maps are used,
and [React Hot Loader](https://github.com/gaearon/react-hot-loader) is enabled.
and [React Refresh](https://github.com/pmmmwh/react-refresh-webpack-plugin#readme)
is enabled.
If you prefer to run type checking and linting as part of the Webpack process,
you can enable usage of the
@@ -589,22 +590,25 @@ For your productivity working with unit tests it's quite essential to have your
A complete launch configuration for VS Code debugging may look like this:
```json
```jsonc
{
"type": "node",
"name": "vscode-jest-tests",
"name": "vscode-jest-tests.v2",
"request": "launch",
"args": [
"repo",
"test",
"--runInBand",
"--watchAll=false",
"--testNamePattern",
"${jest.testNamePattern}",
"--runTestsByPath",
"${jest.testFile}"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"program": "${workspaceFolder}/node_modules/.bin/jest",
"cwd": "${workspaceFolder}",
"args": [
"--config",
"node_modules/@backstage/cli/config/jest.js",
"--runInBand",
"--watchAll=false"
]
"program": "${workspaceFolder}/node_modules/.bin/backstage-cli"
}
```
+41
View File
@@ -47,3 +47,44 @@ The resulting log should now have more information available for debugging:
[1] 2023-04-12T00:51:44.118Z search info Collating documents for tools succeeded type=plugin documentType=tools
[1] 2023-04-12T00:51:44.119Z backstage debug task: search_index_tools will next occur around 2023-04-11T21:01:44.118-04:00 type=taskManager task=search_index_tools
```
## Debugger
### VSCode
In your `launch.json`, add a new entry with the following,
```jsonc
{
"name": "Start Backend",
"type": "node",
"request": "launch",
"args": [
"package",
"start"
],
"cwd": "${workspaceFolder}/packages/backend",
"program": "${workspaceFolder}/node_modules/.bin/backstage-cli",
"skipFiles": [
"<node_internals>/**"
],
"console": "integratedTerminal"
},
```
### WebStorm
This section describes the process for enabling run configurations for Backstage in WebStorm.
Run configurations enable the use of debugging functionality such as steppers and breakpoints.
1. Select `Edit Configurations` in the `Run` dropdown menu. Click the plus sign to add a new
configuration, then select `Node.js`.
2. In `Working directory`, input `{PROJECT_DIR}/packages/backend`.
Replace `{PROJECT_DIR}` with the path to your Backstage repo.
3. In `JavaScript file`, input `{PROJECT_DIR}/node_modules/@backstage/cli/bin/backstage-cli`.
Replace `{PROJECT_DIR}` with the path to your Backstage repo.
4. In `Application parameters`, input `package start`.
5. Optionally, for `Environment Variables`, input `LOG_LEVEL=debug`.
6. Click `Apply` to save the changes.
7. With the newly-created configuration selected, use the `Run` or `Debug` icons on the
toolbar to execute the newly created configuration.
+115
View File
@@ -0,0 +1,115 @@
---
id: 01-getting-started
title: Schema-first plugins with OpenAPI (Experimental)
description: Tutorial on how to start using OpenAPI schema-first development in your plugins.
---
# Getting started with OpenAPI in your Backstage plugins
Target Audience: Plugin developers
Difficulty: Medium
## Goal
The goal of this tutorial is to give you exposure to tools that more tightly couple your OpenAPI specification and plugin lifecycle. The tools we'll be presenting were created by the OpenAPI tooling project area and allow you to create,
1. A typed `express` router that provides strong guardrails during development for input and output values. Support for query, path parameters and request body, as well as experimental support for headers and cookies.
2. An auto-generated client to interact with your plugin's backend. Support for all request types, parameters and body, as well as return types. Provides a low-level interface to allow more customization by higher level libraries.
3. Validation and verification tooling to ensure your API and specification stay in sync. Includes testing against your unit tests.
## Prerequisites
### Technical Knowledge
This tutorial assumes that you're already familiar with the following,
1. How to build a Backstage plugin.
2. `Express.js` and `Typescript`
3. OpenAPI 3.0 schemas
### Setting up
There are two required npm packages before we start,
1. `@backstage/repo-tools`, this package contains all OpenAPI related commands for your plugins. We will be using this throughout the tutorial.
2. `@opticdev/optic`, this package is a dependency of `@backstage/repo-tools` but is only required for OpenAPI related commands.
You should install both of the above packages in the _root_ of your workspace.
## Storing your OpenAPI specification
You should create a new folder, `src/schema` in your backend plugin to store your OpenAPI (and any other) specifications. For example, if you're adding a specification to the catalog plugin, you would add a `src/schema` folder to `plugins/catalog-backend`, making a `plugins/catalog-backend/src/schema` directory. This directory should have an `openapi.yaml` file inside.
> Currently, only the `.yaml` extension is supported, not `.yml`.
## Generating a typed express router from a spec
Run `yarn backstage-repo-tools schema openapi generate <plugin-directory>`. This will create an `openapi.generated.ts` file in the `src/schema` directory that contains the OpenAPI schema as well as a generated express router with types.
Use it like so, update your `router.ts` or `createRouter.ts` file with the following content,
```diff
+ import { createOpenApiRouter } from '../schema/openapi.generated';
- import Router from 'express-promise-router';
...
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
+ const router = await createOpenApiRouter();
- const router = Router();
```
## Generating a typed client from a spec
Run `yarn backstage-repo-tools schema openapi generate-client --input-spec <plugin-directory>/src/schema/openapi.yaml --output-directory <plugin-client-directory>`. `<plugin-directory>` should match the same backend plugin we've been using so far. `<plugin-client-directory>` is a new directory and npm package that you should create. The general pattern is `plugins/<plugin-name>-client`.
The generated client will have a directory `src/generated` that exports a `DefaultApiClient` class and all generated types. You can use the client like so,
```diff
+ import { DefaultApiClient } from './generated';
export class CatalogClient implements CatalogApi {
+ private readonly apiClient: DefaultApiClient;
constructor(options: {
discoveryApi: { getBaseUrl(pluginId: string): Promise<string> };
fetchApi?: { fetch: typeof fetch };
}) {
+ this.apiClient = new DefaultApiClient(options);
}
...
```
usage of the types will depend on your type names.
You should be able to use the generated `DefaultApi.client.ts` file out of the box for your API needs. For full customization, you can use a wrapper around the generated client to adjust the flavor of your clients.
For more information, see [the docs](./generate-client.md).
## Validating your spec with test traffic
Add the following lines to your `createRouter.test.ts` or `router.test.ts` file,
```diff
+ import { wrapInOpenApiTestServer } from '@backstage/backend-openapi-utils';
+ import { Server } from 'http';
...
describe('createRouter', () => {
- let app: express.Express;
+ let app: express.Express | Server;
...
- app = express().use(router);
+ app = wrapInOpenApiTestServer(express().use(router));
```
This adds a wrapper around the express server that allows it to reroute traffic for `supertest`. Run `yarn backstage-repo-tools schema openapi init` to create some required config files. Now, when you run `yarn backstage-repo-tools schema openapi test` your schema will now be tested against your test data. Any errors will be reported.
Our command is a small wrapper over [`Optic`](https://github.com/opticdev/optic) which does all of the heavy lifting.
For more information, see [the docs](./test-case-validation.md).
+31
View File
@@ -0,0 +1,31 @@
---
id: generate-client
title: Generate a client from your OpenAPI spec
description: Documentation on how to create a client for a given OpenAPI spec
---
## How to generate a client with `repo-tools schema openapi generate-client`?
### Prerequisites
1. Add your plugin ID as the last `servers` item, like this,
```yaml
servers:
# first value, used for OpenAPI router validation.
- url: /
# final value, pluginId.
- url: catalog
```
2. Find or create a new plugin to house your new generated client. Currently, we do not support generating an entirely new plugin and instead just generate client files.
### Generating your client
1. Run `yarn backstage-repo-tools schema openapi generate-client --input-spec <file> --output-directory <directory>`. This will create a new folder in `<directory>/src/generated` to house the generated content.
2. You should use the generated files as follows,
- `apis/DefaultApi.client.ts` - this is the client that you should use. It has types for all of the various operations on your API.
- `models/*` - These are the types generated from your OpenAPI file, ideally you should not need to use these directly and can instead use the inferred types from `apis/DefaultApi.client.ts`.
- everything else is directory specific and shouldn't be touched.
+6
View File
@@ -1,3 +1,9 @@
---
id: test-case-validation
title: Validate your OpenAPI spec against test data
description: Documentation on how to use the `schema openapi test` command.
---
## OpenAPI Validation using Test Cases
This is primarily performed by `backstage-repo-tools schema openapi test`. Any errors found in the generated specs can be either
+9 -1
View File
@@ -158,7 +158,7 @@ package export.
The Backstage project uses [Node.js](https://nodejs.org/) for both its development
tooling and backend runtime. In order for expectations to be clear we use the
following schedule for determining the [Node.js releases](https://nodejs.org/en/about/releases/) that we support:
following schedule for determining the [Node.js releases](https://nodejs.org/en/about/previous-releases) that we support:
- At any given point in time we support exactly two adjacent even-numbered
releases of Node.js, for example v12 and v14.
@@ -182,3 +182,11 @@ The TypeScript release cadence is roughly every three months. An important aspec
Our policy is to support the last 3 TypeScript versions, for example 4.8, 4.9, and 5.0. Converted to time, this means that we typically support the TypeScript version from the last six to nine months, depending on where in the TypeScript release window we are. This policy applies as a snapshot at the time of any given Backstage release, new TypeScript releases only apply to the following Backstage main-line release, not to the current one.
For anyone maintaining their own Backstage project, this means that you should strive to bump to the latest TypeScript version at least every 6 months, or you may encounter breakages as you upgrade Backstage packages. If you encounter any issues in doing so, please [file an issue in the main Backstage repository](https://github.com/backstage/backstage/issues/new/choose), as per this policy we should always support the latest version. In order to ensure that we do not start using new TypeScript features too early, the Backstage project itself uses the version at the beginning of the currently supported window, in the above example that would be version 4.8.
## PostgreSQL Releases
The Backstage project recommends and supports using PostgreSQL for persistent storage.
The PostgreSQL [versioning policy](https://www.postgresql.org/support/versioning/) is to release a new major version every year with new features which is then supported for 5 years after its initial release.
Our policy mirrors the PostgreSQL versioning policy - we will support the last 5 major versions. We will also test the newest and oldest versions in that range. For example, if the range we support is currently 12 to 16, then we would only test 12 and 16 explicitly.
+83 -35
View File
@@ -8,9 +8,17 @@ For some use cases, you may want to define custom [rules](./concepts.md#resource
## Define a custom rule
Plugins should export a rule factory that provides type-safety that ensures compatibility with the plugin's backend. The catalog plugin exports `createCatalogPermissionRule` from `@backstage/plugin-catalog-backend/alpha` for this purpose. Note: the `/alpha` path segment is temporary until this API is marked as stable. For this example, we'll define the rule in `packages/backend/src/plugins/permission.ts`, but you can put it anywhere that's accessible by your `backend` package.
Plugins should export a rule factory that provides type-safety that ensures compatibility with the plugin's backend. The catalog plugin exports `createCatalogPermissionRule` from `@backstage/plugin-catalog-backend/alpha` for this purpose. Note: the `/alpha` path segment is temporary until this API is marked as stable. For this example, we'll define the rule and create a condition in `packages/backend/src/plugins/permission.ts`.
We use Zod in our example below. To install, run:
```bash
yarn workspace backend add zod
```
```typescript title="packages/backend/src/plugins/permission.ts"
...
import type { Entity } from '@backstage/catalog-model';
import { createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha';
import { createConditionFactory } from '@backstage/plugin-permission-node';
@@ -41,42 +49,55 @@ export const isInSystemRule = createCatalogPermissionRule({
});
const isInSystem = createConditionFactory(isInSystemRule);
...
```
For a more detailed explanation on defining rules, refer to the [documentation for plugin authors](./plugin-authors/03-adding-a-resource-permission-check.md#adding-support-for-conditional-decisions).
## Provide the rule during plugin setup
Now that we have a custom rule defined, we need provide it to the catalog plugin. This step is important because the catalog plugin will use the rule's `toQuery` and `apply` methods while evaluating conditional authorize results. There's no guarantee that the catalog and permission backends are running on the same server, so we must explicitly link the rule to ensure that it's available at runtime.
The api for providing custom rules may differ between plugins, but there should typically be some integration point during the creation of the backend router. For the catalog, this integration point is exposed via `CatalogBuilder.addPermissionRules`.
```typescript title="packages/backend/src/plugins/catalog.ts"
import { isInSystemRule } from './permission';
// The CatalogBuilder with the addPermissionRules function is in the alpha path
import { CatalogBuilder } from '@backstage/plugin-catalog-backend/alpha';
...
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const builder = await CatalogBuilder.create(env);
builder.addPermissionRules(isInSystemRule);
...
return router;
}
```
The new rule is now ready for use in a permission policy!
## Use the rule in a policy
Let's bring this all together by extending the example policy from the previous section.
Still in the `packages/backend/src/plugins/permission.ts` file, let's use the condition we just created in our `TestPermissionPolicy`.
```ts title="packages/backend/src/plugins/permission.ts"
...
/* highlight-remove-next-line */
import { createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha';
/* highlight-add-next-line */
import { isInSystem } from './catalog';
import { catalogConditions, createCatalogConditionalDecision, createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha';
/* highlight-remove-next-line */
import { createConditionFactory } from '@backstage/plugin-permission-node';
/* highlight-add-next-line */
import { PermissionPolicy, PolicyQuery, createConditionFactory } from '@backstage/plugin-permission-node';
/* highlight-add-start */
import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
import { AuthorizeResult, PolicyDecision, isResourcePermission } from '@backstage/plugin-permission-common';
/* highlight-add-end */
...
export const isInSystemRule = createCatalogPermissionRule({
name: 'IS_IN_SYSTEM',
description: 'Checks if an entity is part of the system provided',
resourceType: 'catalog-entity',
paramsSchema: z.object({
systemRef: z
.string()
.describe('SystemRef to check the resource is part of'),
}),
apply: (resource: Entity, { systemRef }) => {
if (!resource.relations) {
return false;
}
return resource.relations
.filter(relation => relation.type === 'partOf')
.some(relation => relation.targetRef === systemRef);
},
toQuery: ({ systemRef }) => ({
key: 'relations.partOf',
values: [systemRef],
}),
});
const isInSystem = createConditionFactory(isInSystemRule);
class TestPermissionPolicy implements PermissionPolicy {
async handle(
@@ -95,20 +116,47 @@ class TestPermissionPolicy implements PermissionPolicy {
{
anyOf: [
catalogConditions.isEntityOwner({
claims: user?.identity.ownershipEntityRefs ?? []
claims: user?.identity.ownershipEntityRefs ?? [],
}),
isInSystem('interviewing')
]
}
isInSystem({ systemRef: 'interviewing' }),
],
},
/* highlight-add-end */
);
}
return { result: AuthorizeResult.ALLOW };
}
}
...
```
## Provide the rule during plugin setup
Now that we have a custom rule defined and added to our policy, we need provide it to the catalog plugin. This step is important because the catalog plugin will use the rule's `toQuery` and `apply` methods while evaluating conditional authorize results. There's no guarantee that the catalog and permission backends are running on the same server, so we must explicitly link the rule to ensure that it's available at runtime.
The api for providing custom rules may differ between plugins, but there should typically be some integration point during the creation of the backend router. For the catalog, this integration point is exposed via `CatalogBuilder.addPermissionRules`.
```typescript title="packages/backend/src/plugins/catalog.ts"
import { CatalogBuilder } from '@backstage/plugin-catalog-backend';
/* highlight-add-next-line */
import { isInSystemRule } from './permission';
...
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const builder = await CatalogBuilder.create(env);
/* highlight-add-next-line */
builder.addPermissionRules(isInSystemRule);
...
return router;
}
```
The updated policy will allow catalog entity resource permissions if any of the following are true:
- User owns the target entity
- Target entity is part of the `'interviewing'` system
- Target entity is part of the 'interviewing' system
+12 -1
View File
@@ -21,7 +21,18 @@ description: A brief description of the plugin. # Max 170 characters
documentation: # A link to your documentation E.g. Your github README
iconUrl: # Used as the src attribute for your logo.
# You can provide an external url or add your logo under static/img and provide a path
# relative to static/ e.g. img/my-logo.png
# relative to static/ e.g. /img/my-logo.png
npmPackageName: # Your npm package name E.g. '@backstage/plugin-<etc>' quotes are required
addedDate: # The date plugin added to directory E.g. '2022-10-01' quotes are required
```
## Submission Tips
Here are a few tips to help speed up the review process when you submit your plugin:
- For any icon that you use make sure you have the proper rights to use it.
- Make sure that your package had been published on the NPM registry and that it's public.
- Make sure your package on NPM has a link back to your code repo, this helps provide confidence that it's the right package.
- Where possible, please use an [NPM scope](https://docs.npmjs.com/about-scopes) that matches either your Organization name or user name, this provides trust in the plugin
- If your plugin has both a frontend and backend link the documentation to the frontend package but make sure it mentioned needing to install the backend package.
- Where possible include a screenshot of the features in you plugin documentation, it really does help when deciding to use a plugin.
+3 -1
View File
@@ -41,9 +41,11 @@ standalone mode. You can do a first-light test of your service:
```sh
cd plugins/carmen-backend
yarn start
LEGACY_BACKEND_START=true yarn start
```
> Note: `LEGACY_BACKEND_START=true` is needed while we transition fully to the [New Backend System](../backend-system/index.md). The templates have not been migrated yet; you can track this in [issue 21288](https://github.com/backstage/backstage/issues/21288)
This will think for a bit, and then say `Listening on :7007`. In a different
terminal window, now run
+113 -7
View File
@@ -6,11 +6,11 @@ description: Documentation on adding internationalization to the plugin
## Overview
The Backstage core function provides internationalization for plugins
The Backstage core function provides internationalization for plugins. The underlying library is [`i18next`](https://www.i18next.com/) with some additional Backstage typescript magic for type safety with keys.
## For a plugin developer
When you are creating your plugin, you have the possibility to use `createTranslationRef` to define all messages for your plugin. For example
When you are creating your plugin, you have the possibility to use `createTranslationRef` to define all messages for your plugin. For example:
```ts
import { createTranslationRef } from '@backstage/core-plugin-api/alpha';
@@ -19,8 +19,13 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha';
export const myPluginTranslationRef = createTranslationRef({
id: 'plugin.my-plugin',
messages: {
index_page_title: 'All your components',
create_component_button_label: 'Create new component',
indexPage: {
title: 'All your components',
createButtonTitle: 'Create new component',
},
entityPage: {
notFound: 'Entity not found',
},
},
});
```
@@ -33,14 +38,115 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
const { t } = useTranslationRef(myPluginTranslationRef);
return (
<PageHeader title={t('index_page_title')}>
<PageHeader title={t('indexPage.title')}>
<Button onClick={handleCreateComponent}>
{t('create_component_button_label')}
{t('indexPage.createButtonTitle')}
</Button>
</PageHeader>
);
```
You will see how the initial dictionary structure and nesting gets converted into dot notation, so we encourage `camelCase` in key names and lean on the nesting structure to separate keys.
### Guidelines for `i18n` messages and keys
The API for `i18n` messages and keys can be pretty tricky to get right, as it's a pretty flexible API. We've put together some guidelines to help you get started that encourage good practices when thinking about translating plugins:
#### Key names
When defining messages it is recommended to use a nested structure that represents the semantic hierarchy in your translations. This allows for better organization and understanding of the structure. For example:
```ts
export const myPluginTranslationRef = createTranslationRef({
id: 'plugin.my-plugin',
messages: {
dashboardPage: {
title: 'All your components',
subtitle: 'Create new component',
widgets: {
weather: {
title: 'Weather',
description: 'Shows the weather',
},
calendar: {
title: 'Calendar',
description: 'Shows the calendar',
},
},
},
entityPage: {
notFound: 'Entity not found',
},
},
});
```
Think about the semantic placement of content rather than the text content itself. Group related translations under a common prefix, and use nesting to represent relationships between different parts of your application. It's good to start grouping under extensions, page sections, or visual scopes and experiences.
Translations should avoid using their own text content as key where possible, as this can lead to confusion if the translation changes. Instead prefer to use keys that describe the location or usage of the text.
#### Common Key names
This list is intended to grow over time, but below are some examples of common key names and patterns that we encourage you to use where possible:
- `${page}.title`
- `${page}.subtitle`
- `${page}.description`
- `${page}.header.title`
#### Key reuse
Reusing the same key in multiple places is discouraged. This helps prevent ambiguity, and instead keeps the usage of each key as clear as possible. Consider creating duplicate keys that are grouped under a semantic section instead.
#### Flat keys
Avoid a flat key structure at the root level, as it can lead to naming conflicts and make the translation file harder to manage and change evolve over time. Instead, group translations under a common prefix.
```ts
export const myPluginTranslationRef = createTranslationRef({
id: 'plugin.my-plugin',
messages: {
// this is BAD
title: 'My page',
subtitle: 'My subtitle',
// this is GOOD
dashboardPage: {
header: {
title: 'All your components',
subtitle: 'Create new component',
},
},
},
});
```
#### Plurals
The `i18next` library, which is used as the underlying implementation, has built-in support for pluralization. You can use this feature as is described in [the documentation](https://www.i18next.com/translation-function/plurals).
We encourage you to use this feature and avoid creating different key prefixes for pluralized content. For example:
```ts
export const myPluginTranslationRef = createTranslationRef({
id: 'plugin.my-plugin',
messages: {
dashboardPage: {
title: 'All your components',
subtitle: 'Create new component',
cards: {
title_one: 'You have one card',
title_two: 'You have two cards',
title_other: 'You have many cards ({{count}})',
},
},
entityPage: {
notFound: 'Entity not found',
},
},
});
```
## For an application developer overwrite plugin messages
In an app you can both override the default messages, as well as register translations for additional languages:
@@ -53,7 +159,7 @@ In an app you can both override the default messages, as well as register transl
+ createTranslationMessages({
+ ref: myPluginTranslationRef,
+ messages: {
+ create_component_button_label: 'Create new entity',
+ 'indexPage.createButtonTitle': 'Create new entity',
+ },
+ }),
+ createTranslationResource({
+4 -5
View File
@@ -6,25 +6,24 @@ description: Details of the new backend system
## Status
The new backend system is in alpha, and some plugins do not yet fully implement it. But do feel free to try it out! We would love to hear back about your impressions.
The new backend system is released and ready for production use, and many plugins and modules have already been migrated. We recommend all plugins and deployments to migrate to the new system.
You can find an example backend setup in [the backend-next package](https://github.com/backstage/backstage/tree/master/packages/backend-next).
## Overview
The new Backstage backend system is being built to help make it simpler to install backend plugins and to keep projects up to date. It also changes the foundation to one that makes it a lot easier to evolve plugins and the system itself with minimal disruption or cause for breaking changes. You can read more about the reasoning in the [original RFC](https://github.com/backstage/backstage/issues/11611).
The new Backstage backend system was built to help make it simpler to install backend plugins and to keep projects up to date. It also changed the foundation to one that makes it a lot easier to evolve plugins and the system itself with minimal disruption or cause for breaking changes. You can read more about the reasoning in the [original RFC](https://github.com/backstage/backstage/issues/11611).
One of the goals of the new system was to reduce the code needed for setting up a Backstage backend and installing plugins. This is an example of how you create, add features, and start up your backend in the new system:
```ts
import { createBackend } from '@backstage/backend-defaults';
import { catalogPlugin } from '@backstage/plugin-catalog-backend';
// Create your backend instance
const backend = createBackend();
// Install all desired features
backend.add(catalogPlugin());
backend.add(import('@backstage/plugin-catalog-backend'));
// Start up the backend
await backend.start();
@@ -147,8 +146,8 @@ import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'
import { MyCustomProcessor } from './processor';
export const exampleCustomProcessorCatalogModule = createBackendModule({
moduleId: 'exampleCustomProcessor',
pluginId: 'catalog',
moduleId: 'example-custom-processor',
register(env) {
env.registerInit({
deps: {
+7
View File
@@ -53,6 +53,7 @@ Additional steps for the main line release
- Add the release note file as [`/docs/releases/vx.y.0.md`](./releases)
- Add an entry to [`/microsite/sidebar.json`](https://github.com/backstage/backstage/blob/master/microsite/sidebars.json) for the release note
- Update the navigation bar item in [`/microsite/docusaurus.config.js`](https://github.com/backstage/backstage/blob/master/microsite/docusaurus.config.js) to point to the new release note
- Finally copy the content, without the metadata header, into the description of the [`Version Packages` Pull Request](https://github.com/backstage/backstage/pulls?q=is%3Aopen+is%3Apr+in%3Atitle+%22Version+Packages)
Once the release has been published edit the newly created release in the [GitHub repository](https://github.com/backstage/backstage/releases) and replace the text content with the release notes.
@@ -134,3 +135,9 @@ process is used to release an emergency fix as version `6.5.1` in the patch rele
- [ ] The fix, which you can likely cherry-pick from your patch branch: `git cherry-pick origin/patch/v1.18.0^`
- [ ] An updated `CHANGELOG.md` of all patched packages from the tip of the patch branch, `git checkout origin/patch/v1.18.0 -- {packages,plugins}/*/CHANGELOG.md`. Note that if the patch happens after any next-line releases you'll need to restore those entries in the changelog, placing the patch release entry beneath any next-line release entries.
- [ ] A changeset with the message "Applied the fix from version `6.5.1` of this package, which is part of the `v1.18.1` release of Backstage."
## Troubleshooting
### When the release workflow is not triggered for some reason, such as a GitHub incident
Ask one of the maintainers to force push master back to a previous commit and then push the release merge commit again.
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
---
id: v1.20.0
title: v1.20.0
description: Backstage Release v1.20.0
---
These are the release notes for the v1.20.0 release of [Backstage](https://backstage.io/).
A huge thanks to the whole team of maintainers and contributors as well as the amazing Backstage Community for the hard work in getting this release developed and done.
## Highlights
### Support for experimental type build has been removed
The `--experimental-type-build` option is no longer supported by any commands in the Backstage CLI. Existing usage should be migrated to using [subpath exports](https://backstage.io/docs/local-dev/cli-build-system#subpath-exports) instead.
### Experimental support for Vite ⚡
Due to some concerns about performance in larger monorepo setups and hot-reloading times, were looking to replace [Webpack](https://webpack.js.org/) with [Vite](https://vitejs.dev/) longer term, and in this release were shipping experimental support for Vite behind an `EXPERIMENTAL_VITE` environment variable flag when running `yarn start` in the frontend.
Please reach out by [creating a GitHub issue](https://github.com/backstage/backstage/issues/new/choose) with any problems that you might be running into when using this experimental feature.
### Scaffolder: `react-jsonschema-form@v5` + Design tweaks 💅
Its finally here. Weve shipped support for `react-jsonschema-form@v5` which **should** be a completely non-breaking change for end users. Weve also tweaked some of the designs for some of the new Scaffolder Pages. You can read more in the [relevant migration docs](https://backstage.io/docs/features/software-templates/migrating-to-rjsf-v5)
### Catalog Presentation API
The Catalog plugin now uses the `EntityPresentationApi` to decide how entities are rendered in the UI. This includes fetching catalog data in the background in order to show entities using their `metadata.title`. For more information, including how to customize the presentation, see [the PR](https://github.com/backstage/backstage/pull/17474), (documentation is in the works!).
## Security Fixes
This release does not contain any security fixes.
## Upgrade path
We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated).
## Links and References
Below you can find a list of links and references to help you learn about and start using this new release.
- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/)
- [GitHub repository](https://github.com/backstage/backstage)
- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy)
- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support
- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.20.0-changelog.md)
- Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins)
Sign up for our [newsletter](https://info.backstage.spotify.com/newsletter_subscribe) if you want to be informed about what is happening in the world of Backstage.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+140
View File
@@ -0,0 +1,140 @@
# Release v1.21.0-next.1
## @backstage/core-compat-api@0.0.1-next.0
### Patch Changes
- c219b168aa: Made package public so it can be published
## @backstage/create-app@0.5.8-next.1
### Patch Changes
- Bumped create-app version.
## @backstage/plugin-api-docs@0.10.2-next.1
### Patch Changes
- Updated dependencies
- @backstage/plugin-catalog@1.16.0-next.1
## @backstage/plugin-bazaar@0.2.20-next.1
### Patch Changes
- Updated dependencies
- @backstage/plugin-catalog@1.16.0-next.1
## @backstage/plugin-catalog@1.16.0-next.1
### Patch Changes
- Updated dependencies
- @backstage/core-compat-api@0.0.1-next.0
## @backstage/plugin-catalog-import@0.10.4-next.1
### Patch Changes
- Updated dependencies
- @backstage/core-compat-api@0.0.1-next.0
## @backstage/plugin-graphiql@0.3.1-next.1
### Patch Changes
- Updated dependencies
- @backstage/core-compat-api@0.0.1-next.0
## @backstage/plugin-search@1.4.4-next.1
### Patch Changes
- Updated dependencies
- @backstage/core-compat-api@0.0.1-next.0
## @backstage/plugin-tech-radar@0.6.11-next.1
### Patch Changes
- Updated dependencies
- @backstage/core-compat-api@0.0.1-next.0
## @backstage/plugin-techdocs@1.9.2-next.1
### Patch Changes
- Updated dependencies
- @backstage/core-compat-api@0.0.1-next.0
## @backstage/plugin-techdocs-addons-test-utils@1.0.25-next.1
### Patch Changes
- Updated dependencies
- @backstage/plugin-catalog@1.16.0-next.1
- @backstage/plugin-techdocs@1.9.2-next.1
## @backstage/plugin-user-settings@0.7.14-next.1
### Patch Changes
- Updated dependencies
- @backstage/core-compat-api@0.0.1-next.0
## example-app@0.2.90-next.1
### Patch Changes
- Updated dependencies
- @backstage/plugin-catalog@1.16.0-next.1
- @backstage/plugin-catalog-import@0.10.4-next.1
- @backstage/plugin-graphiql@0.3.1-next.1
- @backstage/plugin-search@1.4.4-next.1
- @backstage/plugin-tech-radar@0.6.11-next.1
- @backstage/plugin-techdocs@1.9.2-next.1
- @backstage/plugin-user-settings@0.7.14-next.1
- @backstage/plugin-api-docs@0.10.2-next.1
- @backstage/plugin-catalog-graph@0.3.2-next.0
- @backstage/plugin-explore@0.4.14-next.0
- @backstage/plugin-org@0.6.18-next.0
- @backstage/plugin-scaffolder@1.16.2-next.0
- @backstage/plugin-scaffolder-react@1.6.2-next.0
- @backstage/plugin-techdocs-module-addons-contrib@1.1.3-next.0
## example-app-next@0.0.4-next.1
### Patch Changes
- Updated dependencies
- @backstage/core-compat-api@0.0.1-next.0
- @backstage/plugin-catalog@1.16.0-next.1
- @backstage/plugin-catalog-import@0.10.4-next.1
- @backstage/plugin-graphiql@0.3.1-next.1
- @backstage/plugin-search@1.4.4-next.1
- @backstage/plugin-tech-radar@0.6.11-next.1
- @backstage/plugin-techdocs@1.9.2-next.1
- @backstage/plugin-user-settings@0.7.14-next.1
- @backstage/plugin-api-docs@0.10.2-next.1
- @backstage/plugin-catalog-graph@0.3.2-next.0
- @backstage/plugin-explore@0.4.14-next.0
- @backstage/plugin-org@0.6.18-next.0
- @backstage/plugin-scaffolder@1.16.2-next.0
- @backstage/plugin-scaffolder-react@1.6.2-next.0
- @backstage/plugin-techdocs-module-addons-contrib@1.1.3-next.0
## e2e-test@0.2.10-next.1
### Patch Changes
- Updated dependencies
- @backstage/create-app@0.5.8-next.1
## techdocs-cli-embedded-app@0.2.89-next.1
### Patch Changes
- Updated dependencies
- @backstage/plugin-catalog@1.16.0-next.1
- @backstage/plugin-techdocs@1.9.2-next.1
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+135
View File
@@ -0,0 +1,135 @@
---
id: v1.21.0
title: v1.21.0
description: Backstage Release v1.21.0
---
These are the release notes for the v1.21.0 release of [Backstage](https://backstage.io/).
A huge thanks to the whole team of maintainers and contributors as well as the amazing Backstage Community for the hard work in getting this release developed and done.
## Highlights
### New Frontend System Alpha
This release marks the alpha release of the new frontend system, which has been in an experimental state since implementation began in the middle of 2023. This new system brings declarative integration of plugins, which is the ability to integrate new features into a Backstage app without writing any TypeScript code. Through this capability it also paves the way for supporting dynamic plugin installation at runtime.
The alpha release is a point of increased stability following the earlier experimental phase. There is now a complete system that lets you build out a full application, supported by [documentation](https://backstage.io/docs/frontend-system/). From this point on any breaking changes will also be clearly marked in the changelog.
There is still a long road ahead to a stable release, and this is not the time to migrate existing applications. There are only a few plugins that support this system so far, and if you want to add to a plugin that you own, please do so under an `/alpha` sub-path export.
Still, we encourage you to explore this new system to see whether you are confident in this path forward. If you have feedback or want to know more you can reach out in the #declarative-integration channel on Discord, or join an [Adoption SIG](https://github.com/backstage/community/blob/main/sigs/sig-adoption/README.md) meeting where the new system is frequently discussed. You can also check out our [maintainer talk at KubeCon NA 2023](https://youtu.be/ONMBYnhxnNU?t=436), where we talk about this new system and show a couple of demos.
### React Router Beta deprecation
This release of Backstage officially deprecates, but does _not_ immediately remove, support for old beta versions of [`react-router` 6](https://reactrouter.com/). Actual support for beta versions will be removed entirely in an upcoming release of Backstage. Please upgrade your own Backstage project as soon as possible to a stable version of `react-router`, by [following this guide](https://backstage.io/docs/tutorials/react-router-stable-migration/).
### New PostgreSQL versioning policy
The Backstage project has now settled on [a clearer policy](https://backstage.io/docs/overview/versioning-policy/#postgresql-releases) for what versions of PostgreSQL that it supports. In short, we support the last five [released major versions](https://www.postgresql.org/support/versioning/), and actively test against the first and last of those five, in a rolling window over time.
As part of this, the `TestDatabases` utility class now supports all of the last major versions of PostgreSQL in addition to the ones it supported before. You can also call `TestDatabases.setDefaults` inside your `setupTests.ts` file to configure the set of engines to test against, instead of enumerating them in every individual test.
Contributed by [@awanlin](https://github.com/awanlin) in [#21510](https://github.com/backstage/backstage/pull/21510)
### `UnifiedTheme` Now Supports Overrides
You can now supply overrides for Backstage components when using `createUnifiedTheme`. We've updated the demo sites Aperture theme to work with this and you can see the code for that [here](https://github.com/backstage/demo/blob/402cbb358cddacd59b339580bef0a4c5c2c7e013/packages/app/src/theme/aperture.ts#L85).
If you are switching from the old way of defining a theme to `createUnifiedTheme`, note that it uses the MUI v5 overrides format. The style overrides are now nested in a `styleOverrides` key, and if you want access to the theme youll need to use a callback:
```ts
BackstageHeaderTabs: {
styleOverrides: {
defaultTab: {
textTransform: 'none',
},
},
},
MuiChip: {
styleOverrides: {
root: ({ theme }) => ({
color: theme.palette.primary.dark,
}),
},
},
```
### Catalog pagination
`CatalogIndexPage` now offers an optional pagination feature, designed to accommodate adopters managing extensive catalogs. This new capability allows for better handling of large amounts of data.
To activate the pagination mode, simply update your `App.tsx` as follows:
```diff
const routes = (
<FlatRoutes>
...
- <Route path="/catalog" element={<CatalogIndexPage />} />
+ <Route path="/catalog" element={<CatalogIndexPage pagination />} />
...
```
In case you have a custom catalog page and you want to enable pagination, you need to pass the `pagination` prop to `EntityListProvider` instead. For now both column sorting and search filtering are still done locally, meaning they only apply to each individual page. This is something we will improve in the future and we still wanted to make this feature available early as it can greatly improve the performance of the catalog page.
### Azure DevOps Multi-Org Support
The Azure DevOps plugin now has multi-org support and there is a new processor to help with adding the needed annotations. Contributed by [@awanlin](https://github.com/awanlin) in [#19622](https://github.com/backstage/backstage/issues/19622)
### New Authentication providers
A new Atlassian authentication provider has been added to `@backstage/plugin-auth-backend`. Contributed by [@handsamtw](https://github.com/handsamtw) in [#21007](https://github.com/backstage/backstage/pull/21007)
A new VMware Cloud authentication provider has been added to `@backstage/plugin-auth-backend`. Contributed by [@luchillo17](https://github.com/luchillo17) in [#21337](https://github.com/backstage/backstage/pull/21337)
### Kubernetes single cluster selection
You can now select a `single` kubernetes cluster that the entity is part of from all your defined kubernetes clusters, by providing the `backstage.io/kubernetes-cluster` annotation with the defined cluster name.
If you do not specify the annotation then by default it fetches all defined kubernetes clusters.
To apply, update your `catalog-info.yaml`as follows:
```diff
metadata:
annotations:
'backstage.io/kubernetes-id': dice-roller
'backstage.io/kubernetes-namespace': dice-space
+ 'backstage.io/kubernetes-cluster': dice-cluster
'backstage.io/kubernetes-label-selector': 'app=my-app,component=front-end'
```
Contributed by [@deepan10](https://github.com/deepan10) in [#20954](https://github.com/backstage/backstage/pull/20954)
### BREAKING: Repo tools generated API Reports path changes
API Reports generated for sub-path exports now place the name as a suffix rather than prefix, for example `api-report-alpha.md` instead of `alpha-api-report.md`. When upgrading to this version you'll need to re-create any such API reports and delete the old ones.
### PagerDuty plugin changes home 🏡
The [PagerDuty](https://www.pagerduty.com/) plugin has been marked as deprecated in favor of [pagerduty/backstage-plugin](https://github.com/pagerduty/backstage-plugin) which is maintained by PagerDuty themselves! We encourage you to [migrate to @pagerduty/backstage-plugin](https://pagerduty.github.io/backstage-plugin-docs/migration/) in order to receive future updates.
Congrats to the PagerDuty folks for taking ownership of the plugin 👏
Contributed by [@t1agob](https://github.com/t1agob) in [#21436](https://github.com/backstage/backstage/pull/21436)
## Security Fixes
This release does not contain any security fixes.
## Upgrade path
We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated).
## Links and References
Below you can find a list of links and references to help you learn about and start using this new release.
- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/)
- [GitHub repository](https://github.com/backstage/backstage)
- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy)
- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support
- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.21.0-changelog.md)
- Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins)
Sign up for our [newsletter](https://info.backstage.spotify.com/newsletter_subscribe) if you want to be informed about what is happening in the world of Backstage.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+69
View File
@@ -0,0 +1,69 @@
---
id: v1.22.0
title: v1.22.0
description: Backstage Release v1.22.0
---
These are the release notes for the v1.22.0 release of [Backstage](https://backstage.io/).
A huge thanks to the whole team of maintainers and contributors as well as the amazing Backstage Community for the hard work in getting this release developed and done.
## Highlights
### Updates to new frontend system
There have been several updates to alpha packages in the new frontend system including a breaking change where the `app/router` extension was renamed to `app/root`. Furthermore `elements`, `wrappers`, and `router` were added as inputs to `app/root` making it possible to pass extensions into the root of the app.
### Plugins and modules migrated to the New Backend System
Some more features have been migrated to the new backend system:
- `@backstage/plugin-auth-backend-module-microsoft-provider`
- `@backstage/plugin-auth-backend-module-pinniped-provider`
- `@backstage/plugin-catalog-backend-module-openapi`
- `@backstage/plugin-events-backend-module-azure`
- `@backstage/plugin-events-backend-module-bitbucket-cloud`
- `@backstage/plugin-events-backend-module-gerrit`
- `@backstage/plugin-linguist`
### New plugin: App Visualizer
This release includes the new `@backstage/plugin-app-visualizer` package. This plugin for the new frontend system lets you browse and view the extension structure of your app as a graph, detailed list, or in text form.
### New feature: Dynamic Feature Service
This release includes the new `@backstage/backend-dynamic-feature-service` package.
It is a new and experimental service that lets you dynamically detect and load local plugins and modules in your Backstage instance.
Contributed by [@davidfestal](https://github.com/davidfestal) in [#18862](https://github.com/backstage/backstage/pull/18862)
### New Scaffolder action `gitlab:issues:create`
You can now create GitHub issues in your scaffolder flows! Contributed by [@elaine-mattos](https://github.com/elaine-mattos) in [#21929](https://github.com/backstage/backstage/pull/21929)
### New Scaffolder action `gitlab:repo:push`
You can now push raw branches to GitLab in your scaffolder flows! Contributed by [@gavlyukovskiy](https://github.com/gavlyukovskiy) in [#21977](https://github.com/backstage/backstage/pull/21977)
## Security Fixes
This release does not contain any security fixes.
However, some updates were made to the build facilities in the CLI and the caches in the backend system, such that you can now perform builds on FIPS compliant systems. This may lead to some internal cache invalidation happening, since the hashing algorithms used were updated. This should not pose a problem unless caches were being used as reliable persistent storage systems. Please let us know if you encounter any issues that may be related to this.
## Upgrade path
We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated).
## Links and References
Below you can find a list of links and references to help you learn about and start using this new release.
- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/)
- [GitHub repository](https://github.com/backstage/backstage)
- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy)
- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support
- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.22.0-changelog.md)
- Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins)
Sign up for our [newsletter](https://info.backstage.spotify.com/newsletter_subscribe) if you want to be informed about what is happening in the world of Backstage.
+65
View File
@@ -0,0 +1,65 @@
---
id: migrate-to-mui5
title: Migrating from Material UI v4 to v5
description: Additional resources for the Material UI v5 migration guide specifically for Backstage
---
Backstage supports developing new plugins or components using Material UI v5. At the same time, large parts of the application as well as existing plugins will still be using Material UI v4. To support Material UI v4 and v5 at the same time, we have introduced a new concept called the `UnifiedTheme`. The goal of the `UnifiedTheme` is to allow gradual migration by running both versions in parallel, applying theme options similarly & supporting potential future versions of Material UI.
By default, the `UnifiedThemeProvider` is already used. If you add a custom theme in your `createApp` function, you would need to replace the Material UI `ThemeProvider` with the `UnifiedThemeProvider`:
```diff ts
+ import import {
+ UnifiedThemeProvider,
+ themes as builtinThemes,
+ } from '@backstage/theme';
const app = createApp({
// ...
themes: [
{
// ...
provider: ({ children }) => (
- <ThemeProvider theme={lightTheme}>.
- <CssBaseline>{children}</CssBaseline>.
- </ThemeProvider
+ <UnifiedThemeProvider theme={builtinThemes.light} children={children} />
),
}
]
});
```
Before making specific changes to your Backstage instance, it might be helpful to take a look at the [Migration Guide provided by Material UI](https://mui.com/material-ui/migration/migration-v4/) first. It breaks down the differences between v4 and v5, and will make it easier to understand the impact on your Backstage instance & plugins.
It is worth noting that we are still using `@mui/styles` & `jss`. You may stumble upon documentation for migrating to `emotion` when using `makeStyles` or `withStyles`. It is not necessary to switch to `emotion`.
Important to keep in mind is that Material UI v5 is meant to be used with React Version 17 or higher. This means if you intend to use the Material UI v5 components in your plugins, you have to enforce React Version to be at least 17 for these plugins:
```json
...
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
...
```
To comply with Material UI recommendations, we are enforcing a new linting rule that favors standard imports over named imports and also restricts 3rd-level imports as they are considered private ([Guide: Minimizing Bundle Size](https://mui.com/material-ui/guides/minimizing-bundle-size)).
There are `core-components` as well as components exported from Backstage `*-react` plugins written in Material UI v4, which expect Material UI components as props. In these cases you will still be forced to use Material UI v4.
For current known issues with the Material UI v5 migration, follow our [Milestone on GitHub](https://github.com/backstage/backstage/milestone/40). Please open a new issue if you run into different problems.
### Plugins
To migrate your plugin to Material UI v5, you can build on the resources available.
1. Manually fix the imports from named to default imports to match the new [linting rules for minimizing bundle size](https://mui.com/material-ui/guides/minimizing-bundle-size). Note: you can use the [new `@backstage/no-top-level-material-ui-4-imports` ESLint](https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-top-level-material-ui-4-imports.md) rule to help with this.
2. Run the migration `codemod` for the path of the specific plugin: `npx @mui/codemod v5.0.0/preset-safe plugins/<path>`.
3. Take a look at possible `TODO:` items the `codemod` could not fix.
4. Remove types & methods from `@backstage/theme` which are marked as `@deprecated`.
5. Ensure you are using `"react": "^17.0.0"` (or newer) as a peer dependency
You can follow the [migration of the GraphiQL plugin](https://github.com/backstage/backstage/pull/17696) as an example of a plugin migration.
+69
View File
@@ -0,0 +1,69 @@
---
id: setup-opentelemetry
title: Setup OpenTelemetry
description: Tutorial to setup OpenTelemetry metrics and traces exporters in Backstage
---
Backstage uses [OpenTelemetery](https://opentelemetry.io/) to instrument its components by reporting traces and metrics.
This tutorial shows how to setup exporters in your Backstage backend package. For demonstration purposes we will use the simple console exporters.
## Install dependencies
We will use the OpenTelemetry Node SDK and the `auto-instrumentations-node` packages.
Backstage packages, such as the catalog, uses the OpenTelemetry API to send custom traces and metrics.
The `auto-instrumentations-node` will automatically create spans for code called in libraries like Express.
```bash
yarn --cwd packages/backend add @opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/sdk-metrics
```
## Configure
In your `packages/backend/src` folder, create an `instrumentation.ts` file.
```typescript
import { NodeSDK } from '@opentelemetry/sdk-node';
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import {
PeriodicExportingMetricReader,
ConsoleMetricExporter,
} from '@opentelemetry/sdk-metrics';
const sdk = new NodeSDK({
traceExporter: new ConsoleSpanExporter(),
metricReader: new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(),
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
```
In the `index.ts`, import this file **at the beginning**:
```typescript
import './instrumentation'; // Setup the OpenTelemetry instrumentation
// other imports and backend init...
```
It's important to setup the NodeSDK and the automatic instrumentation **before** importing any library.
## Run Backstage
You can now start your Backstage instance as usual, using `yarn dev`.
When the backend is started, you should see in your console traces and metrics emitted by OpenTelemetry.
Of course in production you probably won't use the console exporters but instead send traces and metrics to an OpenTelemetry Collector using [OTLP exporters](https://opentelemetry.io/docs/instrumentation/js/exporters/).
## References
- [Getting started with OpenTelemetry Node.js](https://opentelemetry.io/docs/instrumentation/js/getting-started/nodejs/)
- [OpenTelemetry NodeSDK API](https://open-telemetry.github.io/opentelemetry-js/classes/_opentelemetry_sdk_node.NodeSDK.html)