Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .agents/skills/acceptance-testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ nohup bash -c "TF_ACC=1 go test -count=1 ./internal/provider/ -parallel 8 -timeo
- Target a single test: `-run 'TestAccBuildingBlock$/<subtest>'`.
- Always tee/redirect output to `/tmp` and **read the log** to investigate — do not re-run the
suite piped through `grep`.
- `ApplyAndTest` sets `MESHSTACK_SKIP_VERSION_CHECK` so the provider's anticipated-next-release
version floor can run ahead of the local `develop` backend — expected, no action needed.

## 2. Investigate

Expand Down
87 changes: 87 additions & 0 deletions .agents/skills/scratch-config-testing/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
name: scratch-config-testing
description: Build and run a meshStack Terraform config in the git-ignored scratch/ dir against a local meshStack, using the dev-built provider binary via dev_overrides. Use when reproducing or debugging a provider bug or a failing acceptance test as a standalone, re-runnable config — by dumping a test's HCL or hand-writing one.
---

# scratch-config-testing

Turn an acceptance test's generated HCL (or hand-written HCL) into a **standalone,
re-runnable** Terraform config under git-ignored `scratch/`, run with the locally-built
provider against a local meshStack. Complements the **acceptance-testing** skill (runs the
suite) and **meshstack-services** skill (brings up the backend). The `testconfig` builders
make a dumped config self-contained: applied to an empty meshStack it creates its full
dependency chain (workspace → dependent resources).

## Prerequisites

1. Local meshStack up — see the **meshstack-services** skill.
2. Provider env vars exported (the `http://localhost` guard applies):

```bash
set -a && source .env && set +a # MESHSTACK_ENDPOINT, MESHSTACK_API_KEY, MESHSTACK_API_SECRET
```
Comment thread
grubmeshi marked this conversation as resolved.

## One-time setup

Build the provider to the repo root, then point Terraform at it via a dev-override CLI config
kept separate from your global `~/.terraformrc`:

```bash
task build # -> ./terraform-provider-meshstack
cat > "$HOME/.terraformrc.dev" <<EOF
provider_installation {
dev_overrides {
"meshcloud/meshstack" = "$PWD" # dir containing the built binary (repo root)
}
direct {}
}
EOF
export TF_CLI_CONFIG_FILE=$HOME/.terraformrc.dev
```

With `dev_overrides`, Terraform prints a warning and **skips `terraform init`** — run
`terraform plan`/`apply` directly. Rebuild (`task build`) after any provider change.

## Get a config into scratch/

**From a test** — `MESHSTACK_SCRATCH_DUMP=1` makes `ApplyAndTest` dump each step's HCL and
return *without running the test* (no backend needed, fast, works even for configs that fail
to apply):

```bash
MESHSTACK_SCRATCH_DUMP=1 go test -run 'TestAccPaymentMethod$' ./internal/provider/ -v
```

Produces, under repo-root `scratch/`:

```text
scratch/TestAccPaymentMethod/step01/{main.tf,provider.tf} # create
scratch/TestAccPaymentMethod/step02/{main.tf,provider.tf} # update
```

`stepNN` is 1-based and matches the framework's step order; import-only steps (no `Config`)
are skipped. Subtests nest, e.g. `scratch/TestAccBuildingBlock/azure_devops/step01/`. Set the
env var to a path instead of `1` to dump elsewhere.

**Hand-written** — drop a `main.tf` into `scratch/repro/` and copy a `provider.tf` next to it
(same block the dump emits: `required_providers { meshstack = { source = "meshcloud/meshstack" } }`
plus an empty `provider "meshstack" {}`).

## Run it

```bash
cd scratch/TestAccPaymentMethod/step01
terraform plan # no init needed with dev_overrides
terraform apply
terraform destroy # clean up the meshObjects when done
```

Provider-side logs: `TF_LOG_PROVIDER=debug terraform apply`. To step through with a debugger,
build with `go build -gcflags="all=-N -l"` and attach delve to the running provider process.

## Notes

- The built-in `TF_ACC_PERSIST_WORKING_DIR=1` only *preserves* a test's temp working dir for
inspection — its provider is injected in-process via reattach, so that dir is **not**
runnable standalone. This skill's output is, because of the `provider.tf` + dev_override.
- `scratch/` is git-ignored; delete freely with `rm -rf scratch/`.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ providers-schema.json

.env

dist/
dist/

scratch/
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ referenced from the relevant sections below:
- **`modern-go`** — Go 1.26 `new(expression)` and the codebase's generics.
- **`changelog-management`** — pick the next version and maintain `CHANGELOG.md`.
- **`meshstack-services`** / **`acceptance-testing`** — bring up the local backend and run/debug the suite.
- **`scratch-config-testing`** — run a config in git-ignored `scratch/` against a local meshStack via the dev-built binary to reproduce/debug bugs.

Official Terraform Provider for managing meshStack resources via the meshObject API
(`/api/meshobjects`). Standard [terraform-plugin-framework](https://github.com/hashicorp/terraform-plugin-framework) v1.
Expand Down Expand Up @@ -72,7 +73,9 @@ TF_ACC=1 go test -count=1 -parallel 4 -timeout 600s ./internal/provider/ 2>&1 |
```

To bring up the backend services, see the **`meshstack-services`** skill; to run/debug the
full acceptance suite, see the **`acceptance-testing`** skill.
full acceptance suite, see the **`acceptance-testing`** skill. To reproduce a single test (or a
bug) as a standalone, re-runnable config against a local meshStack — dumping a test's HCL with
`MESHSTACK_SCRATCH_DUMP=1` or hand-writing one — see the **`scratch-config-testing`** skill.

### Acceptance test authoring (testconfig, builders, state checks)

Expand Down
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# v0.21.1
# v0.22.0

Requires meshStack 2026.24.0 or later due to roll out of one shared meshcloud hosted building block runner.

BREAKING CHANGES:
- `meshstack_building_block_definition`: For manual building blocks, `version_spec.outputs` is now computed from the API and must be omitted from configuration — the backend derives one output per input. Configuring an output with any `assignment_type` other than `PLATFORM_TENANT_ID` is now rejected. Remove `outputs` blocks from manual building block definitions; you may still declare an output with `assignment_type = "PLATFORM_TENANT_ID"` to mark which output carries the tenant id.

FIXES:
- `meshstack_building_block_definition`: Fixed "Provider produced inconsistent result after apply" for manual building blocks whose outputs the backend auto-generates from inputs (e.g. `SINGLE_SELECT`/`STATIC` inputs), including when toggling `version_spec.draft` from `false` to `true` together with input changes ([#131](https://github.com/meshcloud/terraform-provider-meshstack/issues/131), [#176](https://github.com/meshcloud/terraform-provider-meshstack/issues/176)). Outputs are now reconciled from the API response.
- `meshstack_building_block_definition`: Rotating a sensitive input's secret on a released (immutable) version now fails with a clear "Updating a version_spec in non-draft state is not allowed" error instead of an opaque "Failed to determine content hash ... [plaintext]" error ([#196](https://github.com/meshcloud/terraform-provider-meshstack/issues/196)). Set `version_spec.draft = true` to create a new draft version; the secret rotation can be applied in the same step.
- `meshstack_building_block_definition`: Fixed an erroneous "Failed to determine content hash" error for definitions whose `version_spec.inputs`/`version_spec.outputs` contain an entry named `plaintext`. The secret safeguard now inspects the typed secret values instead of matching the literal `plaintext` JSON key, so user-chosen input/output names are no longer mistaken for secrets.
- When no `runner_ref` is provided, the new shared building block runner UUID `98520496-627d-43e6-82da-ce499179ff3f` is used which is suitable for all implementation types.
Existing `building_block_definition` resources will see a plan change addressing this migration to a single shared runner.
Using the old shared runner UUIDs is deprecated but handled gracefully by the API.
Expand Down
2 changes: 1 addition & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/meshcloud/terraform-provider-meshstack/client/version"
)

var MinMeshStackVersion = version.MustParse("2026.23.0")
var MinMeshStackVersion = version.MustParse("2026.24.0")

// HttpError represents an HTTP error response with status code.
// This error is returned when an HTTP request fails with a non-2XX status code.
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description: |-

The meshStack terraform provider is an open-source tool, licensed under the MPL-2.0, and is actively maintained by meshcloud GmbH. The provider exposes APIs of meshStack to manage resources as code.

**Note:** This provider version requires meshStack version 2026.23.0 or higher. The provider automatically validates version compatibility during initialization.
**Note:** This provider version requires meshStack version 2026.24.0 or higher. The provider automatically validates version compatibility during initialization.

## Dependency wiring patterns

Expand Down
12 changes: 3 additions & 9 deletions docs/resources/building_block_definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,14 +215,8 @@ resource "meshstack_building_block_definition" "example_03_manual" {
manual = {}
}

# Output keys must match with inputs, as the backend copies over inputs to outputs
outputs = {
approval_required = {
display_name = "Approval Required"
type = "BOOLEAN"
assignment_type = "NONE"
}
}
# Outputs are omitted for manual building blocks: the backend derives them from the inputs
# (one output per input), so version_spec.outputs is computed and must not be set here.
}
}
```
Expand Down Expand Up @@ -395,7 +389,7 @@ Optional:
- `dependency_refs` (Attributes Set) Set of refs to building block definitions this definition depends on. Prefer reusable refs from `meshstack_building_block_definition.<name>.ref` or `one(data.meshstack_building_block_definitions.<name>.building_block_definitions).ref`. (see [below for nested schema](#nestedatt--version_spec--dependency_refs))
- `inputs` (Attributes Map) Map of input definitions for the building block. Keys are input names, values are input configuration objects. Inputs define parameters that building blocks can receive. (see [below for nested schema](#nestedatt--version_spec--inputs))
- `only_apply_once_per_tenant` (Boolean) Whether this building block can only be applied once per tenant.
- `outputs` (Attributes Map) Map of output definitions for the building block. Keys are output names, values are output configuration objects. Outputs define values that building blocks produce and can be consumed by other building blocks. (see [below for nested schema](#nestedatt--version_spec--outputs))
- `outputs` (Attributes Map) Map of output definitions for the building block. Keys are output names, values are output configuration objects. Outputs define values that building blocks produce and can be consumed by other building blocks. If implementation type is `manual`, outputs are computed from the API response, so omit this attribute entirely unless you want to specify a static `assignment_type = "PLATFORM_TENANT_ID"` as part of a landing zone. (see [below for nested schema](#nestedatt--version_spec--outputs))
- `permissions` (Set of String) Set of API permissions required by this building block. Will provide building block runs with an ephemeral API token with the specified workspace permissions. See [Workspace Permissions](https://docs.meshcloud.io/api/authentication/api-permissions/) for available values and [documentation on ephemeral API keys](https://docs.dev.meshcloud.io/concepts/building-block/#ephemeral-api-keys).
- `runner_ref` (Attributes) Reference to the runner to run the implementation. If omitted, the pre-defined shared runner is used suitable for the given `implementation` choice (see [below for nested schema](#nestedatt--version_spec--runner_ref))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,7 @@ resource "meshstack_building_block_definition" "example_03_manual" {
manual = {}
}

# Output keys must match with inputs, as the backend copies over inputs to outputs
outputs = {
approval_required = {
display_name = "Approval Required"
type = "BOOLEAN"
assignment_type = "NONE"
}
}
# Outputs are omitted for manual building blocks: the backend derives them from the inputs
# (one output per input), so version_spec.outputs is computed and must not be set here.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,33 +50,7 @@ resource "meshstack_building_block_definition" "example" {
manual = {}
}

# Output keys must match with inputs, as the backend copies over inputs to outputs for manual implementations.
outputs = {
name = {
display_name = "Name"
type = "STRING"
assignment_type = "NONE"
}
size = {
display_name = "Size"
type = "INTEGER"
assignment_type = "NONE"
}
environment = {
display_name = "Environment"
type = "STRING"
assignment_type = "NONE"
}
region = {
display_name = "Region"
type = "STRING"
assignment_type = "NONE"
}
enable_monitoring = {
display_name = "Enable Monitoring"
type = "BOOLEAN"
assignment_type = "NONE"
}
}
# Outputs are omitted for manual building blocks: the backend derives them from the inputs, so
# version_spec.outputs is computed and must not be set here.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,7 @@ resource "meshstack_building_block_definition" "bb_v2_tenant_bbd" {
manual = {}
}

# Output keys must match with inputs, as the backend copies over inputs to outputs for manual implementations.
outputs = {
name = {
display_name = "Name"
type = "STRING"
assignment_type = "NONE"
}
size = {
display_name = "Size"
type = "INTEGER"
assignment_type = "NONE"
}
environment = {
display_name = "Environment"
type = "STRING"
assignment_type = "NONE"
}
}
# Outputs are omitted for manual building blocks: the backend derives them from the inputs, so
# version_spec.outputs is computed and must not be set here.
}
}
45 changes: 45 additions & 0 deletions internal/clientmock/mock_buildingblock_definition_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func (m meshBuildingBlockDefinitionVersionClient) Create(_ context.Context, owne
versionUuid := uuid.NewString()
// Compute hashes for all secrets in the spec
backendSecretBehavior(true, &versionSpec, nil)
applyManualOutputBehavior(&versionSpec)

// Set version number if not already set
if versionSpec.VersionNumber == nil {
Expand All @@ -51,6 +52,7 @@ func (m meshBuildingBlockDefinitionVersionClient) Update(_ context.Context, uuid
if existing, ok := m.Store.Get(uuid); ok {
// Compute hashes for all secrets in the spec
backendSecretBehavior(false, &versionSpec, &existing.Spec)
applyManualOutputBehavior(&versionSpec)
if existing.Metadata.OwnedByWorkspace != ownedByWorkspace {
return nil, fmt.Errorf("mismatching workspace ownership: %s (existing) != %s (expected)", existing.Metadata.OwnedByWorkspace, ownedByWorkspace)
}
Expand All @@ -60,6 +62,49 @@ func (m meshBuildingBlockDefinitionVersionClient) Update(_ context.Context, uuid
return nil, fmt.Errorf("building block definition version not found: %s", uuid)
}

// applyManualOutputBehavior mirrors the real backend's ManualBuildingBlockCreationModule: for manual
// building blocks the outputs are derived from the inputs (one output per input, assignment type NONE,
// with SINGLE_SELECT/MULTI_SELECT/LIST input types translated to output-compatible types). Any output the
// caller marked PLATFORM_TENANT_ID keeps that assignment for the matching input; all other supplied
// outputs are ignored, matching the backend.
func applyManualOutputBehavior(versionSpec *client.MeshBuildingBlockDefinitionVersionSpec) {
if versionSpec.Implementation.Manual == nil {
return
}
platformTenantIdKeys := map[string]bool{}
for key, output := range versionSpec.Outputs {
if output.AssignmentType == client.MeshBuildingBlockDefinitionOutputAssignmentTypePlatformTenantID.Unwrap() {
platformTenantIdKeys[key] = true
}
}
outputs := make(map[string]client.MeshBuildingBlockDefinitionOutput, len(versionSpec.Inputs))
for key, input := range versionSpec.Inputs {
assignmentType := client.MeshBuildingBlockDefinitionOutputAssignmentTypeNone.Unwrap()
if platformTenantIdKeys[key] {
assignmentType = client.MeshBuildingBlockDefinitionOutputAssignmentTypePlatformTenantID.Unwrap()
}
outputs[key] = client.MeshBuildingBlockDefinitionOutput{
DisplayName: input.DisplayName,
Type: translateManualInputTypeToOutput(input.Type),
AssignmentType: assignmentType,
}
}
versionSpec.Outputs = outputs
}

// translateManualInputTypeToOutput mirrors backend ManualIOTypeTranslation: SINGLE_SELECT, MULTI_SELECT
// and LIST cannot be output types and are translated; all other types are kept as-is.
func translateManualInputTypeToOutput(inputType client.MeshBuildingBlockIOType) client.MeshBuildingBlockIOType {
switch inputType {
case client.MeshBuildingBlockIOTypeSingleSelect.Unwrap():
return client.MeshBuildingBlockIOTypeString.Unwrap()
case client.MeshBuildingBlockIOTypeMultiSelect.Unwrap(), client.MeshBuildingBlockIOTypeList.Unwrap():
return client.MeshBuildingBlockIOTypeCode.Unwrap()
default:
return inputType
}
}

func (m meshBuildingBlockDefinitionVersionClient) getNextVersionNumber() int {
maxNum := 0
for _, v := range m.Store.Values() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,43 @@ func BBDWithIntegration(t *testing.T, suffix string) (config Config, buildingBlo
).Join(workspaceConfig, integrationConfig), buildingBlockDefinitionAddr
}

// BBDGithubTwoIntegrations builds a github_workflows BBD wired to a first test integration ("A"),
// plus a second github integration ("B") owned by the same workspace. It returns the config, the
// BBD address, and integration B's address so a test can switch the BBD's integration_ref from A to
// B (e.g. to assert that re-drafting a released version with a new integration keeps the released
// version immutable). Both integrations reuse the github integration test-support file; the second
// is renamed and given a distinct display name so the backend treats it as a different integration.
func BBDGithubTwoIntegrations(t *testing.T) (config Config, buildingBlockDefinitionAddr Traversal, integrationBAddr Traversal) {
t.Helper()
workspaceConfig, workspaceAddr := Workspace(t)
exampleResource := Resource{Name: "building_block_definition", Suffix: "_02_github_workflows"}

var integrationAAddr Traversal
integrationAConfig := exampleResource.TestSupportConfig(t, "_integration").WithFirstBlock(
ExtractAddress(&integrationAAddr),
OwnedByWorkspace(workspaceAddr),
)
integrationBConfig := exampleResource.TestSupportConfig(t, "_integration").WithFirstBlock(
RenameKey("github_b"),
ExtractAddress(&integrationBAddr),
OwnedByWorkspace(workspaceAddr),
Descend("spec", "display_name")(SetString("GitHub Integration B")),
)

return exampleResource.Config(t).WithFirstBlock(
ExtractAddress(&buildingBlockDefinitionAddr),
OwnedByWorkspace(workspaceAddr),
Descend("version_spec", "implementation", "github_workflows", "integration_ref")(
SetAddr(integrationAAddr, "ref"),
),
// Depend on integration A explicitly: once the BBD switches its integration_ref to B, the
// released version still pins A on the backend (which refuses integration deletion while
// referenced). This keeps A scheduled for destruction after the BBD so teardown succeeds; B is
// already implicitly ordered via the integration_ref reference.
Descend("depends_on")(SetRawExpr("[%s]", integrationAAddr)),
).Join(workspaceConfig, integrationAConfig, integrationBConfig), buildingBlockDefinitionAddr, integrationBAddr
}

// BBDManual builds a BBD config with manual implementation type, owned by a new test workspace.
func BBDManual(t *testing.T) (config Config, buildingBlockDefinitionAddr Traversal) {
t.Helper()
Expand Down
Loading