diff --git a/index.ts b/index.ts index bb15ec39..ddd44274 100644 --- a/index.ts +++ b/index.ts @@ -50,13 +50,14 @@ function findPlatforms(): Platform[] { const platformDir: string = path.join(repoRoot, dir.name); const platformLogo = getPlatformLogoOrThrow(platformDir, dir.name); const platformReadme = getPlatformReadmeOrThrow(platformDir); - const { name, description, content } = extractReadmeFrontMatter(platformReadme); + const { name, description, category, content } = extractReadmeFrontMatter(platformReadme); const terraformSnippet = getTerraformSnippet(platformDir); return { platformType: dir.name, name, description, + category, logo: platformLogo, readme: content, terraformSnippet @@ -81,11 +82,11 @@ function getPlatformReadmeOrThrow(platformDir: string) { try { return fs.readFileSync(path.join(platformDir, "README.md"), "utf-8"); } catch { - throw new Error('Platform README.md not found. Each platform should have a README.md file.'); + throw new Error(`Platform README.md not found for ${platformDir}. Each platform should have a README.md file.`); } } -function extractReadmeFrontMatter(platformReadme: string): { name: string; description: string; content: string } { +function extractReadmeFrontMatter(platformReadme: string): { name: string; description: string; category?: string; content: string } { const { data, content } = matter(platformReadme); const name = data.name; @@ -98,10 +99,13 @@ function extractReadmeFrontMatter(platformReadme: string): { name: string; descr throw new Error('Property "description" is missing in the front matter of the platform README.md. Each platform README.md should have a description defined in the front matter.'); } + const category = data.category; + return { name, description, - content + content, + category } } @@ -169,12 +173,16 @@ function parseReadme(filePath) { ? getBuildingBlockFolderUrl(backplaneDir) : null; + const terraformSnippetDir = path.join(buildingBlockDir, ".."); + const terraformSnippet = getTerraformSnippet(terraformSnippetDir); + return { id, platformType: platform, logo: buildingBlockLogoPath, buildingBlockUrl, backplaneUrl, + terraformSnippet, ...data, howToUse: extractSection(/## How to Use([\s\S]*?)(##|$)/), resources: parseTable(body.match(/## Resources([\s\S]*)/)), @@ -224,5 +232,6 @@ export interface Platform { description: string; logo: string; readme: string; + category?: string; terraformSnippet?: string; } diff --git a/modules/azure/storage-account/meshstack_integration.tf b/modules/azure/storage-account/meshstack_integration.tf new file mode 100644 index 00000000..0c735d39 --- /dev/null +++ b/modules/azure/storage-account/meshstack_integration.tf @@ -0,0 +1,66 @@ +# TODO: this is actual not a correct file but just acts as an example for now + +locals { + name = "azure-storage-account" + scope = "/subscriptions/00000000-0000-0000-0000-000000000000" + existing_principal_ids = [ + "00000000-0000-0000-0000-000000000000" + ] + service_principal_name = "storage-account-deployer" + + workspace_identifier = "my-workspace" +} + +provider "meshstack" { + # Configure meshStack API credentials here or use environment variables. + # endpoint = "https://api.my.meshstack.io" + # apikey = "00000000-0000-0000-0000-000000000000" + # apisecret = "uFOu4OjbE4JiewPxezDuemSP3DUrCYmw" +} + +provider "azurerm" { + features {} +} + +# Import the backplane module to get IAM and other required outputs +module "backplane" { + source = "./backplane" + name = local.name + scope = local.scope + existing_principal_ids = local.existing_principal_ids + create_service_principal_name = local.service_principal_name + workload_identity_federation = {} # TODO this should come from data_source +} + +# Import the building block definition into meshStack +# NOTE: meshstack_buildingblock_definition is a placeholder for demonstration. Replace with the actual resource if available. +resource "meshstack_buildingblock_definition" "storage_account" { + metadata = { + name = "azure-storage-account" + owned_by_workspace = local.workspace_identifier + } + + spec = { + display_name = "Azure Storage Account" + description = "Provision Azure Storage Accounts with encryption and access control" + + supported_platforms = ["azure"] + + source = { + git = { + url = "https://github.com/meshcloud/meshstack-hub.git" + ref = "main" + path = "modules/azure/storage-account/buildingblock" + } + } + + implementation_type = "Terraform" + # Pass IAM outputs as inputs if required by your building block + role_definition_id = module.backplane.role_definition_id + role_assignment_ids = module.backplane.role_assignment_ids + principal_ids = module.backplane.role_assignment_principal_ids + service_principal = module.backplane.created_service_principal + application = module.backplane.created_application + scope = module.backplane.scope + } +} diff --git a/modules/meshstack/README.md b/modules/meshstack/README.md new file mode 100644 index 00000000..8a67a3c1 --- /dev/null +++ b/modules/meshstack/README.md @@ -0,0 +1,6 @@ +--- +name: meshStack +description: meshStack is a cloud management platform that provides a unified interface for managing and governing cloud environments +--- + + diff --git a/website/src/app/core/template.ts b/website/src/app/core/template.ts index 91d7ace8..38b47a3c 100644 --- a/website/src/app/core/template.ts +++ b/website/src/app/core/template.ts @@ -8,4 +8,5 @@ export interface Template { buildingBlockUrl: string; backplaneUrl: string | null; supportedPlatforms: string[]; + terraformSnippet?: string; } diff --git a/website/src/app/features/template-details/template-details.component.html b/website/src/app/features/template-details/template-details.component.html index c3b88bb6..743d9520 100644 --- a/website/src/app/features/template-details/template-details.component.html +++ b/website/src/app/features/template-details/template-details.component.html @@ -1,138 +1,203 @@ -
-
- - +
+ +
+
-
- -
+
+
+
-
- -
- -
-
-
-
- - - Unknown Logo - -
-
-
-

{{ template.name }}

-

{{ template.description }}

-
+ +
+
+
+
+ + + Unknown Logo + +
+
+
+
+

{{ template.name }}

+

{{ template.description }}

- - - + + +
+ + + This building block has a + backplane + that prepares all required resources in the cloud platform. + +
+ + +
+
+

+ + Prefer UI over code? You can also import this building block via the meshStack UI instead of using Terraform. +

+
+
+
+
+
- -
+ +
+ +
+
+ +

- Quick Start + Setup with OpenTofu/Terraform

- +

+ Use this Terraform configuration to create the building block definition directly in meshStack. +

- -
-
- 1 -
+
+
1
-

Add to your meshStack

-

Click the button to import this building block directly into your meshStack instance.

+
Add the OpenTofu snippet
+
Copy the provided OpenTofu code from the right and add it to your infrastructure codebase.
- - -
-
- 2 -
+
+
2
-

Set up infrastructure (optional)

-

- Run the - - backplane Terraform files - - to prepare your cloud environment. -

+
Configure variables
+
+ Update the required variables in the locals block and configure your meshStack provider credentials. +
- - -
-
- {{ template.backplaneUrl ? '3' : '2' }} +
+
3
+
+
Deploy with OpenTofu
+
Run tofu init, tofu plan, and tofu apply to create the building block definition.
+
+
+
-

Configure and deploy

-

Fill in required inputs and secrets, then deploy to your platforms.

+
Success!
+
The building block definition is now available in your meshStack.
- - -
-

- - Prefer manual setup? -

-

- Copy the Terraform files from the repository above into your own repo, then create a building block definition in meshStack pointing to your repository. -

-
+ +
+ +
{{ template.terraformSnippet }}
+
+
- -
- -
-
-
- meshStack + + +
+ + +
+
+

+ + Import via UI +

+

Quick and easy setup directly from meshStack

+
+ +
+
+
1
+
+
Click the "Add to meshStack" button
+
This will open the import wizard to guide you through the setup process.
+
+
+
+
2
+
+
Run through the wizard in your meshStack
+
Follow the import wizard in your meshStack to set up the building block definition.
+
+
+
+
3
+
+
+ Optional + Apply the backplane Terraform files +
+
+ Run the + backplane Terraform files + to prepare all necessary resources in your cloud platform. +
+
+
+
+
+
+
Success!
+
The building block definition is now available in your meshStack and ready to use.
+
-

Ready to deploy?

-

Import this building block into your meshStack instance

- -
+
+ + + +
+
+

+ + Manual Setup +

+

For users who prefer to own the Terraform code

+
+ +
+
+
1
+
+
Copy the Terraform files from the repository
+
Clone or download the building block code from GitHub and add it to your own repository.
+
+
+
+
2
+
+
Create building block definition
+
In meshStack, create a new building block definition pointing to your repository location.
+
+
+
+
+
- +
+
+
+ + +
+
+ +

Building Block Not Found

+

This building block does not exist or has been removed.

+ + Back to Home +
diff --git a/website/src/app/features/template-details/template-details.component.ts b/website/src/app/features/template-details/template-details.component.ts index 3271e0bf..df7695b7 100644 --- a/website/src/app/features/template-details/template-details.component.ts +++ b/website/src/app/features/template-details/template-details.component.ts @@ -1,16 +1,20 @@ import { CommonModule } from '@angular/common'; import { Component, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, RouterLink } from '@angular/router'; import { Dialog } from '@angular/cdk/dialog'; -import { Observable, Subscription, map, switchMap } from 'rxjs'; +import { Observable, Subscription, map, switchMap, of } from 'rxjs'; import { BreadCrumbService } from 'app/shared/breadcrumb/bread-crumb.service'; import { BreadcrumbItem } from 'app/shared/breadcrumb/breadcrumb'; import { BreadcrumbComponent } from 'app/shared/breadcrumb/breadcrumb.component'; +import { CardComponent } from 'app/shared/card'; import { TemplateService } from 'app/shared/template'; +import { extractLogoColor } from 'app/shared/util/logo-color.util'; import { ImportDialogComponent } from './import-dialog/import-dialog.component'; +const DEFAULT_HEADER_BG_COLOR = 'rgba(203,213,225,0.3)'; + interface TemplateDetailsVm { imageUrl: string | null; name: string; @@ -19,11 +23,12 @@ interface TemplateDetailsVm { howToUse: string; source: string; backplaneUrl: string | null; + terraformSnippet?: string; } @Component({ selector: 'mst-template-details', - imports: [CommonModule, BreadcrumbComponent], + imports: [CommonModule, BreadcrumbComponent, CardComponent, RouterLink], templateUrl: './template-details.component.html', styleUrl: './template-details.component.scss', standalone: true @@ -37,6 +42,10 @@ export class TemplateDetailsComponent implements OnInit, OnDestroy { public copyLabel = 'Copy'; + public copiedTerraform = false; + + public headerBgColor$!: Observable; + private routeSubscription!: Subscription; constructor( @@ -54,6 +63,17 @@ export class TemplateDetailsComponent implements OnInit, OnDestroy { return secondLastBreadcrumb ? secondLastBreadcrumb : '/'; })); + + // Reactive header background color + this.headerBgColor$ = this.template$.pipe( + switchMap(template => + template && template.imageUrl + ? extractLogoColor(template.imageUrl).pipe( + map(color => color || DEFAULT_HEADER_BG_COLOR) + ) + : of(DEFAULT_HEADER_BG_COLOR) + ) + ); } public ngOnDestroy(): void { @@ -65,6 +85,16 @@ export class TemplateDetailsComponent implements OnInit, OnDestroy { .then(() => this.updateCopyLabel()); } + public copyTerraform(value: string): void { + navigator.clipboard.writeText(value) + .then(() => { + this.copiedTerraform = true; + setTimeout(() => { + this.copiedTerraform = false; + }, 2000); + }); + } + public open(template: TemplateDetailsVm): void { const modulePath = this.extractModulePath(template.source); @@ -102,7 +132,8 @@ export class TemplateDetailsComponent implements OnInit, OnDestroy { ...template, imageUrl: template.logo, source: template.buildingBlockUrl, - howToUse: template.howToUse + howToUse: template.howToUse, + terraformSnippet: template.terraformSnippet })) ); }); diff --git a/website/src/app/features/template-gallery/platform-cards/platform-cards.component.html b/website/src/app/features/template-gallery/platform-cards/platform-cards.component.html index 0d97c8b4..d61ab514 100644 --- a/website/src/app/features/template-gallery/platform-cards/platform-cards.component.html +++ b/website/src/app/features/template-gallery/platform-cards/platform-cards.component.html @@ -1,21 +1,21 @@
-
+
-
+
-
-
+
+ class="w-5 h-5 object-contain" /> - Unknown Logo + Unknown Logo
@@ -24,7 +24,7 @@
-

+

{{ card.description }}

diff --git a/website/src/app/features/template-gallery/platform-cards/platform-cards.component.ts b/website/src/app/features/template-gallery/platform-cards/platform-cards.component.ts index 06666cbe..be0831cd 100644 --- a/website/src/app/features/template-gallery/platform-cards/platform-cards.component.ts +++ b/website/src/app/features/template-gallery/platform-cards/platform-cards.component.ts @@ -15,6 +15,25 @@ export class PlatformCardsComponent { @Input() public cards!: PlatformCard[]; + /** + * Define your custom order here. Use the property that uniquely identifies the platform (e.g., title or id). + * Example: ['Azure', 'AWS', 'GCP'] + */ + public customOrder: string[] = ['Microsoft Azure', 'Amazon Web Services', 'Google Cloud Platform', 'Azure Kubernetes Service', 'STACKIT']; // <-- customize as needed + + /** + * Returns the cards sorted by customOrder, with others following in original order. + */ + public get sortedCards(): PlatformCard[] { + console.log(this.cards); + if (!this.cards) return []; + const order = this.customOrder; + return [ + ...this.cards.filter(card => order.includes(card.title)).sort((a, b) => order.indexOf(a.title) - order.indexOf(b.title)), + ...this.cards.filter(card => !order.includes(card.title)) + ]; + } + public logoBackgroundColors: { [key: string]: string } = {}; public onBackgroundColorExtracted(cardTitle: string, color: string): void {