move over documentation from old place
All checks were successful
Build and Deploy Antora Docs / build (push) Successful in 38s
Build and Deploy Antora Docs / deploy (push) Successful in 5s

This commit is contained in:
Tobias Brunner 2025-03-11 15:37:13 +01:00
parent 16e8c2729b
commit d61465b6ea
No known key found for this signature in database
11 changed files with 1184 additions and 2 deletions

View file

@ -0,0 +1,400 @@
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="889px" height="402px" viewBox="-0.5 -0.5 889 402" content="&lt;mxfile&gt;&lt;diagram id=&quot;oCg05mIILSXJTHsOPe0H&quot; name=&quot;Page-1&quot;&gt;5Vxtk9o2EP41zLQfjrEtv/ERuGvSpp0w5TJp+s3YAjwxiMri7sivr4QlbFkyGDAGLpeZnLW2FunZR7urtbgOGC7ePuBgNf8LRTDpWEb01gGPHcsyPd+kv5hkwyUmsDLJDMcRl+WCcfwDcqHBpes4gqn0IEEoIfFKFoZouYQhkWQBxuhVfmyKEvlTV8EMKoJxGCSq9GsckXkm9R0jl3+E8WwuPtk0+J1FIB7mgnQeROi1IAJPHTDECJHsavE2hAlDT+CS9fut4u5uYBguSZ0Ots/HQTZicjCic+XNJVrSX4M5WSS0ZdJLqhdv/qENo+uI5jfWFI3HN6m1Ea23mBS60dY3oZFe551YQ/RRJ8Pnl6I1DvlwOXNIgGeQP+VkIjaRQjcOwAeIFpCOjD6AYRKQ+EU2asC5Mds9l8NHLziCejRdN9PxEiRrrvWTn1JBf/S7Fuc/gwldGxLAQRLPlvQ6pJOGmApeICYx5V6f31jEUcR0DDBM4x/BZKuPwbVC8ZJsR+8MOs6jFkBhb6YUvnU0i4UrlPgoocZ7PRhdy3S4uo2kqTauXPmIDbygebdcuFphUKECTacptXXZMLsx1rKV3TuW+Scw+NTVcirzgX816gti3S71e01R3+janicxFJxHfK7FvgDNPetEmpsFkueUP0RzieQ55xumudu7Fs09QaKbonltSstgZ9zYQ3PLA47E84emPLwtqb2Ef/fAOyS+Z1yN+PZ7hNO8GpxH5933AKd3LTj9oxd7RWJmHoAmTwJ7oGiJ/XagjRHEMZ0W8/cFjEtInQK6X+HEG8fYUiLfVzihghHCJEgU/DFaLyMYcUxf5zGB41WwncgrDlayOaZxkgxRgvC2L4gC6E9DKk8JRt9h4Y4b+nAy3cdRJRZWxjfLlrcZpti/v+Z7els8My/s5x3nfDRtBc2Pz8+j8ZE0rkShyBCr6VWpD+q9EprCxQkNGY95p7NCu7s/FlE+ztEMLYPkKZfuY5sD/cjWsc23JoDu6cUdUesBdX0kaAd505Wht8X+WajI2KBA38c42BQe4+ll5ee4jvZjckNmCk82q6OsiTHELzFFk+bXYQhTOjY3oXAPJphezdgVXzUGG4ZlPA9Hu+svjyMtTVrfeAq6NlJzAb4rbz0fuPZTOdRo0g0UC35JKaBlM6TzYMUu14ukHxJURHxrnRFKYxIjhjxBK409JogQtJCNiNYkiZd0+YqaL7PPHOH4B1qyAJU9lZT070yucKCBCOMcDjBAE19co9p6deOL6SmmODZFurV6l6OGNvNq+yGrqYzzEDQpnS/ps5cWudqLFdMFQaVquttOFAO+vFqA53VLiVZFHDvBUamhZjhmoYOO2viXgUwjjhpsqG+hiQDTM0oC9lCTOe/hLKQBj1ROFawc5IJTMrVZbxNeSQ0Q6jJaRiW6F0CLgnS+A7gSj4NULczV2TPVMxktegisbVfWUJERq3mXf0BRc+vCUsv479hAdk+uQ9pljte1UFmRZYPL+S6g2ihzXtZ79FiOfVWPJXTsixSD94g7sK6Lu6ngroA4oyiu6k/16O2W6VVmJIcgaCSFV08TVEAABiHb49Ddj1xeLKLinA9LK5NWC2ONLp7pdGqF2tJi5E5cR794KrGrri2WFo+68TMtDYJ2AwiqbyjvEUBTBtBsEUBTdflfEf7O6hjGMFmnBGJNISrfNdwR2G3A6VjX9+QAXNWTOzW2PUd4cutsWFqZ9E168irsbtGTO2qV4h4RvKIrd9QUqr4rH9wV2K3A6SmQvIPTwo3XkOvVNy2zVCGoW2wov7e2/AsWSl31PcINHHiT1ogYYRPnOm1PezLt7PNuQO7SyMs2p0at7tKJVbk603ZiVWOzc0Ridf7h+DYmLXTcVlpQhd0tJlZujeLSHSB4xcTKVTd1msTqjiBtBTTVW/VHoy/xZwbarmCsC6znIAfNyIGeDrme64GgoXKxV8pu/JpkbMIheqpD/EzmWy6ON5SKCzXFh91Zl97+I8ZBszR12D8mp/YsyLMfnRHc7U/tN2enW8QU3/1qxSSqh61pkk9wEyYo+P6zmMUytRnTxQzjKsDe6bmVS+/XXKPXlQ881j+RUkeZpShr7oSsp+7aqpdfgQvuf2u0FSMcQfwQZuuhv9WEf3l4KMp/3RpH9Cit4s8RQj/NCu61GOt81bFmx/AH7GvtWwsj9t/fT+PnimSCTpPIWMvY8cVaNAAX1d/I64ybm9/QW0Wz71IMVZ0Ml0/za9JhV2MU0IRR1GxYfCvCeFTLiOL4axSQICUIVwGmHPQ/CyC3tOHSHEbV4XMCaWkz/wsGmffK/xAEePof&lt;/diagram&gt;&lt;/mxfile&gt;">
<defs/>
<g>
<path d="M 448 155 L 224.07 225.59" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 219.07 227.16 L 224.69 221.72 L 224.07 225.59 L 226.79 228.4 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.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: 210px; margin-left: 258px;">
<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;">
K8s API
</div>
</div>
</div>
</foreignObject>
<text x="258" y="213" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">
K8s API
</text>
</switch>
</g>
<path d="M 448 155 L 592.27 224.73" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 596.99 227.01 L 589.17 227.12 L 592.27 224.73 L 592.21 220.82 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.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: 210px; margin-left: 561px;">
<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;">
K8s API
</div>
</div>
</div>
</foreignObject>
<text x="561" y="214" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">
K8s API
</text>
</switch>
</g>
<path d="M 668 127.5 L 762.72 63.56" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 767.07 60.63 L 763.23 67.44 L 762.72 63.56 L 759.31 61.64 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.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: 91px; margin-left: 719px;">
<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;">
K8s API
</div>
</div>
</div>
</foreignObject>
<text x="719" y="94" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">
K8s API
</text>
</switch>
</g>
<path d="M 668 127.5 L 761.63 127.97" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 766.88 127.99 L 759.86 131.46 L 761.63 127.97 L 759.9 124.46 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 668 127.5 L 762.78 193.85" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 767.08 196.86 L 759.34 195.71 L 762.78 193.85 L 763.36 189.98 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 668 127.5 L 764.34 264.79" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 767.36 269.08 L 760.47 265.37 L 764.34 264.79 L 766.2 261.34 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 637.2 100 L 637.87 66.37" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 637.98 61.12 L 641.34 68.19 L 637.87 66.37 L 634.34 68.05 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<rect x="228" y="100" width="440" height="55" rx="8.25" ry="8.25" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/>
<g transform="translate(-0.5 -0.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: 438px; height: 1px; padding-top: 128px; margin-left: 229px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<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;">
Web Portal
</div>
</div>
</div>
</foreignObject>
<text x="448" y="131" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
Web Portal
</text>
</switch>
</g>
<path d="M 78 128 L 221.63 127.81" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 226.88 127.8 L 219.89 131.31 L 221.63 127.81 L 219.88 124.31 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.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: 128px; margin-left: 153px;">
<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;">
HTTPS
</div>
</div>
</div>
</foreignObject>
<text x="153" y="131" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">
HTTPS
</text>
</switch>
</g>
<path d="M 53 160 L 53 340 Q 53 350 63 350 L 137.9 350" fill="none" stroke="#82b366" stroke-width="3" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 144.65 350 L 135.65 354.5 L 137.9 350 L 135.65 345.5 Z" fill="#82b366" stroke="#82b366" stroke-width="3" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.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: 248px; margin-left: 50px;">
<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;">
Service Access
<br/>
HTTPS / TCP / UDP
</div>
</div>
</div>
</foreignObject>
<text x="50" y="251" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">
Service Access...
</text>
</switch>
</g>
<ellipse cx="53" cy="107.5" rx="7.5" ry="7.5" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<path d="M 53 115 L 53 140 M 53 120 L 38 120 M 53 120 L 68 120 M 53 140 L 38 160 M 53 140 L 68 160" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.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 flex-end; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 97px; margin-left: 53px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<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: nowrap;">
User
</div>
</div>
</div>
</foreignObject>
<text x="53" y="97" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
User
</text>
</switch>
</g>
<path d="M 218 277.5 L 218 321.13" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 218 326.38 L 214.5 319.38 L 218 321.13 L 221.5 319.38 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 378 277.5 L 378 321.13" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 378 326.38 L 374.5 319.38 L 378 321.13 L 381.5 319.38 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
<rect x="148" y="227.5" width="140" height="50" rx="7.5" ry="7.5" fill="#d5e8d4" stroke="#82b366" pointer-events="all"/>
<g transform="translate(-0.5 -0.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: 138px; height: 1px; padding-top: 253px; margin-left: 149px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<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;">
CSP 1 Zone A
<br/>
Control Plane
</div>
</div>
</div>
</foreignObject>
<text x="218" y="256" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
CSP 1 Zone A...
</text>
</switch>
</g>
<path d="M 9 196 L 669 196" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="stroke"/>
<path d="M 482 400 L 482 193.5" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="stroke"/>
<rect x="528" y="227.5" width="140" height="50" rx="7.5" ry="7.5" fill="#d5e8d4" stroke="#82b366" pointer-events="all"/>
<g transform="translate(-0.5 -0.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: 138px; height: 1px; padding-top: 253px; margin-left: 529px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<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;">
CSP 2
<br/>
Control Plane
</div>
</div>
</div>
</foreignObject>
<text x="598" y="256" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
CSP 2...
</text>
</switch>
</g>
<rect x="308" y="227.5" width="140" height="50" rx="7.5" ry="7.5" fill="#d5e8d4" stroke="#82b366" pointer-events="all"/>
<g transform="translate(-0.5 -0.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: 138px; height: 1px; padding-top: 253px; margin-left: 309px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<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;">
CSP 1 Zone B
<br/>
Control Plane
</div>
</div>
</div>
</foreignObject>
<text x="378" y="256" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
CSP 1 Zone B...
</text>
</switch>
</g>
<rect x="178" y="347.5" width="120" height="40" rx="6" ry="6" fill="#fff2cc" stroke="#d6b656" pointer-events="none"/>
<rect x="168" y="337.5" width="120" height="40" rx="6" ry="6" fill="#fff2cc" stroke="#d6b656" pointer-events="none"/>
<rect x="158" y="327.5" width="120" height="40" rx="6" ry="6" fill="#fff2cc" stroke="#d6b656" pointer-events="none"/>
<g transform="translate(-0.5 -0.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: 118px; height: 1px; padding-top: 348px; margin-left: 159px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<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;">
Worker Clusters
<br/>
Zone A
</div>
</div>
</div>
</foreignObject>
<text x="218" y="351" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
Worker Clusters...
</text>
</switch>
</g>
<rect x="338" y="347.5" width="120" height="40" rx="6" ry="6" fill="#fff2cc" stroke="#d6b656" pointer-events="none"/>
<rect x="328" y="337.5" width="120" height="40" rx="6" ry="6" fill="#fff2cc" stroke="#d6b656" pointer-events="none"/>
<rect x="318" y="327.5" width="120" height="40" rx="6" ry="6" fill="#fff2cc" stroke="#d6b656" pointer-events="none"/>
<g transform="translate(-0.5 -0.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: 118px; height: 1px; padding-top: 348px; margin-left: 319px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<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;">
Worker Clusters
<br/>
Zone B
</div>
</div>
</div>
</foreignObject>
<text x="378" y="351" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
Worker Clusters...
</text>
</switch>
</g>
<path d="M 448 155 L 382.42 222.92" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 378.78 226.7 L 381.12 219.23 L 382.42 222.92 L 386.16 224.09 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<g transform="translate(-0.5 -0.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: 211px; margin-left: 388px;">
<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: none; background-color: rgb(255, 255, 255); white-space: nowrap;">
K8s API
</div>
</div>
</div>
</foreignObject>
<text x="388" y="215" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="11px" text-anchor="middle">
K8s API
</text>
</switch>
</g>
<rect x="548" y="347.5" width="120" height="40" rx="6" ry="6" fill="#fff2cc" stroke="#d6b656" pointer-events="none"/>
<rect x="538" y="337.5" width="120" height="40" rx="6" ry="6" fill="#fff2cc" stroke="#d6b656" pointer-events="none"/>
<rect x="528" y="327.5" width="120" height="40" rx="6" ry="6" fill="#fff2cc" stroke="#d6b656" pointer-events="none"/>
<g transform="translate(-0.5 -0.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: 118px; height: 1px; padding-top: 348px; margin-left: 529px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<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;">
Worker Clusters
</div>
</div>
</div>
</foreignObject>
<text x="588" y="351" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
Worker Clusters
</text>
</switch>
</g>
<rect x="768" y="30" width="120" height="60" rx="9" ry="9" fill="#e1d5e7" stroke="#9673a6" pointer-events="none"/>
<g transform="translate(-0.5 -0.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: 118px; height: 1px; padding-top: 60px; margin-left: 769px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<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;">
APPUiO Control API
</div>
</div>
</div>
</foreignObject>
<text x="828" y="64" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
APPUiO Control API
</text>
</switch>
</g>
<rect x="768" y="98" width="120" height="60" rx="9" ry="9" fill="#f5f5f5" stroke="#666666" stroke-dasharray="3 3" pointer-events="none"/>
<g transform="translate(-0.5 -0.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: 118px; height: 1px; padding-top: 128px; margin-left: 769px;">
<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: none; white-space: normal; overflow-wrap: normal;">
Other System
<br/>
e.g. Jira
</div>
</div>
</div>
</foreignObject>
<text x="828" y="132" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
Other System...
</text>
</switch>
</g>
<rect x="768" y="167.5" width="120" height="60" rx="9" ry="9" fill="#f5f5f5" stroke="#666666" stroke-dasharray="3 3" pointer-events="none"/>
<g transform="translate(-0.5 -0.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: 118px; height: 1px; padding-top: 198px; margin-left: 769px;">
<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: none; white-space: normal; overflow-wrap: normal;">
Other System
<br/>
e.g. Keycloak
</div>
</div>
</div>
</foreignObject>
<text x="828" y="201" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
Other System...
</text>
</switch>
</g>
<path d="M 597.5 277.5 L 597.5 321.13" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 597.5 326.38 L 594 319.38 L 597.5 321.13 L 601 319.38 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<rect x="768" y="240" width="120" height="60" rx="9" ry="9" fill="#f5f5f5" stroke="#666666" stroke-dasharray="3 3" pointer-events="none"/>
<g transform="translate(-0.5 -0.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: 118px; height: 1px; padding-top: 270px; margin-left: 769px;">
<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: none; white-space: normal; overflow-wrap: normal;">
Other System
<br style="border-color: var(--border-color);"/>
e.g. Odoo
</div>
</div>
</div>
</foreignObject>
<text x="828" y="274" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
Other System...
</text>
</switch>
</g>
<g transform="translate(-0.5 -0.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: 75px; margin-left: 129px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<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;">
Web Browser or REST API
</div>
</div>
</div>
</foreignObject>
<text x="158" y="79" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
Web Browse...
</text>
</switch>
</g>
<path d="M 608 8 C 608 -2.67 668 -2.67 668 8 L 668 52 C 668 62.67 608 62.67 608 52 Z" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<path d="M 608 8 C 608 16 668 16 668 8 M 608 12 C 608 20 668 20 668 12 M 608 16 C 608 24 668 24 668 16" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="none"/>
<g transform="translate(-0.5 -0.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: 40px; margin-left: 609px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<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;">
Portal DB
</div>
</div>
</div>
</foreignObject>
<text x="638" y="44" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
Portal DB
</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: 34 KiB

View file

@ -1,5 +1,15 @@
* xref:index.adoc[Home]
* xref:terminology.adoc[]
* Web Portal
** xref:user-stories.adoc[]
* xref:web-portal.adoc[]
** xref:user-stories.adoc[]
** xref:organizations.adoc[]
** xref:authentication.adoc[]
** xref:control-planes.adoc[]
** xref:service-catalog.adoc[]
** xref:service-instances.adoc[]
** xref:api.adoc[]
** xref:database-diagram.adoc[]
* Cloud Providers
** xref:exoscale-osb.adoc[]

View file

@ -0,0 +1,7 @@
= Portal API
To allow the use of infrastructure as code tools like Terraform, Pulumi or even Crossplane, the portal exposes an API.
The API is following the RESTful API principle and makes use of the OpenAPI v3 standard.
We will either use https://django-ninja.dev/[Django Ninja^] or https://www.django-rest-framework.org/[Django REST framework^] to build the API.
Throughout the development, it's crucial to differentiate between views and business logic, so that we can reuse the same business logic for the web portal, as well as for the API.

View file

@ -0,0 +1,11 @@
= User Authentication / Login
Authentication to the portal happens via OpenID Connect, the so called VSHN Account.
This IdP is provided by Keycloak at https://id.vshn.net/[id.vshn.net].
We use https://docs.allauth.org/[Django Allauth] to properly integrate into Django.
There is no user registration, password reset or other account management functionality in the portal.
All these processes are provided by the VSHN Account Keycloak system.
A user who can authenticate with a VSHN Account automatically gets access to the portal.
If no user exists in the portal yet, it will get automatically created and linked to the VSHN Account.

View file

@ -0,0 +1,23 @@
= Service Provider Control Planes
Control Planes are Kubernetes API endpoints, reachable directly from the Web Portal. It represents a datacenter ("Zone") of a Service Provider.
A Service Provider can have multiple zones.
The portal connects to these Kubernetes API endpoints by using the official https://github.com/kubernetes-client/python[Python Kubernetes Client^].
Every control plane is registered in the portal database with connection details, names and other metadata (description, location, service provider, zone, logo, etc.).
Authentication happens via different mechanisms, depending on the task at hand:
System Connections::
Certain operations are initiated directly by the portal, for example retrieving the available service definitions (XRDs).
This is done via a dedicated Service Account token, having stringent RBAC rules on the cluster.
User Connections::
Tasks like creating, listing, updating, or deleting service instances is done in the users context. On the Kubernetes API, we take appropriate measure to secure the access.
For acting in the users context, we use:
. https://www.keycloak.org/securing-apps/token-exchange[OIDC Token Exchange] to get a token to authenticate in the users context against the control plane API.
. https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation[User impersonation] by using the system connections credentials.
Not all users and organizations have access to all control planes, therefore we implement access control on a user and organization level to control planes.
This way we filter the available service providers available.

View file

@ -0,0 +1,108 @@
= Database Diagram
[mermaid,database,png]
....
erDiagram
User ||--o{ OrganizationMember : "is member of"
OrganizationMember {
string role
}
Organization ||--o{ OrganizationMember : has
Organization }|--|| BillingEntity : "belongs to"
Organization ||--|| OrganizationOrigin : "has"
BillingEntity {
string name
string description
string erp_reference
}
OrganizationOrigin {
string name
string description
}
OrganizationOrigin ||--o{ OrganizationOriginServiceProvider : "can filter"
ServiceProvider ||--o{ OrganizationOriginServiceProvider : "is filtered by"
Service {
string name
string description
string logo
json external_links
}
Service ||--o{ ServiceCategory : "belongs to"
ServiceCategory {
string name
string description
string logo
}
ServiceCategory ||--o| ServiceCategory : "has parent"
ServiceProvider {
string name
string description
string logo
json external_links
}
ServiceProvider ||--o{ ControlPlane : "has"
ControlPlane {
string name
string description
string k8s_api_endpoint
json api_credentials
}
ServiceOffering {
string description
}
ServiceOffering ||--|| Service : "offers"
ServiceOffering ||--|| ServiceProvider : "provided by"
ServiceOffering ||--o{ ControlPlane : "deployed on"
Plan {
string name
string description
json features
json pricing
string term
}
ServiceOffering ||--o{ ServiceOfferingPlan : "has"
Plan ||--o{ ServiceOfferingPlan : "included in"
ServiceOfferingPlan {
json pricing
}
....
User and Organization Structure::
Users can be members of multiple organizations, and organizations can have multiple members. This relationship is managed through an OrganizationMember junction table that stores the specific role each user has within each organization. A user's role can differ between organizations.
Organizations::
Each organization is required to have exactly one billing entity and one organization origin. Billing entities, which contain name, description, and ERP reference information, can be shared across multiple organizations. Similarly, organization origins (containing name and description) can be associated with multiple organizations.
Organization Origins and Service Providers::
Organization origins can be linked to service providers through a filtering mechanism. This relationship allows controlling which service offerings are available to organizations based on their origin.
Services and Categories::
Services are defined with basic information (name, description, logo) and external links. Each service can belong to multiple service categories. Service categories themselves can be nested, meaning a category can have a parent category, enabling a hierarchical categorization structure. Categories include name, description, and logo attributes.
Service Providers and Control Planes::
Service providers are entities with name, description, logo, and external links. Each service provider can operate zero or more control planes. A control plane belongs to exactly one service provider and contains configuration details including name, description, Kubernetes API endpoint, and API credentials.
Service Offerings and Plans::
Service offerings connect services with service providers. Each service offering:
* Must be associated with exactly one service and one service provider
* Can be deployed on multiple control planes
* Includes multiple plans with customized pricing
Plans are reusable across different service offerings and include:
* Basic information (name, description)
* Feature specifications
* Multi-currency pricing options
* Term details
The relationship between service offerings and plans is managed through a ServiceOfferingPlan junction table, which allows the same plan to have different pricing depending on the specific service offering.

View file

@ -0,0 +1,415 @@
= Exoscale Marketplace Integration
To integrate services from the https://products.vshn.ch/appcat/services_index.html[VSHN Application Catalog^] into the https://www.exoscale.com/marketplace/[Exoscale Marketplace^], we provide an Exoscale-specific https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md[Open Service Broker (OSB) API^] endpoint to provision Organizations and enable access to the service plans. The Servala web portal is used by the end-user of Exoscale to provision and manage services.
When an Exoscale customer enables a service on the Exoscale Marketplace, access to the service will be enabled on the xref:web-portal.adoc[Servala Web Portal].
The Exoscale customer then self-service provisions and configures the service instances via the portal.
Disabling a service by an Exoscale customer will disable access to the service on Servala, and after a grace period, all instances are deleted if they haven't been deleted by the customer yet.
Except the Servala portal everything runs on Exoscale premises.
Every Exoscale zone will run at least one Worker Cluster which hosts the service instances and a xref:control-planes.adoc[Control Plane (CP)].
We regularly send usage data from https://central.vshn.ch/[VSHN Central (Odoo)^] to Exoscale for invoicing the end-user.
Every month an invoice is sent from VSHN to Exoscale for the service usage of the last month.
== Terminology
Open Service Broker API::
"The https://www.openservicebrokerapi.org/[Open Service Broker API project^] allows independent software vendors, SaaS providers and developers to easily provide backing services to workloads running on cloud native platforms."
See official https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md[API spec^].
OSB API::
Abbreviation of Open Service Broker API.
OSB Specific Terms::
Excerpt from https://github.com/openservicebrokerapi/servicebroker/blob/v2.17/spec.md#terminology[API spec Terminology^]:
* _Service Offering_: The advertisement of a Service that a Service Broker supports.
* _Service Plan_: The representation of the costs and benefits for a given variant of the Service Offering, potentially as a tier.
* _Service Instance_: An instantiation of a Service Offering and Service Plan.
Exoscale Marketplace::
The https://www.exoscale.com/marketplace/[marketplace^] of Exoscale enabling the self-service ordering of services from third-party providers.
== Architecture
[mermaid,arch,png]
....
flowchart TB
exo["Exoscale Marketplace"]
eu("Exoscale End-User")
vshn["Servala Exoscale OSB API"]
vshnccpapi["Control Plane(s)"]
vshnccpui["Servala Portal"]
exo-- OSB API -->vshn-- API --> vshnccpui
eu-- WebUI -->exo
eu-- WebUI -->vshnccpui-- K8s API --> vshnccpapi
....
The xref:web-portal.adoc[Servala Web Portal] is doing the heavy lifting, while the custom OSB API endpoint is used for Exoscale integration.
The "Enable" button in the Exoscale Marketplace Portal will:
* Provision an Organization at Servala (if it not already exists) which is tied to Exoscale (`origin=exoscale`). See <<Exoscale Organizations>>.
* Enable access to the enabled service, ready to be provisioned in the Servala Portal
This Organization will be tied to Exoscale services, it can only see services from Exoscale and only provision at Exoscale premises.
=== OSB API at Exoscale
Exoscale uses the OpenServiceBroker (OSB) API for service provisioning and deprovisioning with external vendors, that's why we're integrating this way.
The API currently https://community.exoscale.com/documentation/vendor/marketplace-managed-services-provision/#open-service-broker-api-osbapi[has the following features^]:
* Service instance provisioning
* Service instance update (plan changes, suspensions, user sync)
* Service instance deprovisioning
* Synchronous requests support
== Service Listing
Every service of the VSHN Application Catalog that is available in the Exoscale Marketplace is individually listed in the Exoscale Marketplace.
Listing in the Exoscale Marketplace is done by Exoscale themselves (they manually add them to their database).
We specifically only list services in the Servala Portal which are enabled on the Exoscale Marketplace when being in the context of an organization with origin Exoscale.
Other available services do get a hint in the Servala Portal that they need to be enabled first on the Exoscale Marketplace to get available for provisioning.
This is done due to the fact that there is no backchannel from the Servala Portal to the Exoscale Marketplace.
Would we allow to provision other services than enabled in the Exoscale Marketplace, there would be a mismatch, also from billing and legal perspectives.
=== OSB Plans and Services
There are a few https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md#service-plan-object[OSB plan^] requirements by Exoscale:
* An OSB API service must match one product on the Exoscale marketplace.
* An OSB API plan should be setup for each published plan for that product.
* An additional technical plan must be available for managing suspensions of end user organizations.
OSB services and plans, including their mapping to specific configurations are configured in the Portal database.
== Exoscale Organizations
When an Exoscale user clicks on the green "Enable" button on a service offering page, the <<Onboarding, onboarding flow>> is triggered in the Servala Portal.
image::exoscale-marketplace-empty-service.png[]
=== Onboarding
[mermaid,onboarding,png]
....
sequenceDiagram
autonumber
actor EU as Exoscale User
participant EP as Exoscale Portal
participant VCA as Servala OSB API
participant CCP as Servala Portal
EU->>EP: Enable VSHN Service
EP->>VCA: OSB API "PUT"
VCA-->>CCP: Create "Organization"<br/>(if not exist)
CCP-->>EU: Send invitation to organization<br/> via E-Mail (if new Organization)
VCA->>CCP: Enable Service Plan
CCP->>EU: Send Service Welcome Mail
VCA->>EP: OSB API Confirmation
Note over VCA,EP: see return codes below
EP->>EU: Confirmation
....
.OSB API Provisioning call from Exoscale to VSHN
[source,json]
----
PUT http://exo-osbapi.vshn.net/v2/service_instances/:instance_id
{
"service_id": "service-test-guid", <1>
"plan_id": "plan1-test-guid", <2>
"organization_guid": "org-guid-here", <3>
"space_guid": "org-guid-here", <3>
"parameters": {
"users": [ <4>
{
"email":"email",
"full_name": "full name",
"role":"owner|tech"
}
]
},
"context": {
"platform": "exoscale",
"organization_guid": "org-guid-here", <3>
"space_guid": "org-guid-here", <3>
"organization_name": "organization-name",
"organization_display_name": "organization-display-name",
}
}
----
<1> The ID of the service on VSHN side
<2> The ID of the plan on VSHN side
<3> The Exoscale organization UUID
<4> List of users
https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md#response-3[HTTP response codes^]:
* `200`: Service already enabled
* `201`: Successfully enabled service
Sources:
* https://community.exoscale.com/documentation/vendor/marketplace-managed-services-provision/#provisioning[Exoscale docs - Provisioning^]
* https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md#provisioning[OSB API Spec^]
In the Servala Portal an Organization is created by the OSB API if it doesn't exist yet.
Organization Display Name::
The display name is set to the name of the Exoscale organization
Organization Origin::
The organization origin is set to `exoscale` (hardcoded in the OSB API service)
Invitation::
When the Organization is created the first time, an invitation is sent to the user in the field `parameters.users[0].email` from the OSB API.
=== Suspension
This flow is triggered when an Exoscale organization:
* changes their current plan
* is suspended
* changes the user list on Exoscale side and user sync is turned on
The suspension uses a special "suspension" plan.
[mermaid,suspension,png]
....
sequenceDiagram
autonumber
participant EP as Exoscale Portal
participant VCA as Servala OSB API
participant CCP as Servala Portal
participant VSHNEER as VSHNeer
EP->>VCA: OSB API "PATCH"
Note over EP, VCA: Set suspension Plan
VCA->>CCP: Disable Service
CCP->>VSHNEER: Send E-Mail
VCA->>EP: OSB API Confirmation
Note over VCA,EP: see return codes below
....
[source,json]
----
PATCH http://exo-osbapi.vshn.net/v2/service_instances/:instance_id
{
"service_id": "service-test-guid",
"plan_id": "plan1-test-guid", <1>
"parameters": {
"users": [
{
"email":"email",
"full_name": "full name",
"role":"owner|tech"
}
]
}
}
----
<1> Special suspension plan, to be defined
https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md#response-5[HTTP response codes^]:
* `200`: Service is disabled
Sources:
* https://community.exoscale.com/documentation/vendor/marketplace-managed-services-provision/#service-instance-update[Exoscale docs - Service Instance Update^]
* https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md#updating-a-service-instance[OSB API Spec^]
When the suspension plan is triggered, we send an E-Mail to customers@vshn.ch with all the information we have, so that we can check back with Exoscale what to do.
No service is automatically suspended. If it has to happen, we'll do it manually.
=== Offboarding
This flow is triggered when an Exoscale organization:
* decides to unsubscribe the product
* suspension is not resolved before 7 days in trial mode, or 30 days outside of trial mode, which triggers a purge of their resources
* decides to close their Exoscale account, or their account is terminated
[mermaid.offboarding,png]
....
sequenceDiagram
autonumber
actor EU as Exoscale User
participant EP as Exoscale Portal
participant VCA as Servala OSB API
participant CCP as Servala Portal
participant CP as Control Plane
EU->>EP: Disable VSHN Service
EP->>VCA: OSB API "DELETE"
VCA->>CCP: Disable Service
CCP->>EU: Send Deletion Confirmation Mail
CCP->>VCA: Confirmation
VCA->>EP: OSB API Confirmation
Note over VCA,EP: see return codes below
EP->>EU: Confirmation
CCP->>CP: Delete service instances<br />after grace period
....
[source,json]
----
DELETE http://exo-osbapi.vshn.net/v2/service_instances/:instance_id?service_id=service-test-guid&plan_id=plan1-test-guid
----
https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md#response-10[HTTP response codes^]:
* `200`: Service disabled
Sources:
* https://community.exoscale.com/documentation/vendor/marketplace-managed-services-provision/#deprovisioning[Exoscale docs - Deprovisioning^]
* https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md#deprovisioning[OSB API Spec^]
When all services are deleted (none exists anymore), an email is sent to customer@vshn.ch for the final closure of the organization.
Also, there is a monitoring check which triggers when no service is available, but service instances are still there and the deletion grace period is over.
This means something failed in cleaning up.
See also <<Deprovisioning>>, which details the single service deprovisioning.
=== User Synchronization
We don't do https://community.exoscale.com/documentation/vendor/marketplace-managed-services-provision/#user-sync[user synchronization^] from Exoscale to VSHN.
____
When user sync is disabled, only the information of the user that made the product purchase will be provided. The information will never be updated.
____
== Instances
=== Provisioning
Instances aren't directly provisioned via the OSB API.
Instead, the service is enabled in the Servala Portal for the organization.
An E-Mail is sent with a well-crafted link to the portal to actually provision the instance.
The portal link encodes:
* The Organization GUID
* The `service_id`
* The `plan_id`
When this portal link is opened, a pre-filled service ordering form is presented in the portal, ready for the user to actually provision the service.
This flow allows an Exoscale user to have more than one instance per service per Exoscale organization.
=== Plan Change
We don't support plan changes on the Exoscale console, all service parameters are configured on our portal on the actual service provisioning.
There is only one plan per service, the default plan.
One exception is the "suspension plan" which is described in the <<Suspension, suspension flow>>.
=== Deprovisioning
See also <<Offboarding>> which talks about Organization offboarding and the OSB API flow.
TODO
We also send an E-Mail for each service instance which gets deleted that way, telling the customer that the service either has to be removed from the VSHN Portal or that it's automatically deleted after the deletion grace period.
== Billing
NOTE: This part is still in its early stages!
The basic flow: We send billing data to Exoscale, Exoscale invoices the end-user, VSHN sends an invoice to Exoscale, Exoscale pays VSHN.
[mermaid,billing,png]
....
flowchart TB
exo["Exoscale"]
exocust["Exoscale Customer"]
vshn["VSHN"]
exo-- Invoices --> exocust
exocust-- Pays -->exo
vshn-- Invoices --> exo
exo-- Pays -->vshn
....
Exoscale must keep track on our pricing on their end, because we only send usage data and they do the calculation.
TODO
* Send billing data to Exoscale billing API - Exoscale does invoicing to customer - we send invoice to Exoscale
* One SO on VSHN side for Exoscale, to send invoice to Exoscale
** We track the Exoscale organization ID in the SO
** Maybe different product in product DB? Or different variant?
* How to send billing data to Exoscale? Once per month directly from Odoo data, so that we send the same data?
From Exoscale docs:
____
You can define one or more plans corresponding to various service offerings or service levels on your platform.
*Monthly fees*
Each plan can have an optional monthly fee.
When a subscriber unsubscribes from your service, the service is cancelled immediately and they are charged with a pro-rated amount dating from their last subscription charge.
*Additional charges*
It is possible to charge for additional products and services in addition to the optional monthly fee.
All additional billing dimensions must be declared in advance with a defined price for each available plan.
Billing dimensions are specified by:
* a technical name
* a unit
Supported units are:
* h : hours
* gb : gigabytes
* gb.h : gigabytes per hour
* u : arbitrary quantity
The frequency of metering reporting is up to the vendor. You can meter as frequently as every hour.
Metering should be reported at least once a month per customer.
Metering is reported per client organization with the consumption that has occurred since the last successful report. Multiple charges can be reported at once.
When reporting usage, you send the quantity for each defined variable and the client is charged accordingly.
____
[source,json]
----
POST /orgs/:uuid/usage <1>
{
"records": [
{
"variable": "something",
"quantity": 12.5
},
{
"variable": "something_else",
"quantity": 1.2
}
]
}
----
<1> `:uuid` is the technical ID of the client organization in the Exoscale backend, which will be shared during the onboarding process.
== Resources
* https://kb.vshn.ch/appuio-cloud/references/architecture/control-api.html[APPUiO Control API Architecture^]
* https://kb.vshn.ch/appuio-cloud/references/architecture/invitations.html[APPUiO Invitations]
* https://github.com/vshn/crossplane-service-broker[Crossplane Service Broker (Code)^] - xref:how-tos/crossplane_service_broker/overview.adoc[Crossplane Service Broker (Docs)]
* https://github.com/vshn/swisscom-service-broker[Swisscom Service Broker^]
* https://community.exoscale.com/documentation/vendor/marketplace-managed-services/[Exoscale Vendor Documentation - Managed Services^]
* https://community.exoscale.com/documentation/vendor/marketplace-managed-services-billing/[Exoscale Vendor Documentation - Managed Services Billing^]
* https://community.exoscale.com/documentation/vendor/marketplace-managed-services-provision/[Exoscale Vendor Documentation - Managed Services Provisioning^]

View file

@ -0,0 +1,59 @@
= Multi-Tenancy and Organizations
The portal is fully multi-tenant aware. Everything happens in the context of an "Organization".
This context dictates multiple parameters, for example which service providers or service offerings are available to the organization.
Users of the platform must be part of one or multiple organizations and can have different access rights in different organizations.
Some parts of the portal are global:
* Organizations
* Users
Everything else happens in the context of an organization.
Therefore, organizations are a main part of the user interface and will be prominently displayed and enforced.
Users can either be manually granted access to an organization by an organization admin, or they can get an invitation which allows them to join the organization with a certain role.
Potential candidates to implement this functionality:
* https://django-organizations.readthedocs.io/[django-organizations: multi-user accounts^] (Has some https://django-organizations.readthedocs.io/en/latest/reference/backends.html[invitation^] functionality)
* https://github.com/raphaelm/django-scopes[django-scopes] (Safely separate multiple tenants in a Django database)
* https://github.com/dfunckt/django-rules[`rules`^] to realize object-level permissions
== Source of Truth of Organizations
Today, VSHN has the notion of organizations in the context of the https://kb.vshn.ch/appuio-cloud/references/architecture/control-api.html[APPUiO Control API^].
We must make sure to not duplicate this concept and be very clear about the source of truth.
Also to prepare for a potential integration of APPUiO features into the portal.
An organization in APPUiO manages:
* A user group in VSHN Account (Keycloak)
* A sales order in VSHN Central (Odoo)
* A Kubernetes namespace in the Control API
The https://github.com/appuio/appuio-cloud-agent[APPUiO Cloud Agent^] connects to the control API via the Kubernetes API to retrieve certain information, for example user group mapping.
We therefore cannot just switch the organization handling to the Portal.
Should we decide to do so, we would need to have a proper migration path.
As organizations must also be available in the portal, we must keep the APPUiO Control API and the portal database in sync: Creating, updating and deleting an organization in the Portal must do the same in the APPUiO Control API, and vice versa.
This will imply some form of synchronization mechanism, with all its downsides of data consistency.
The main source of truth is the APPUiO Control API and has precedence over the data in the portal (at least from now, that's subject to future changes).
This includes the https://kb.vshn.ch/appuio-cloud/references/architecture/control-api-billing-entity.html[BillingEntity^] as well.
== Organization Origin
For some functionality, like filtering available control planes or service plans, we need to know the origin of an organization.
As this is a feature specific to the portal, we track the origin of the organization in the portal.
Organization origins have a specific configuration, to be managed in the portal.
A default organization origin can be specified, for organizations not having a specific origin configured during creation.
== Organization Origin Configuration
The organization origin configuration specifies certain behavior:
* Which control planes and plans are available to the organization
* Default billing entity

View file

@ -0,0 +1,23 @@
= Service Catalog and Offering
The service catalog will be available publicly (without authentication), so that they can be discovered.
An authenticated user might see more or less services, depending on 1) the user rights and 2) the organization context and 3) the organization origin configuration.
Services will be registered in the portal database to make them available in the catalog.
Each service contains meta information (Description, logo, Links, etc.).
Services are made available through plans (zero, or more). A plan consists of:
* Meta information (Description, pricing, links, etc.)
* Kubernetes GVK (Group/Version/Kind)
* Control Planes offering this plan (named: "Service Provider Zone")
* Service spec per Control Plane
* Access control to the plan (who can see and access this plan? User and organization specific. Public or not.)
All Control Planes expose the service definition of the GVK (Group/Version/Kind) via Kubernetes APIs (Crossplane XRDs).
The Web Portal discovers these APIs and loads the definition from the https://kubernetes.io/docs/concepts/overview/kubernetes-api/#openapi-interface-definition[OpenAPI spec^] into the database (updated regularly).
With this OpenAPI spec, the fields of the service spec are dynamically built.
As the OpenAPI spec doesn't contain nice field names, we might want to be able to edit the service spec for the presentation in the web UI, or we add some heuristics to make them look nice.
If a plan doesn't link to a Control Plane or a service doesn't belong to a plan, the service or plan is "available on request".
This means we show a contact form for a customer to show interest.

View file

@ -0,0 +1,62 @@
= Service Instances
Working with service instances happens by directly talking to the xref:control-planes.adoc[control plane APIs].
The actions of ordering, listing, viewing, updating and deleting are directly executed on the control plane API.
No state of any service instance is stored in the database of the portal, the source of truth is in the possession of the control plane.
All actions are executed in the context of the organization and control plane tuple.
This dictates what is available and possible.
Examples of Kubernetes service resources, see https://docs.appcat.ch/[docs.appcat.ch^].
== Organization Namespace
Every organization has a dedicated Kubernetes namespace on the control plane.
This namespace is managed by the portal and created on first use (see https://docs.appuio.cloud/user/how-to/manage-projects-and-namespaces.html#_creating_namespaces[APPUiO docs on creating namespaces]).
Service instance resources live in the organizations namespace.
== Dynamic Service Spec Form
Service parametrization is subject to the capability of the API (XRD) exposed by the control plane.
The form is dynamically generated by the OpenAPI spec.
Certain standard fields of this spec are intercepted and either hidden, shown read-only or filled with default values.
These mainly concerns the Crossplane core fields:
* `spec.compositionRef`
* `spec.compositionSelector`
* `spec.compositionRevisionRef`
* `spec.compositionRevisionSelector`
* `spec.resourceRefs`
* `spec.writeConnectionSecretToRef`
We can also specify rules how to make field names look nicer (For example: `logLevel` (original) becomes `Log Level`).
And we might also want to intercept other fields (configurable).
== Create / Update / Delete
By choosing a service from the available plans (organization and control plane context), the service is parametrized and then created directly in the chosen control plane in the organizations namespace.
The control plane does its validation and admission tasks, any errors, warnings or other information returned by the control plane is surfaced to the user via the portal.
For updating a service instance, the form is pre-filled with the current values.
Deletion happens by deleting the resource in the control plane.
The portal makes sure to handle potentially available deletion protection, available on certain services (For example https://docs.appcat.ch/vshn-managed/postgresql/delete.html[PostgreSQL by VSHN^]).
== Listing and Viewing
Listing of service instances and viewing their details is done by directly querying the control plane API.
The list view shows all service instances on all control planes of the organization and depending on the access the user has.
In the detail view of a service instance, all important details are displayed, also from the `.status` sub-resource.
This view also allows access to the connection credentials which contain the details how to access the service instance.
== Other Service Features
Services might have additional features which will be incorporated into the portal for ease of use.
A non-exhaustive list:
* Listing of available backups
* Restoring from backup
* Metrics and alerts

View file

@ -0,0 +1,64 @@
= Web Portal
image::web-portal-arch.drawio.svg[]
The Servala Web Portal is the central multi-tenant multi-service-provider aggregation and self-service main entrypoint for Servala.
Servala is the brand for the product formerly known as "VSHN Application Marketplace".
It offers self-service provisioning, multi-tenancy via organizations, access control, and provides central management access over multi cloud providers and the instances running this way.
The portal is a web application consuming various third party APIs to provide an aggregated and opinionated view.
External resources to read about it:
* http://vshn.ch/marketplace[vshn.ch Website^]
* https://products.vshn.ch/marketplace/index.html[VSHN products site^]
The source code can be found on the https://servala-2nkgm.app.codey.ch/servala/servala-portal[Servala Codey instance^].
== Technology Stack
We choose:
* Python https://docs.djangoproject.com/en/dev/internals/release-process/#term-Long-term-support-release[Django LTS^]
* PostgreSQL as database backend
* https://gunicorn.org/[Gunicorn^] Python WSGI HTTP Server
* https://caddyserver.com/[Caddy^] for serving static files and WSGI, or https://whitenoise.readthedocs.io/en/latest/[WhiteNoise^] (TBD)
* https://docs.astral.sh/uv/[Astral uv^] for Python project, dependency and build management
* https://htmx.org/[htmx] for the dynamic part in the frontend
* https://getbootstrap.com/[Bootstrap 5] for styling the frontend
A complete reasoning for this stack is available in https://vshnwiki.atlassian.net/wiki/spaces/VSHNPM/pages/402718747/Self-Service+Marketplace+Web+Application[our wiki^] (internal page).
== Development Paradigms
Keep usage of third-party dependencies low::
Every external dependency adds a burden on the maintenance of the application.
Adding a dependency must be done with care:
* Is the dependency well maintained and adopted in the ecosystem?
* Could we do it without the dependency? If not, why?
* What happens if the dependency is abandoned?
* Document the reason for each dependency, why it has been chosen and why we can't live without it.
Graceful degradation::
The application will connect to various upstream APIs which we can't control.
Should issues arise with one of these APIs, gracefully degrade the feature set and inform accordingly ("This service is currently not available - we're working on it").
Never must the application crash because and upstream API not being reachable, has slow response time or react in an undefined way.
Use database and caches wisely::
External systems like databases, caches or queues add additional complexity and burden to the application operations.
Every additional system must be chosen carefully and the reasoning documented.
Alternatives must be considered and documented.
Business logic vs. views::
Whenever possible, split business logic from views.
This allows to progress the application in the future to allow for different views (For example APIs or other alternative frontends).
Django specifics::
* We use class based views by default, exceptions can be made
* Dynamic configuration happens via environment variables
* Different environments (dev / test / prod) must be clearly separated inside the application
Testing::
Business functionality must be https://docs.djangoproject.com/en/5.1/topics/testing/[tested^] with code.
We preferrably use https://docs.pytest.org/[pytest^].