diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a89f2c76..f7ded3e2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -36,6 +36,30 @@ modules/// └── meshstack_integration.tf # Example wiring into a meshStack instance ``` +### `meshstack_integration.tf` as a single-file module + +Each `meshstack_integration.tf` is a **self-contained single-file Terraform module**. It must +include `variable`, `output`, `terraform {}`, `locals`, and `resource` blocks all in one file — +do **not** create separate `variables.tf`, `outputs.tf`, or `versions.tf` alongside it. +This keeps the integration compact and easy to call as a sub-module from composition modules. + +### Building block logos / symbols + +Use `provider::meshstack::load_image_file()` (requires meshstack provider `>= 0.19.3`) to set +the `symbol` attribute on `meshstack_building_block_definition` resources. Reference the +`logo.png` already present in the `buildingblock/` directory: + +```hcl +resource "meshstack_building_block_definition" "example" { + spec = { + symbol = provider::meshstack::load_image_file("${path.module}/buildingblock/logo.png") + # ... + } +} +``` + +This keeps logo management inside the hub — callers don't need to provide their own assets. + --- ## `meshstack_integration.tf` Conventions @@ -103,6 +127,32 @@ module "backplane" { } ``` +### Cross-module references (composition modules) + +When a `meshstack_integration.tf` needs to call **another hub module** (e.g. the AKS starterkit +calling `modules/github/repository`), use a **git URL** with a **hardcoded ref** — never a +relative `../../` path. Relative upward traversal breaks when the module is sourced via a git URL +from LCF/ICF/meshkube, because Terraform only downloads the specified subdirectory. + +```hcl +# ✅ Cross-module reference — git URL with hardcoded ref +module "github_repo_bbd" { + source = "github.com/meshcloud/meshstack-hub//modules/github/repository?ref=main" + # ... +} +``` + +**Rules:** +- `./backplane` → relative path (same module subtree, always works) +- `../../other/module` → **git URL** with hardcoded `?ref=` (cross-module) +- `var.hub.git_ref` → used only in `implementation.terraform.ref_name` of BBD resources, NOT + in `source` arguments (Terraform does not support variable interpolation in `source`) + +**Ref management:** The hardcoded refs in git URL `source` lines default to `"main"` during +development. A Go CI tool (planned) will walk the hub module dependency tree bottom-up and +update these refs to specific tags on release. Leaf modules (no cross-module deps) are updated +first, then composition modules. + ### ❌ Avoid these patterns ```hcl @@ -115,11 +165,16 @@ locals { # ❌ provider blocks inside mesh_integration.tf — the Hub UI renders these provider "meshstack" { ... } -# ❌ absolute GitHub source URL in module block — use relative path instead +# ❌ absolute GitHub source URL for backplane — use relative path instead module "backplane" { source = "github.com/meshcloud/meshstack-hub//modules/aws/s3_bucket/backplane?ref=main" } +# ❌ relative path for cross-module refs — breaks when sourced via git URL +module "github_repo_bbd" { + source = "../../github/repository" +} + # ❌ standalone meshstack_hub_git_ref variable — use variable "hub" { type = object({git_ref=string}) } instead variable "meshstack_hub_git_ref" { ... } diff --git a/.github/workflows/update-module-refs.yml b/.github/workflows/update-module-refs.yml new file mode 100644 index 00000000..1f03de28 --- /dev/null +++ b/.github/workflows/update-module-refs.yml @@ -0,0 +1,51 @@ +name: update-module-refs + +on: + push: + branches: [main] + paths: + - 'modules/**' + pull_request: + branches: [main] + paths: + - 'modules/**' + +permissions: + contents: write + +jobs: + update-module-refs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # Full history needed for git log to resolve commit SHAs + fetch-depth: 0 + # On push to main, use a token that can push commits + token: ${{ github.event_name == 'push' && secrets.GITHUB_TOKEN || github.token }} + + - uses: actions/setup-go@v5 + with: + go-version-file: tools/update-module-refs/go.mod + cache-dependency-path: tools/update-module-refs/go.sum + + - name: Build + working-directory: tools/update-module-refs + run: go build -o update-module-refs . + + - name: Update module refs (dry-run) + if: github.event_name == 'pull_request' + working-directory: modules + run: ../tools/update-module-refs/update-module-refs -dry-run + + - name: Update module refs + if: github.event_name == 'push' + working-directory: modules + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + ../tools/update-module-refs/update-module-refs + + - name: Push ref updates + if: github.event_name == 'push' + run: git push diff --git a/.gitignore b/.gitignore index 2088652d..692d4427 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,7 @@ website/public/assets/building-block-logos/ website/public/assets/logos/ website/public/assets/*.json .worktrees/ + +# Go +.nix-go/ +tools/update-module-refs/update-module-refs diff --git a/aks-starterkit-reusability-plan.md b/aks-starterkit-reusability-plan.md new file mode 100644 index 00000000..92b695cb --- /dev/null +++ b/aks-starterkit-reusability-plan.md @@ -0,0 +1,332 @@ +# Plan: Make Hub AKS Starterkit Reusable for LCF and Meshkube + +## Problem + +The hub's `modules/aks/starterkit/meshstack_integration.tf` cannot be reused by either: +- **meshkube** — has 3 diverged inline BBD files with hardcoded locals, missing `ref_name` +- **LCF** — missing the `meshstack_building_block_definition` resource entirely (TODO in `kit/aks/meshplatform/main.tf`) + +Additionally, `modules/aks/meshstack_integration.tf` (platform-level) is not callable as a module: +uses hardcoded `locals`, has `provider` blocks, no outputs. Both meshkube and LCF duplicate the +platform+landingzone+meshplatform setup independently. + +Root causes: +1. Hub integration uses hardcoded `locals`, undefined `local.image_data_uri`, hardcoded commit SHA for `ref_name` +2. `modules/github/repository/` and `modules/aks/github-connector/` have no `meshstack_integration.tf` — their BBDs are hardcoded inline in meshkube only +3. `modules/azure/postgresql/` also has no `meshstack_integration.tf` (needed for LCF) +4. `modules/aks/meshstack_integration.tf` not reusable as a module — violates hub conventions + +## Architecture + +### Layer 1: AKS Platform Registration (`modules/aks/`) + +Refactor `modules/aks/meshstack_integration.tf` into a callable Terraform module that: +1. Calls `meshcloud/meshplatform/aks` (HashiCorp registry) — creates replicator SP + k8s service accounts +2. Creates `meshstack_platform.aks` — wires SP credentials and tokens into platform config +3. Creates `meshstack_landingzone.aks_default` — standard AKS role mappings + +All three codebases currently use `meshcloud/meshplatform/aks` independently: +- Hub: `source = "meshcloud/meshplatform/aks"` version `~> 0.2.0` +- Meshkube: `source = "meshcloud/meshplatform/aks"` version `0.2.0` +- LCF: `source = "git::https://github.com/meshcloud/terraform-aks-meshplatform.git?ref=88fc6ed..."` + +The meshplatform outputs (`replicator_service_principal`, `replicator_token`, `metering_token`) +are only consumed internally to configure the `meshstack_platform` resource — no caller needs +them as external outputs. This makes the module fully self-contained. + +#### Variable layout (`modules/aks/`) + +Two variables cleanly separate infrastructure from meshStack registration: + +```hcl +variable "aks" { + description = "AKS cluster infrastructure and service principal configuration." + type = object({ + # Cluster connection + base_url = string + subscription_id = string + cluster_name = string + resource_group = string + + # meshcloud/meshplatform/aks module config + service_principal_name = string + namespace = optional(string, "meshcloud") + create_password = optional(bool, false) + workload_identity_federation = optional(object({ issuer = string, access_subject = string })) + replicator_enabled = optional(bool, true) + replicator_additional_rules = optional(list(object({ + api_groups = list(string), resources = list(string), verbs = list(string), + resource_names = optional(list(string)), non_resource_urls = optional(list(string)) + })), []) + existing_clusterrole_name_replicator = optional(string, "") + kubernetes_name_suffix_replicator = optional(string, "") + metering_enabled = optional(bool, true) + metering_additional_rules = optional(list(object({ + api_groups = list(string), resources = list(string), verbs = list(string), + resource_names = optional(list(string)), non_resource_urls = optional(list(string)) + })), []) + }) +} + +variable "meshstack_platform" { + description = "meshStack platform and landing zone registration." + type = object({ + owning_workspace_identifier = string + platform_identifier = string + location_identifier = optional(string, "global") + + # Display + display_name = optional(string, "AKS Namespace") + description = optional(string, "Azure Kubernetes Service (AKS). Create a k8s namespace in our AKS cluster.") + + # Replication behavior + disable_ssl_validation = optional(bool, true) + group_name_pattern = optional(string, "aks-#{workspaceIdentifier}.#{projectIdentifier}-#{platformGroupAlias}") + namespace_name_pattern = optional(string, "#{workspaceIdentifier}-#{projectIdentifier}") + user_lookup_strategy = optional(string, "UserByMailLookupStrategy") + send_azure_invitation_mail = optional(bool, false) + + # Landing zone + landing_zone = optional(object({ + name = optional(string) # defaults to "${platform_identifier}-default" + display_name = optional(string, "AKS Default") + description = optional(string, "Default AKS landing zone") + automate_deletion_approval = optional(bool, true) + automate_deletion_replication = optional(bool, true) + kubernetes_role_mappings = optional(list(object({ + platform_roles = list(string) + project_role_ref = object({ name = string }) + })), [ + { platform_roles = ["admin"], project_role_ref = { name = "admin" } }, + { platform_roles = ["edit"], project_role_ref = { name = "user" } }, + { platform_roles = ["view"], project_role_ref = { name = "reader" } } + ]) + }), {}) + }) +} +``` + +#### Outputs (`modules/aks/`) + +```hcl +output "aks" { + description = "AKS platform identifiers for use as var.aks in the starterkit." + value = { + full_platform_identifier = "${meshstack_platform.aks.metadata.name}.${var.meshstack_platform.location_identifier}" + landing_zone_dev_identifier = meshstack_landingzone.aks_default.metadata.name + landing_zone_prod_identifier = meshstack_landingzone.aks_default.metadata.name + } +} +``` + +Callers with separate dev/prod LZs can ignore this output and construct `var.aks` manually. + +#### Caller example — meshkube + +```hcl +module "aks_platform" { + source = "github.com/meshcloud/meshstack-hub//modules/aks?ref=${var.hub.git_ref}" + + aks = { + base_url = "https://dev-oug61sf3.hcp.germanywestcentral.azmk8s.io:443" + subscription_id = "7490f509-..." + cluster_name = "aks" + resource_group = "aks-rg" + service_principal_name = "aks_replicator.${var.domain}" + existing_clusterrole_name_replicator = "meshfed-service" + kubernetes_name_suffix_replicator = var.name + namespace = var.name + metering_enabled = false + workload_identity_federation = { issuer = var.workload_identity_issuer, access_subject = "..." } + } + + meshstack_platform = { + owning_workspace_identifier = meshstack_workspace.meshcloud.metadata.name + platform_identifier = "aks-ns" + group_name_pattern = "aks-${var.name}-#{workspaceIdentifier}.#{projectIdentifier}-#{platformGroupAlias}" + namespace_name_pattern = "${var.name}-#{workspaceIdentifier}-#{projectIdentifier}" + } +} + +# Then pass to starterkit: +module "aks_starterkit" { + source = "github.com/meshcloud/meshstack-hub//modules/aks/starterkit?ref=${var.hub.git_ref}" + hub = var.hub + meshstack = { owning_workspace_identifier = meshstack_workspace.meshcloud.metadata.name } + aks = module.aks_platform.aks # direct pass-through + github = var.github +} +``` + +### Layer 2: Constituent Building Block Definitions + +Each constituent hub module gets its own `meshstack_integration.tf` (registering ONE BBD, following hub conventions). The starterkit `meshstack_integration.tf` calls them as Terraform module blocks (using relative paths — so they naturally track the same git ref as the hub checkout) and creates only the starterkit composition BBD itself. + +``` +modules/github/repository/ + buildingblock/ # existing + meshstack_integration.tf # NEW — single-file module: variables + terraform{} + resource + outputs + +modules/aks/github-connector/ + backplane/, buildingblock/ # existing (stays in place) + meshstack_integration.tf # NEW — single-file module + +modules/azure/postgresql/ + backplane/, buildingblock/ # existing + meshstack_integration.tf # NEW — single-file module +``` + +### Layer 3: Starterkit Composition (`modules/aks/starterkit/`) + +``` +modules/aks/starterkit/ + backplane/ # UNCHANGED (stays as-is: README.md + permissions.png) + buildingblock/ # existing + meshstack_integration.tf # REWRITTEN: single-file module with 3 sub-module calls + 1 BBD (composition) +``` + +#### `meshstack_integration.tf` pattern: +```hcl +module "github_repo_bbd" { + source = "../../github/repository" # relative path → same git ref as hub checkout + hub = var.hub + meshstack = var.meshstack + github = { org = var.github.org, app_id = ..., ... } +} + +module "github_connector_bbd" { + source = "../github-connector" + hub = var.hub + meshstack = var.meshstack + github = var.github +} + +module "postgresql_bbd" { + count = var.postgresql != null ? 1 : 0 + source = "../../azure/postgresql" + hub = var.hub + meshstack = var.meshstack +} + +resource "meshstack_building_block_definition" "aks_starterkit" { + # THE ONLY BBD resource — the composition + version_spec = { + implementation.terraform.ref_name = var.hub.git_ref + inputs = { + github_repo_definition_version_uuid = { + argument = jsonencode(module.github_repo_bbd.bbd_version_uuid) + assignment_type = "STATIC" + ... + } + # etc. + } + } +} +``` + +**Note on module sources**: Terraform `source` cannot interpolate variables. Using relative paths is correct: when a caller sources the hub at a specific git ref (e.g., `github.com/meshcloud/meshstack-hub//modules/aks/starterkit?ref=v1.2.3`), the relative sub-module paths resolve within the same checkout. The `ref_name` field in `meshstack_building_block_definition` uses `var.hub.git_ref` to tell meshStack which ref to run buildingblocks from. + +#### Variable Structure (starterkit integration) + +```hcl +variable "hub" { + type = object({ git_ref = string }) + default = { git_ref = "main" } + description = "Hub release reference. Set git_ref to a tag (e.g. 'v1.2.3') or branch." +} + +variable "meshstack" { + type = object({ owning_workspace_identifier = string }) + description = "Shared meshStack context passed down from the IaC runtime." +} + +variable "aks" { + description = "AKS platform identifiers. Can be passed from module.aks_platform.aks output." + type = object({ + full_platform_identifier = string + landing_zone_dev_identifier = string + landing_zone_prod_identifier = string + }) +} + +variable "github" { + type = object({ + org = string + app_id = string + app_installation_id = string + app_pem_file = string + connector_config_tf_base64 = string + }) + sensitive = true +} + +variable "postgresql" { + description = "When non-null, registers the azure/postgresql BBD. Omit/null for meshkube." + type = object({}) + default = null +} +``` + +## Todos + +### Hub — refactor platform-level integration + +| ID | Description | +|----|-------------| +| `hub-refactor-aks-platform` | Refactor `modules/aks/meshstack_integration.tf` into a callable module. Remove provider blocks, convert locals to `variable "aks"` (infrastructure) and `variable "meshstack_platform"` (registration). Add `variables.tf` + `versions.tf` + `outputs.tf`. Integrate `meshcloud/meshplatform/aks` call. Output `aks` object matching `var.aks` shape of starterkit. | + +### Hub — new integration files for constituent modules + +| ID | Description | +|----|-------------| +| `hub-github-repo-integration` | Create `modules/github/repository/meshstack_integration.tf` + supporting files. Registers github/repository BBD with `ref_name = var.hub.git_ref`. | +| `hub-github-connector-integration` | Create `modules/aks/github-connector/meshstack_integration.tf` + supporting files. Registers github-connector BBD. | +| `hub-postgresql-integration` | Create `modules/azure/postgresql/meshstack_integration.tf` + supporting files. Registers azure/postgresql BBD. | + +### Hub — starterkit integration rewrite + +| ID | Depends on | +|----|------------| +| `hub-starterkit-variables` | — | +| `hub-rewrite-integration` | all 3 integration modules + hub-starterkit-variables | +| `hub-starterkit-outputs` | hub-rewrite-integration | +| `hub-starterkit-versions` | hub-rewrite-integration | + +### Meshkube + +| ID | Description | Depends on | +|----|-------------|-----------| +| `meshkube-add-hub-var` | Add `variable "hub"` to meshkube `variables.tf`. | — | +| `meshkube-refactor` | Remove `bbd_aks_starterkit.tf`, `bbd_github_repo.tf`, `bbd_github_actions_connector.tf`. Replace `platform_integration.tf` contents with `module "aks_platform"` call (sourcing hub `modules/aks/`) + `module "aks_starterkit"` call (sourcing hub `modules/aks/starterkit/`). Map `var.github` to new shape. Add `moved` blocks for state migration. | hub-refactor-aks-platform, hub-rewrite-integration, meshkube-add-hub-var | + +### LCF + +| ID | Description | Depends on | +|----|-------------|-----------| +| `lcf-add-starterkit-integration` | Create `kit/aks/buildingblocks/aks-starterkit-composition/meshstack_integration.tf` + `variables.tf` sourcing hub module with `postgresql` enabled. Add `terragrunt.hcl` in `foundations/likvid-prod/platforms/aks/buildingblocks/aks-starterkit/`. | hub-rewrite-integration | +| `lcf-add-azure-postgres-backplane` | Add `foundations/.../platforms/aks/buildingblocks/azure-postgresql/backplane/terragrunt.hcl` sourcing hub `modules/azure/postgresql/backplane`. | lcf-add-starterkit-integration | + +### Single-file module convention + +Each `meshstack_integration.tf` is a self-contained single-file Terraform module. Variables, +`terraform {}`, locals, resources, and outputs all in one file — no separate `variables.tf`, +`outputs.tf`, or `versions.tf`. This was chosen for simplicity since these are integration +wiring modules, not complex infrastructure. + +### Logos / symbols + +Each BBD resource sets `symbol = provider::meshstack::load_image_file("${path.module}/buildingblock/logo.png")`. +This uses the `meshcloud/meshstack` provider function (requires `>= 0.19.3`) to load the +`logo.png` already present in each buildingblock directory. Callers no longer need to manage +logo assets separately — they come from the hub. + +## Notes + +- `modules/aks/github-connector/` stays in its current location (already part of hub, no move needed). +- The starterkit backplane stays as-is (README.md + permissions.png). The `meshstack_landingzone` is a platform-level concern — it already exists in `modules/aks/meshstack_integration.tf` and in meshkube's `platform_integration.tf`. The starterkit takes LZ identifiers as inputs. +- `modules/azure/aks/` (AKS cluster provisioning building block) is orthogonal to the starterkit — no changes needed for this work. +- meshplatform outputs (`replicator_service_principal`, `replicator_token`, `metering_token`) are consumed internally by `meshstack_platform` config — no caller needs them as external outputs. +- Meshkube state migration: `moved` blocks needed for `meshstack_platform.aks`, `meshstack_landingzone.aks`, `meshstack_building_block_definition.aks_starterkit[0]`, `github_repo[0]`, `github_actions_connector[0]` → new `module.aks_platform.*` / `module.aks_starterkit.*` addresses. +- LCF's `kit/aks/buildingblocks/aks-starterkit-composition/starterkit/` copy with hardcoded UUIDs becomes obsolete — removal is a follow-up, out of scope. +- LCF's `kit/aks/meshplatform/` currently only calls the meshplatform terraform module. It could be replaced with the hub's `modules/aks/` module to also manage the `meshstack_platform` and `meshstack_landingzone` resources via IaC — this is a follow-up opportunity. +- Provider blocks must NOT appear in any `meshstack_integration.tf` per hub conventions. Callers must configure `meshstack`, `azuread`, `azurerm`, `kubernetes` providers themselves. diff --git a/flake.nix b/flake.nix index 2b23830e..256d9a65 100644 --- a/flake.nix +++ b/flake.nix @@ -44,6 +44,15 @@ pre-commit ]; + # Go toolchain for tools/ development + go_packages = pkgs: + with pkgs; + [ + go + golangci-lint + go-task + ]; + importNixpkgs = system: import nixpkgs { inherit system; }; defaultShellForSystem = system: @@ -52,7 +61,7 @@ in { default = pkgs.mkShell { name = "meshstack-hub"; - packages = (github_actions_preinstalled pkgs) ++ (core_packages pkgs); + packages = (github_actions_preinstalled pkgs) ++ (core_packages pkgs) ++ (go_packages pkgs); }; website = pkgs.mkShell { diff --git a/modules/aks/github-connector/meshstack_integration.tf b/modules/aks/github-connector/meshstack_integration.tf new file mode 100644 index 00000000..8e507c24 --- /dev/null +++ b/modules/aks/github-connector/meshstack_integration.tf @@ -0,0 +1,185 @@ +variable "hub" { + type = object({ + git_ref = string + }) + default = { + git_ref = "main" + } + description = "Hub release reference. Set git_ref to a tag (e.g. 'v1.2.3') or branch for the meshstack-hub repo." +} + +variable "meshstack" { + type = object({ + owning_workspace_identifier = string + }) + description = "Shared meshStack context passed down from the IaC runtime." +} + +variable "github" { + type = object({ + org = string + app_id = string + app_installation_id = string + app_pem_file = string + connector_config_tf_base64 = string + }) + sensitive = true + description = "GitHub App credentials and connector configuration for AKS integration." +} + +variable "github_repo_bbd" { + type = object({ + uuid = string + }) + description = "Reference to the GitHub Repository building block definition (dependency)." +} + +terraform { + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = ">= 0.19.3" + } + } +} + +locals { + config_tf_secret_value = "data:application/octet-stream;base64,${var.github.connector_config_tf_base64}" + + github_auth_inputs = { + "GITHUB_OWNER" = { + argument = jsonencode(var.github.org) + assignment_type = "STATIC" + description = "GitHub organization or user that owns the repositories managed by this building block." + display_name = "GitHub Owner" + is_environment = true + type = "STRING" + updateable_by_consumer = false + } + "GITHUB_APP_ID" = { + argument = jsonencode(var.github.app_id) + assignment_type = "STATIC" + description = "GitHub App ID used to authenticate the GitHub Terraform provider." + display_name = "GitHub App ID" + is_environment = true + type = "STRING" + updateable_by_consumer = false + } + "GITHUB_APP_INSTALLATION_ID" = { + argument = jsonencode(var.github.app_installation_id) + assignment_type = "STATIC" + description = "GitHub App Installation ID used to authenticate the GitHub Terraform provider." + display_name = "GitHub App Installation ID" + is_environment = true + type = "STRING" + updateable_by_consumer = false + } + "GITHUB_APP_PEM_FILE" = { + assignment_type = "STATIC" + description = "GitHub App PEM private key used to authenticate the GitHub Terraform provider." + display_name = "GitHub App PEM File" + is_environment = true + type = "CODE" + updateable_by_consumer = false + sensitive = { + argument = { + secret_value = var.github.app_pem_file + secret_version = sha256(var.github.app_pem_file) + } + } + } + } +} + +resource "meshstack_building_block_definition" "github_actions_connector" { + metadata = { + owned_by_workspace = var.meshstack.owning_workspace_identifier + } + + spec = { + description = "CI/CD pipeline using GitHub Actions for secure, scalable AKS deployment." + display_name = "GitHub Actions Integration with AKS" + symbol = provider::meshstack::load_image_file("${path.module}/buildingblock/logo.png") + target_type = "TENANT_LEVEL" + supported_platforms = [{ name = "AZURE_KUBERNETES_SERVICE" }] + run_transparency = true + readme = file("${path.module}/buildingblock/README.md") + } + + version_spec = { + draft = true + implementation = { + terraform = { + repository_url = "https://github.com/meshcloud/meshstack-hub.git" + terraform_version = "1.9.0" + async = false + ref_name = var.hub.git_ref + repository_path = "modules/aks/github-connector/buildingblock" + use_mesh_http_backend_fallback = true + } + } + dependency_refs = [ + { uuid = var.github_repo_bbd.uuid } + ] + inputs = merge(local.github_auth_inputs, { + "github_repo" = { + argument = jsonencode("${var.github_repo_bbd.uuid}.repo_name") + assignment_type = "BUILDING_BLOCK_OUTPUT" + description = "Full name (owner/repo) of the GitHub repository to connect." + display_name = "GitHub Repository" + is_environment = false + type = "STRING" + updateable_by_consumer = false + } + "namespace" = { + assignment_type = "PLATFORM_TENANT_ID" + display_name = "AKS Namespace" + is_environment = false + type = "STRING" + } + "github_environment_name" = { + assignment_type = "USER_INPUT" + description = "Name of the GitHub environment to use for deployments." + display_name = "GitHub Environment Name" + default_value = jsonencode("production") + is_environment = false + type = "STRING" + updateable_by_consumer = false + } + "additional_environment_variables" = { + assignment_type = "USER_INPUT" + description = "Map of additional environment variable key/value pairs to set as GitHub Actions environment variables." + display_name = "Additional Environment Variables" + default_value = jsonencode({}) + is_environment = false + type = "CODE" + updateable_by_consumer = false + } + "config.tf" = { + assignment_type = "STATIC" + description = "Content of the config.tf file provided to the building block run." + display_name = "config.tf" + is_environment = false + type = "FILE" + updateable_by_consumer = false + sensitive = { + argument = { + secret_value = local.config_tf_secret_value + secret_version = sha256(local.config_tf_secret_value) + } + } + } + }) + outputs = {} + } +} + +output "bbd_uuid" { + description = "UUID of the GitHub Actions Connector building block definition." + value = meshstack_building_block_definition.github_actions_connector.ref.uuid +} + +output "bbd_version_uuid" { + description = "UUID of the latest version of the GitHub Actions Connector building block definition." + value = meshstack_building_block_definition.github_actions_connector.version_latest.uuid +} diff --git a/modules/aks/meshstack_integration.tf b/modules/aks/meshstack_integration.tf index 4430884c..f1a6dc06 100644 --- a/modules/aks/meshstack_integration.tf +++ b/modules/aks/meshstack_integration.tf @@ -1,52 +1,163 @@ -# Change these values according to your AKS and meshStack setup. +variable "manage_meshstack_platform" { + description = "Whether to create meshstack_platform and meshstack_landingzone resources. Set to false for infra-only deployments." + type = bool + default = true +} + +variable "aks" { + description = "AKS cluster infrastructure and service principal configuration." + type = object({ + base_url = string + subscription_id = string + cluster_name = string + resource_group = string + + # meshcloud/meshplatform/aks module config + service_principal_name = string + namespace = optional(string, "meshcloud") + create_password = optional(bool, false) + workload_identity_federation = optional(object({ + issuer = string + access_subject = string + })) + replicator_enabled = optional(bool, true) + replicator_additional_rules = optional(list(object({ + api_groups = list(string) + resources = list(string) + verbs = list(string) + resource_names = optional(list(string)) + non_resource_urls = optional(list(string)) + })), []) + existing_clusterrole_name_replicator = optional(string, "") + kubernetes_name_suffix_replicator = optional(string, "") + metering_enabled = optional(bool, true) + metering_additional_rules = optional(list(object({ + api_groups = list(string) + resources = list(string) + verbs = list(string) + resource_names = optional(list(string)) + non_resource_urls = optional(list(string)) + })), []) + }) +} + +variable "meshstack_platform" { + description = "meshStack platform and landing zone registration. Required when manage_meshstack_platform is true." + default = null + type = object({ + owning_workspace_identifier = string + platform_identifier = string + location_identifier = optional(string, "global") + + display_name = optional(string, "AKS Namespace") + description = optional(string, "Azure Kubernetes Service (AKS). Create a k8s namespace in our AKS cluster.") + documentation_url = optional(string) + support_url = optional(string) + + disable_ssl_validation = optional(bool, true) + group_name_pattern = optional(string, "aks-#{workspaceIdentifier}.#{projectIdentifier}-#{platformGroupAlias}") + namespace_name_pattern = optional(string, "#{workspaceIdentifier}-#{projectIdentifier}") + user_lookup_strategy = optional(string, "UserByMailLookupStrategy") + send_azure_invitation_mail = optional(bool, false) + redirect_url = optional(string) + + quota_definitions = optional(list(object({ + quota_key = string + label = string + description = string + unit = string + max_value = number + min_value = number + auto_approval_threshold = number + })), []) + + landing_zone = optional(object({ + name = optional(string) + display_name = optional(string, "AKS Default") + description = optional(string, "Default AKS landing zone") + automate_deletion_approval = optional(bool, true) + automate_deletion_replication = optional(bool, true) + tags = optional(map(list(string)), {}) + quotas = optional(list(object({ + key = string + value = number + })), []) + kubernetes_role_mappings = optional(list(object({ + platform_roles = list(string) + project_role_ref = object({ name = string }) + })), [ + { platform_roles = ["admin"], project_role_ref = { name = "admin" } }, + { platform_roles = ["edit"], project_role_ref = { name = "user" } }, + { platform_roles = ["view"], project_role_ref = { name = "reader" } } + ]) + }), {}) + }) +} + +terraform { + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = ">= 0.19.3" + } + azuread = { + source = "hashicorp/azuread" + version = ">= 2.0.0" + } + } +} + locals { - # Existing AKS cluster config. - aks_base_url = "https://my-cluster.abc.europe.azmk8s.io" - aks_subscription_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - aks_cluster_name = "my-cluster" - aks_resource_group = "my-resource-group" - - # meshStack workspace that will manage the platform - aks_platform_workspace = "platform-aks" - aks_platform_identifier = "aks" - aks_location_identifier = "global" + landing_zone_name = var.manage_meshstack_platform ? coalesce( + var.meshstack_platform.landing_zone.name, + "${var.meshstack_platform.platform_identifier}-default" + ) : null } module "aks_meshplatform" { source = "meshcloud/meshplatform/aks" version = "~> 0.2.0" - namespace = "meshcloud" - scope = local.aks_subscription_id + namespace = var.aks.namespace + scope = var.aks.subscription_id - replicator_enabled = true - service_principal_name = "replicator-service-principal" + service_principal_name = var.aks.service_principal_name + create_password = var.aks.create_password + workload_identity_federation = var.aks.workload_identity_federation - metering_enabled = true + replicator_enabled = var.aks.replicator_enabled + replicator_additional_rules = var.aks.replicator_additional_rules + existing_clusterrole_name_replicator = var.aks.existing_clusterrole_name_replicator + kubernetes_name_suffix_replicator = var.aks.kubernetes_name_suffix_replicator - create_password = false # Use only workload identity federation - workload_identity_federation = { - issuer = data.meshstack_integrations.integrations.workload_identity_federation.replicator.issuer - access_subject = data.meshstack_integrations.integrations.workload_identity_federation.replicator.subject - } + metering_enabled = var.aks.metering_enabled + metering_additional_rules = var.aks.metering_additional_rules +} + +# For Entra tenant name +data "azuread_domains" "aad_domains" { + only_initial = true } resource "meshstack_platform" "aks" { + count = var.manage_meshstack_platform ? 1 : 0 + metadata = { - name = local.aks_platform_identifier - owned_by_workspace = local.aks_platform_workspace + name = var.meshstack_platform.platform_identifier + owned_by_workspace = var.meshstack_platform.owning_workspace_identifier } spec = { - description = "Azure Kubernetes Service (AKS). Create a k8s namespace in our AKS cluster." - display_name = "AKS Namespace" - endpoint = local.aks_base_url + description = var.meshstack_platform.description + display_name = var.meshstack_platform.display_name + documentation_url = var.meshstack_platform.documentation_url + support_url = var.meshstack_platform.support_url + endpoint = var.aks.base_url location_ref = { - name = local.aks_location_identifier + name = var.meshstack_platform.location_identifier } - # This platform is available to all users availability = { restriction = "PUBLIC" publication_state = "PUBLISHED" @@ -54,46 +165,41 @@ resource "meshstack_platform" "aks" { config = { aks = { - base_url = local.aks_base_url - disable_ssl_validation = true # Usually the case for Kubernetes clusters + base_url = var.aks.base_url + disable_ssl_validation = var.meshstack_platform.disable_ssl_validation replication = { - service_principal = { entra_tenant = data.azuread_domains.aad_domains.domains[0].domain_name client_id = module.aks_meshplatform.replicator_service_principal.Application_Client_ID object_id = module.aks_meshplatform.replicator_service_principal.Enterprise_Application_Object_ID - # No credential -> use workload identity federation auth = { credential = null } } - # Direct k8s access does not use workload identity federation access_token = { - secret_value = module.aks_meshplatform.replicator_token - # Use this to detect secret changes. Unfortunately, kubernets TF provider does not support ephemeral resources at the moment. + secret_value = module.aks_meshplatform.replicator_token secret_version = sha256(module.aks_meshplatform.replicator_token) } - group_name_pattern = "aks-#{workspaceIdentifier}.#{projectIdentifier}-#{platformGroupAlias}" - namespace_name_pattern = "#{workspaceIdentifier}-#{projectIdentifier}" - - user_lookup_strategy = "UserByMailLookupStrategy" - send_azure_invitation_mail = false + group_name_pattern = var.meshstack_platform.group_name_pattern + namespace_name_pattern = var.meshstack_platform.namespace_name_pattern - aks_subscription_id = local.aks_subscription_id - aks_cluster_name = local.aks_cluster_name - aks_resource_group = local.aks_resource_group + user_lookup_strategy = var.meshstack_platform.user_lookup_strategy + send_azure_invitation_mail = var.meshstack_platform.send_azure_invitation_mail + aks_subscription_id = var.aks.subscription_id + aks_cluster_name = var.aks.cluster_name + aks_resource_group = var.aks.resource_group + redirect_url = var.meshstack_platform.redirect_url } metering = { client_config = { access_token = { - secret_value = module.aks_meshplatform.metering_token - # Use this to detect secret changes. Unfortunately, kubernets TF provider does not support ephemeral resources at the moment. + secret_value = module.aks_meshplatform.metering_token secret_version = sha256(module.aks_meshplatform.metering_token) } } @@ -101,86 +207,68 @@ resource "meshstack_platform" "aks" { } } } + + quota_definitions = var.meshstack_platform.quota_definitions } } resource "meshstack_landingzone" "aks_default" { + count = var.manage_meshstack_platform ? 1 : 0 + metadata = { - name = "${local.aks_platform_identifier}-default" - owned_by_workspace = local.aks_platform_workspace + name = local.landing_zone_name + owned_by_workspace = var.meshstack_platform.owning_workspace_identifier + tags = var.meshstack_platform.landing_zone.tags } spec = { - description = "Default AKS landing zone" - display_name = "AKS Default" + description = var.meshstack_platform.landing_zone.description + display_name = var.meshstack_platform.landing_zone.display_name - platform_ref = meshstack_platform.aks.metadata + platform_ref = meshstack_platform.aks[0].metadata - automate_deletion_approval = true - automate_deletion_replication = true + automate_deletion_approval = var.meshstack_platform.landing_zone.automate_deletion_approval + automate_deletion_replication = var.meshstack_platform.landing_zone.automate_deletion_replication platform_properties = { aks = { - kubernetes_role_mappings = [ - { - platform_roles = [ - "admin" - ] - project_role_ref = { - name = "admin" - } - }, - { - platform_roles = [ - "edit" - ] - project_role_ref = { - name = "user" - } - }, - { - platform_roles = [ - "view" - ] - project_role_ref = { - name = "reader" - } - } - ] + kubernetes_role_mappings = var.meshstack_platform.landing_zone.kubernetes_role_mappings } } + + quotas = var.meshstack_platform.landing_zone.quotas } } -# For workload identity federation config -data "meshstack_integrations" "integrations" {} - -# For Entra tenant name -data "azuread_domains" "aad_domains" { - only_initial = true +output "aks" { + description = "AKS platform identifiers for use as var.aks in the starterkit. Null when manage_meshstack_platform is false." + value = var.manage_meshstack_platform ? { + full_platform_identifier = "${meshstack_platform.aks[0].metadata.name}.${var.meshstack_platform.location_identifier}" + landing_zone_dev_identifier = meshstack_landingzone.aks_default[0].metadata.name + landing_zone_prod_identifier = meshstack_landingzone.aks_default[0].metadata.name + } : null } -terraform { - required_providers { - meshstack = { - source = "meshcloud/meshstack" - version = "~> 0.19.1" - } - } +output "replicator_token" { + description = "Replicator service account token." + value = module.aks_meshplatform.replicator_token + sensitive = true } -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" +output "metering_token" { + description = "Metering service account token." + value = module.aks_meshplatform.metering_token + sensitive = true } -# Configure required providers -provider "azurerm" { - features {} - subscription_id = local.aks_subscription_id +output "replicator_service_principal" { + description = "Replicator Service Principal." + value = module.aks_meshplatform.replicator_service_principal + sensitive = true } -provider "kubernetes" { +output "replicator_service_principal_password" { + description = "Password for Replicator Service Principal." + value = module.aks_meshplatform.replicator_service_principal_password + sensitive = true } diff --git a/modules/aks/starterkit/backplane/README.md b/modules/aks/starterkit/backplane/README.md index 73f451ec..ecb56aa8 100644 --- a/modules/aks/starterkit/backplane/README.md +++ b/modules/aks/starterkit/backplane/README.md @@ -1,14 +1,46 @@ # AKS Starterkit Backplane -There is no terraform for starterkit backplane. +The AKS Starterkit backplane registers the child building block definitions +(GitHub Repository, GitHub Actions Connector, and optionally Azure PostgreSQL) +that compose the starterkit. -You need to manually create an API Key in meshStack and fill in the variables in the imported definition. +The backplane references other Hub modules via git URLs. +The `meshstack_integration.tf` includes this backplane using a local `./backplane` source. -## How to create an API Key + +## Requirements -> **Note**: you need to have Organization Admin permission in meshStack to create an API Key with admin rights. +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [meshstack](#requirement\_meshstack) | >= 0.19.3 | -1. In the Admin Area, go to "Access Control" > "API Keys" -2. Create a new API Key with the following permissions: -![alt text](permissions.png) -3. Copy the key ID to MESHSTACK_API_KEY and secret to MESHSTACK_API_SECRET +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [github\_connector\_bbd](#module\_github\_connector\_bbd) | github.com/meshcloud/meshstack-hub//modules/aks/github-connector | feature/aks-starter-kit-refactoring | +| [github\_repo\_bbd](#module\_github\_repo\_bbd) | github.com/meshcloud/meshstack-hub//modules/github/repository | feature/aks-starter-kit-refactoring | +| [postgresql\_bbd](#module\_postgresql\_bbd) | github.com/meshcloud/meshstack-hub//modules/azure/postgresql | feature/aks-starter-kit-refactoring | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [github](#input\_github) | GitHub App credentials and connector configuration. |
object({
org = string
app_id = string
app_installation_id = string
app_pem_file = string
connector_config_tf_base64 = string
})
| n/a | yes | +| [hub](#input\_hub) | Hub release reference. Set git\_ref to a tag (e.g. 'v1.2.3') or branch for the meshstack-hub repo. |
object({
git_ref = string
})
| n/a | yes | +| [meshstack](#input\_meshstack) | Shared meshStack context passed down from the IaC runtime. |
object({
owning_workspace_identifier = string
})
| n/a | yes | +| [postgresql](#input\_postgresql) | When non-null, registers the azure/postgresql BBD as part of the starterkit composition. Omit/null for deployments that don't need PostgreSQL. | `object({})` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [github\_connector\_bbd\_version\_uuid](#output\_github\_connector\_bbd\_version\_uuid) | UUID of the latest version of the GitHub Actions connector building block definition. | +| [github\_repo\_bbd\_uuid](#output\_github\_repo\_bbd\_uuid) | UUID of the GitHub repository building block definition. | +| [github\_repo\_bbd\_version\_uuid](#output\_github\_repo\_bbd\_version\_uuid) | UUID of the latest version of the GitHub repository building block definition. | + \ No newline at end of file diff --git a/modules/aks/starterkit/backplane/main.tf b/modules/aks/starterkit/backplane/main.tf new file mode 100644 index 00000000..623171c6 --- /dev/null +++ b/modules/aks/starterkit/backplane/main.tf @@ -0,0 +1,31 @@ +module "github_repo_bbd" { + source = "github.com/meshcloud/meshstack-hub//modules/github/repository?ref=feature/aks-starter-kit-refactoring" # will be updated by CI once merged to main + + hub = var.hub + meshstack = var.meshstack + github = { + org = var.github.org + app_id = var.github.app_id + app_installation_id = var.github.app_installation_id + app_pem_file = var.github.app_pem_file + } +} + +module "github_connector_bbd" { + source = "github.com/meshcloud/meshstack-hub//modules/aks/github-connector?ref=feature/aks-starter-kit-refactoring" + + hub = var.hub + meshstack = var.meshstack + github = var.github + github_repo_bbd = { + uuid = module.github_repo_bbd.bbd_uuid + } +} + +module "postgresql_bbd" { + count = var.postgresql != null ? 1 : 0 + source = "github.com/meshcloud/meshstack-hub//modules/azure/postgresql?ref=feature/aks-starter-kit-refactoring" + + hub = var.hub + meshstack = var.meshstack +} diff --git a/modules/aks/starterkit/backplane/outputs.tf b/modules/aks/starterkit/backplane/outputs.tf new file mode 100644 index 00000000..262aeab9 --- /dev/null +++ b/modules/aks/starterkit/backplane/outputs.tf @@ -0,0 +1,14 @@ +output "github_repo_bbd_uuid" { + description = "UUID of the GitHub repository building block definition." + value = module.github_repo_bbd.bbd_uuid +} + +output "github_repo_bbd_version_uuid" { + description = "UUID of the latest version of the GitHub repository building block definition." + value = module.github_repo_bbd.bbd_version_uuid +} + +output "github_connector_bbd_version_uuid" { + description = "UUID of the latest version of the GitHub Actions connector building block definition." + value = module.github_connector_bbd.bbd_version_uuid +} diff --git a/modules/aks/starterkit/backplane/variables.tf b/modules/aks/starterkit/backplane/variables.tf new file mode 100644 index 00000000..1d59c4e1 --- /dev/null +++ b/modules/aks/starterkit/backplane/variables.tf @@ -0,0 +1,31 @@ +variable "hub" { + type = object({ + git_ref = string + }) + description = "Hub release reference. Set git_ref to a tag (e.g. 'v1.2.3') or branch for the meshstack-hub repo." +} + +variable "meshstack" { + type = object({ + owning_workspace_identifier = string + }) + description = "Shared meshStack context passed down from the IaC runtime." +} + +variable "github" { + type = object({ + org = string + app_id = string + app_installation_id = string + app_pem_file = string + connector_config_tf_base64 = string + }) + sensitive = true + description = "GitHub App credentials and connector configuration." +} + +variable "postgresql" { + description = "When non-null, registers the azure/postgresql BBD as part of the starterkit composition. Omit/null for deployments that don't need PostgreSQL." + type = object({}) + default = null +} diff --git a/modules/aks/starterkit/backplane/versions.tf b/modules/aks/starterkit/backplane/versions.tf new file mode 100644 index 00000000..6c6a9bf0 --- /dev/null +++ b/modules/aks/starterkit/backplane/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = ">= 0.19.3" + } + } +} diff --git a/modules/aks/starterkit/meshstack_integration.tf b/modules/aks/starterkit/meshstack_integration.tf index f5d7e2c9..8b6fefa9 100644 --- a/modules/aks/starterkit/meshstack_integration.tf +++ b/modules/aks/starterkit/meshstack_integration.tf @@ -1,17 +1,51 @@ -locals { - owning_workspace_identifier = "my-workspace" - full_platform_identifier = "aks.k8s" - github_actions_connector_definition_version_uuid = "61f8de01-551d-4f1f-b9c4-ba94323910cd" - github_org = "my-org" - github_repo_definition_uuid = "11240216-2b3c-42db-8e15-c7b595cf207a" - github_repo_definition_version_uuid = "24654b9d-aedd-4dd3-94b0-0bc3bef52cb7" - landing_zone_dev_identifier = "aks-dev" - landing_zone_prod_identifier = "aks-prod" - tags = { +variable "hub" { + type = object({ + git_ref = string + }) + default = { + git_ref = "main" } - notification_subscribers = [ - ] - project_tags_yaml = trimspace(<<-YAML + description = "Hub release reference. Set git_ref to a tag (e.g. 'v1.2.3') or branch for the meshstack-hub repo." +} + +variable "meshstack" { + type = object({ + owning_workspace_identifier = string + }) + description = "Shared meshStack context passed down from the IaC runtime." +} + +variable "aks" { + description = "AKS platform identifiers. Can be passed from module.aks_platform.aks output." + type = object({ + full_platform_identifier = string + landing_zone_dev_identifier = string + landing_zone_prod_identifier = string + }) +} + +variable "github" { + type = object({ + org = string + app_id = string + app_installation_id = string + app_pem_file = string + connector_config_tf_base64 = string + }) + sensitive = true + description = "GitHub App credentials and connector configuration." +} + +variable "postgresql" { + description = "When non-null, registers the azure/postgresql BBD as part of the starterkit composition. Omit/null for deployments that don't need PostgreSQL." + type = object({}) + default = null +} + +variable "project_tags_yaml" { + description = "YAML string defining tags for created projects." + type = string + default = <<-YAML dev: environment: - "dev" @@ -19,23 +53,39 @@ prod: environment: - "prod" YAML - ) +} + +terraform { + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = ">= 0.19.3" + } + } +} + +module "backplane" { + source = "./backplane" + + hub = var.hub + meshstack = var.meshstack + github = var.github + postgresql = var.postgresql } resource "meshstack_building_block_definition" "aks_starterkit" { metadata = { - owned_by_workspace = local.owning_workspace_identifier - tags = local.tags + owned_by_workspace = var.meshstack.owning_workspace_identifier } spec = { - description = "The AKS Starterkit provides application teams with a pre-configured Kubernetes environment following Likvid Bank's best practices. It includes a Git repository, a CI/CD pipeline using GitHub Actions, and a secure container registry integration." - display_name = "AKS Starterkit" - notification_subscribers = local.notification_subscribers + description = "The AKS Starterkit provides application teams with a pre-configured Kubernetes environment following best practices. It includes a Git repository, a CI/CD pipeline using GitHub Actions, and a secure container registry integration." + display_name = "AKS Starterkit" + symbol = provider::meshstack::load_image_file("${path.module}/buildingblock/logo.png") readme = chomp(< 0 { + // Find all nodes whose dependencies are fully resolved. + var wave []T + for node, needs := range remaining { + ready := true + for dep := range needs { + if !resolved[dep] { + ready = false + break + } + } + if ready { + wave = append(wave, node) + } + } + + if len(wave) == 0 { + return nil, fmt.Errorf("circular dependency detected among remaining nodes") + } + + // Mark wave nodes as resolved and remove from remaining. + for _, node := range wave { + resolved[node] = true + delete(remaining, node) + } + + waves = append(waves, wave) + } + + return waves, nil +} diff --git a/tools/update-module-refs/dependency/dep_test.go b/tools/update-module-refs/dependency/dep_test.go new file mode 100644 index 00000000..57dbd467 --- /dev/null +++ b/tools/update-module-refs/dependency/dep_test.go @@ -0,0 +1,85 @@ +package dependency_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/meshcloud/meshstack-hub/tools/update-module-refs/dependency" +) + +func TestCalculateWaves_Linear(t *testing.T) { + // c -> b -> a (a is leaf) + deps := dependency.Dependencies[string]{ + {This: "a"}, + {This: "b", DependsOn: []string{"a"}}, + {This: "c", DependsOn: []string{"b"}}, + } + + waves, err := deps.CalculateWaves() + require.NoError(t, err) + require.Len(t, waves, 3) + + assert.ElementsMatch(t, []string{"a"}, waves[0]) + assert.ElementsMatch(t, []string{"b"}, waves[1]) + assert.ElementsMatch(t, []string{"c"}, waves[2]) +} + +func TestCalculateWaves_Diamond(t *testing.T) { + // d -> b, c; b -> a; c -> a + deps := dependency.Dependencies[string]{ + {This: "a"}, + {This: "b", DependsOn: []string{"a"}}, + {This: "c", DependsOn: []string{"a"}}, + {This: "d", DependsOn: []string{"b", "c"}}, + } + + waves, err := deps.CalculateWaves() + require.NoError(t, err) + require.Len(t, waves, 3) + + assert.ElementsMatch(t, []string{"a"}, waves[0]) + assert.ElementsMatch(t, []string{"b", "c"}, waves[1]) + assert.ElementsMatch(t, []string{"d"}, waves[2]) +} + +func TestCalculateWaves_AllIndependent(t *testing.T) { + deps := dependency.Dependencies[string]{ + {This: "x"}, + {This: "y"}, + {This: "z"}, + } + + waves, err := deps.CalculateWaves() + require.NoError(t, err) + require.Len(t, waves, 1) + + assert.ElementsMatch(t, []string{"x", "y", "z"}, waves[0]) +} + +func TestCalculateWaves_Circular(t *testing.T) { + deps := dependency.Dependencies[string]{ + {This: "a", DependsOn: []string{"b"}}, + {This: "b", DependsOn: []string{"a"}}, + } + + _, err := deps.CalculateWaves() + assert.Error(t, err) + assert.Contains(t, err.Error(), "circular dependency") +} + +func TestCalculateWaves_ExternalDeps(t *testing.T) { + // b depends on "ext" which is not in the deps list. + deps := dependency.Dependencies[string]{ + {This: "a"}, + {This: "b", DependsOn: []string{"a", "ext"}}, + } + + waves, err := deps.CalculateWaves() + require.NoError(t, err) + require.Len(t, waves, 2) + + assert.ElementsMatch(t, []string{"a"}, waves[0]) + assert.ElementsMatch(t, []string{"b"}, waves[1]) +} diff --git a/tools/update-module-refs/git/git.go b/tools/update-module-refs/git/git.go new file mode 100644 index 00000000..6883306c --- /dev/null +++ b/tools/update-module-refs/git/git.go @@ -0,0 +1,97 @@ +package git + +import ( + "fmt" + "os/exec" + "path/filepath" + "strings" +) + +// Log returns the commit SHA of the most recent commit that touched dirPath. +// dirPath can be absolute or relative to the repository root. +func Log(dirPath string) (string, error) { + root, err := repoRoot() + if err != nil { + return "", err + } + + relPath, err := toRelPath(root, dirPath) + if err != nil { + return "", err + } + + cmd := exec.Command("git", "log", "-n1", "--pretty=%H", "--", relPath) //nolint:gosec // dirPath is not user input + cmd.Dir = root + + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("git log for %q: %w", relPath, err) + } + + sha := strings.TrimSpace(string(out)) + if sha == "" { + return "", fmt.Errorf("git log for %q: no commits found", relPath) + } + + return sha, nil +} + +// AddAndCommit stages all changes under dirPath and commits with the given message. +// dirPath can be absolute or relative to the repository root. +func AddAndCommit(dirPath string, message string) error { + root, err := repoRoot() + if err != nil { + return err + } + + relPath, err := toRelPath(root, dirPath) + if err != nil { + return err + } + + add := exec.Command("git", "add", relPath) //nolint:gosec // dirPath is not user input + add.Dir = root + if out, err := add.CombinedOutput(); err != nil { + return fmt.Errorf("git add %q: %w\n%s", relPath, err, out) + } + + // Check if there are staged changes before committing. + diff := exec.Command("git", "diff", "--cached", "--quiet") + diff.Dir = root + if err := diff.Run(); err == nil { + // Exit code 0 means no staged changes. + return nil + } + + commit := exec.Command("git", "commit", "-m", message) //nolint:gosec // message is not user input + commit.Dir = root + if out, err := commit.CombinedOutput(); err != nil { + return fmt.Errorf("git commit: %w\n%s", err, out) + } + + return nil +} + +func repoRoot() (string, error) { + cmd := exec.Command("git", "rev-parse", "--show-toplevel") + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("git rev-parse --show-toplevel: %w", err) + } + + return strings.TrimSpace(string(out)), nil +} + +// toRelPath converts dirPath to a path relative to root if it is absolute. +func toRelPath(root, dirPath string) (string, error) { + if filepath.IsAbs(dirPath) { + rel, err := filepath.Rel(root, dirPath) + if err != nil { + return "", fmt.Errorf("making %q relative to %q: %w", dirPath, root, err) + } + + return rel, nil + } + + return dirPath, nil +} diff --git a/tools/update-module-refs/git/git_test.go b/tools/update-module-refs/git/git_test.go new file mode 100644 index 00000000..bd0558e6 --- /dev/null +++ b/tools/update-module-refs/git/git_test.go @@ -0,0 +1,72 @@ +package git_test + +import ( + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/meshcloud/meshstack-hub/tools/update-module-refs/git" +) + +func TestLog(t *testing.T) { + sha, err := git.Log(".") + require.NoError(t, err) + assert.Regexp(t, regexp.MustCompile(`^[0-9a-f]{40}$`), sha) +} + +func TestLogAbsolutePath(t *testing.T) { + // Resolve repo root to get an absolute path. + out, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() + require.NoError(t, err) + + absPath := strings.TrimSpace(string(out)) + sha, err := git.Log(absPath) + require.NoError(t, err) + assert.Regexp(t, regexp.MustCompile(`^[0-9a-f]{40}$`), sha) +} + +func TestAddAndCommit(t *testing.T) { + // Create a temporary git repo for an isolated test. + dir := t.TempDir() + + run := func(args ...string) { + t.Helper() + cmd := exec.Command(args[0], args[1:]...) //nolint:gosec + cmd.Dir = dir + out, err := cmd.CombinedOutput() + require.NoError(t, err, "command %v failed: %s", args, out) + } + + run("git", "init") + run("git", "config", "user.email", "test@test.com") + run("git", "config", "user.name", "Test") + + // Create an initial commit so the branch exists. + require.NoError(t, os.WriteFile(filepath.Join(dir, "init.txt"), []byte("init"), 0o644)) + run("git", "add", ".") + run("git", "commit", "-m", "initial") + + // Create a file and commit it via AddAndCommit. + subDir := filepath.Join(dir, "sub") + require.NoError(t, os.MkdirAll(subDir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(subDir, "test.txt"), []byte("hello"), 0o644)) + + // Chdir into the temp repo so git commands resolve to it. + t.Chdir(dir) + + err := git.AddAndCommit("sub", "test commit") + require.NoError(t, err) + + // Verify the commit exists. + cmd := exec.Command("git", "log", "-n1", "--pretty=%s") + cmd.Dir = dir + out, err := cmd.Output() + require.NoError(t, err) + assert.Equal(t, "test commit", strings.TrimSpace(string(out))) +} diff --git a/tools/update-module-refs/go.mod b/tools/update-module-refs/go.mod new file mode 100644 index 00000000..a0f4a626 --- /dev/null +++ b/tools/update-module-refs/go.mod @@ -0,0 +1,23 @@ +module github.com/meshcloud/meshstack-hub/tools/update-module-refs + +go 1.25.7 + +require ( + github.com/hashicorp/hcl/v2 v2.24.0 + github.com/stretchr/testify v1.11.1 + github.com/zclconf/go-cty v1.16.3 +) + +require ( + github.com/agext/levenshtein v1.2.1 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/tools/update-module-refs/go.sum b/tools/update-module-refs/go.sum new file mode 100644 index 00000000..5e4ae799 --- /dev/null +++ b/tools/update-module-refs/go.sum @@ -0,0 +1,34 @@ +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= +github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= +github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tools/update-module-refs/main.go b/tools/update-module-refs/main.go new file mode 100644 index 00000000..12fc7120 --- /dev/null +++ b/tools/update-module-refs/main.go @@ -0,0 +1,225 @@ +package main + +import ( + "flag" + "fmt" + "io/fs" + "log" + "os" + "path/filepath" + "strings" + + "github.com/meshcloud/meshstack-hub/tools/update-module-refs/dependency" + "github.com/meshcloud/meshstack-hub/tools/update-module-refs/git" + "github.com/meshcloud/meshstack-hub/tools/update-module-refs/tf" +) + +func main() { + repoURL := flag.String("repo", "github.com/meshcloud/meshstack-hub", "repository host+path to match module sources against") + dryRun := flag.Bool("dry-run", false, "only print what would be updated, do not write or commit") + flag.Parse() + + cwd, err := os.Getwd() + if err != nil { + log.Fatalf("failed to get working directory: %v", err) + } + + fsys := os.DirFS(cwd) + modules, err := scanModules(fsys, *repoURL) + if err != nil { + log.Fatalf("failed to scan modules: %v", err) + } + + log.Printf("found modules in %d directories", len(modules)) + + deps := buildDependencies(modules, *repoURL) + waves, err := deps.CalculateWaves() + if err != nil { + log.Fatalf("failed to calculate dependency waves: %v", err) + } + + log.Printf("calculated %d dependency waves", len(waves)) + + if *dryRun { + log.Println("WARNING: dry-run mode — refs shown may not be accurate as changes are not committed") + } + + for i, wave := range waves { + log.Printf("wave %d: %v", i, wave) + + for _, dir := range wave { + if err := processDirectory(cwd, dir, modules[dir], *repoURL, *dryRun); err != nil { + log.Fatalf("failed to process %q: %v", dir, err) + } + } + } +} + +func processDirectory(cwd, dir string, files []tf.File, repoURL string, dryRun bool) error { + dirChanged := false + + for _, f := range files { + fileChanged := false + + for _, m := range f.Modules { + depPath := extractModulePath(repoURL, m.Source()) + + sha, err := git.Log(depPath) + if err != nil { + return fmt.Errorf("git log for %q: %w", depPath, err) + } + + newSource := repoURL + "//" + depPath + "?ref=" + sha + if m.Source() == newSource { + log.Printf(" %s/%s -> %s (up to date)", dir, m.Name, depPath) + continue + } + + log.Printf(" %s/%s -> %s ref=%s", dir, m.Name, depPath, sha) + m.SetSource(newSource) + fileChanged = true + } + + if !fileChanged { + continue + } + + dirChanged = true + + if dryRun { + log.Printf(" would write %s", filepath.Join(dir, f.Path)) + continue + } + + outPath := filepath.Join(cwd, dir, f.Path) + if err := os.WriteFile(outPath, f.HCL.Bytes(), 0o644); err != nil { + return fmt.Errorf("writing %q: %w", outPath, err) + } + + log.Printf(" wrote %s", filepath.Join(dir, f.Path)) + } + + if !dirChanged { + log.Printf(" %s: all refs up to date, nothing to commit", dir) + return nil + } + + commitMsg := fmt.Sprintf("chore: update module refs in %s", dir) + + if dryRun { + log.Printf(" would commit %s: %q", dir, commitMsg) + return nil + } + + absDir := filepath.Join(cwd, dir) + if err := git.AddAndCommit(absDir, commitMsg); err != nil { + return fmt.Errorf("commit %q: %w", dir, err) + } + + log.Printf(" committed %s", dir) + + return nil +} + +// scanModules walks all directories in fsys and returns only those +// containing terraform modules with sources matching repoURL. +func scanModules(fsys fs.FS, repoURL string) (map[string][]tf.File, error) { + result := make(map[string][]tf.File) + + err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if !d.IsDir() { + return nil + } + + sub, err := fs.Sub(fsys, path) + if err != nil { + return err + } + + files, err := tf.Load(sub) + if err != nil { + return err + } + + // Filter to only files that have at least one module source matching repoURL. + var matched []tf.File + for _, f := range files { + var matchingModules []tf.ModuleSource + for _, m := range f.Modules { + if strings.HasPrefix(m.Source(), repoURL+"//") { + matchingModules = append(matchingModules, m) + } + } + + if len(matchingModules) > 0 { + matched = append(matched, tf.File{ + Path: f.Path, + HCL: f.HCL, + Modules: matchingModules, + }) + } + } + + if len(matched) > 0 { + result[path] = matched + } + + return nil + }) + + return result, err +} + +// buildDependencies creates dependency graph entries from the module map. +// Each directory becomes a node, its dependencies are the repo-internal module paths it references. +func buildDependencies(modules map[string][]tf.File, repoURL string) dependency.Dependencies[string] { + var deps dependency.Dependencies[string] + + for dir, files := range modules { + seen := make(map[string]bool) + + for _, f := range files { + for _, m := range f.Modules { + depPath := extractModulePath(repoURL, m.Source()) + if depPath != "" && !seen[depPath] { + seen[depPath] = true + } + } + } + + var dependsOn []string + for dep := range seen { + dependsOn = append(dependsOn, dep) + } + + deps = append(deps, dependency.Dependency[string]{ + This: dir, + DependsOn: dependsOn, + }) + } + + return deps +} + +// extractModulePath extracts the repo-relative path from a terraform module source. +// e.g. "github.com/meshcloud/meshstack-hub//modules/azure/postgresql?ref=v1.0.0" +// returns "modules/azure/postgresql". +func extractModulePath(repoURL, source string) string { + prefix := repoURL + "//" + if !strings.HasPrefix(source, prefix) { + return "" + } + + path := strings.TrimPrefix(source, prefix) + + // Strip ?ref=... query parameter. + if idx := strings.Index(path, "?"); idx != -1 { + path = path[:idx] + } + + return path +} diff --git a/tools/update-module-refs/tf/load.go b/tools/update-module-refs/tf/load.go new file mode 100644 index 00000000..1cae7271 --- /dev/null +++ b/tools/update-module-refs/tf/load.go @@ -0,0 +1,101 @@ +package tf + +import ( + "io/fs" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/zclconf/go-cty/cty" +) + +// File represents a parsed .tf file containing module blocks. +type File struct { + // Path is the file path relative to the walked fs.FS root. + Path string + // HCL is the underlying hclwrite file for serialization. + HCL *hclwrite.File + // Modules are the module blocks found in this file. + Modules []ModuleSource +} + +// ModuleSource represents a module block found in a .tf file. +type ModuleSource struct { + // Name is the module block label (e.g. "github_repo_bbd"). + Name string + // Body is the hclwrite body of the module block. + Body *hclwrite.Body +} + +// Source returns the raw value of the source attribute. +func (m ModuleSource) Source() string { + srcAttr := m.Body.GetAttribute("source") + if srcAttr == nil { + return "" + } + + src := strings.TrimSpace(string(srcAttr.Expr().BuildTokens(nil).Bytes())) + src = strings.Trim(src, `"`) + return src +} + +// SetSource updates the source attribute value. +func (m ModuleSource) SetSource(source string) { + m.Body.SetAttributeValue("source", cty.StringVal(source)) +} + +// Load reads all *.tf files in the directory represented by fsys, parses them +// with hclwrite, and extracts module blocks with their source attributes. +func Load(fsys fs.FS) ([]File, error) { + entries, err := fs.ReadDir(fsys, ".") + if err != nil { + return nil, err + } + + var results []File + + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".tf") { + continue + } + + content, err := fs.ReadFile(fsys, entry.Name()) + if err != nil { + return nil, err + } + + file, diags := hclwrite.ParseConfig(content, entry.Name(), hcl.InitialPos) + if diags.HasErrors() { + continue + } + + var modules []ModuleSource + for _, block := range file.Body().Blocks() { + if block.Type() != "module" { + continue + } + labels := block.Labels() + if len(labels) == 0 { + continue + } + if block.Body().GetAttribute("source") == nil { + continue + } + + modules = append(modules, ModuleSource{ + Name: labels[0], + Body: block.Body(), + }) + } + + if len(modules) > 0 { + results = append(results, File{ + Path: entry.Name(), + HCL: file, + Modules: modules, + }) + } + } + + return results, nil +} diff --git a/tools/update-module-refs/tf/load_test.go b/tools/update-module-refs/tf/load_test.go new file mode 100644 index 00000000..e8322a85 --- /dev/null +++ b/tools/update-module-refs/tf/load_test.go @@ -0,0 +1,51 @@ +package tf_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/meshcloud/meshstack-hub/tools/update-module-refs/tf" +) + +func TestLoad(t *testing.T) { + fsys := os.DirFS("testdata/simple") + files, err := tf.Load(fsys) + require.NoError(t, err) + + // Collect all modules across files. + type moduleInfo struct { + path string + source string + } + found := map[string]moduleInfo{} + for _, f := range files { + for _, m := range f.Modules { + found[m.Name] = moduleInfo{path: f.Path, source: m.Source()} + } + } + + assert.Len(t, found, 3) + + assert.Equal(t, moduleInfo{ + path: "main.tf", + source: "github.com/meshcloud/meshstack-hub//modules/github/repository?ref=main", + }, found["github_repo_bbd"]) + + assert.Equal(t, moduleInfo{ + path: "main.tf", + source: "./backplane", + }, found["backplane"]) + + assert.Equal(t, moduleInfo{ + path: "main.tf", + source: "github.com/meshcloud/meshstack-hub//modules/azure/postgresql?ref=v1.0.0", + }, found["postgresql_bbd"]) + + // Verify HCL file is present for serialization. + for _, f := range files { + assert.NotNil(t, f.HCL, "file %q: HCL should not be nil", f.Path) + } +} diff --git a/tools/update-module-refs/tf/testdata/simple/main.tf b/tools/update-module-refs/tf/testdata/simple/main.tf new file mode 100644 index 00000000..30f7f28b --- /dev/null +++ b/tools/update-module-refs/tf/testdata/simple/main.tf @@ -0,0 +1,25 @@ +variable "hub" { + type = object({ + git_ref = string + }) +} + +module "github_repo_bbd" { + source = "github.com/meshcloud/meshstack-hub//modules/github/repository?ref=main" + + hub = var.hub +} + +module "backplane" { + source = "./backplane" +} + +resource "meshstack_building_block_definition" "test" { + metadata = { + owned_by_workspace = "test" + } +} +module "postgresql_bbd" { + source = "github.com/meshcloud/meshstack-hub//modules/azure/postgresql?ref=v1.0.0" +} +