From c052801cc6236e88a0cb8f65915cb0dc9cc34ad3 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 14 Oct 2026 10:41:01 +0200 Subject: [PATCH 1/3] test(testacc): skip the meshStack version check The backend of a satellite run or a local stack reports its newest tag. A branch that raises MinMeshStackVersion for the coming release would otherwise fail every test at login. Co-Authored-By: Claude Opus 5.5 --- cmd/internal/testacc/testacc_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/internal/testacc/testacc_test.go b/cmd/internal/testacc/testacc_test.go index 026b1d7f..4fcc1e49 100644 --- a/cmd/internal/testacc/testacc_test.go +++ b/cmd/internal/testacc/testacc_test.go @@ -116,7 +116,8 @@ func (c *cli) environ() []string { envEndpoint + "=" + c.endpoint, envProfile + "=", envWorkspace + "=", - setting.SkipVersionCheck.EnvKey() + "=", + // The backend under test reports its newest tag, which is below a floor raised for the coming release. + setting.SkipVersionCheck.EnvKey() + "=true", setting.ApiKeyClientId.EnvKey() + "=", setting.ApiKeyClientSecret.EnvKey() + "=", setting.ApiToken.EnvKey() + "=", From 21849ca92c9fd294d11baeaf3f5e14a7731665ee Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 14 Oct 2026 10:09:11 +0200 Subject: [PATCH 2/3] feat(client): move building blocks and their definitions to the GA APIs The meshBuildingBlock client uses the v2 media type and the meshBuildingBlockDefinition and its version client the v1 one, none of them -preview any more. The GA meshBuildingBlock carries the parents in spec.parentBuildingBlockRefs alone, so the client drops the deprecated parentBuildingBlocks field and its MarshalJSON and UnmarshalJSON. The v1 building block client (client.BuildingBlock, MeshBuildingBlock and its DTOs) goes as well: only the Terraform provider's removed meshstack_buildingblock resource used it. MinMeshStackVersion is 2026.42.0, the earliest release that can serve the GA APIs. Set it to the release that ships them once it is known. Co-Authored-By: Claude Opus 5.5 --- client/building_block_definition.go | 2 +- client/building_block_definition_version.go | 2 +- client/building_block_v2.go | 124 +----------------- client/buildingblock.go | 95 -------------- client/client.go | 4 +- client/client_kind_test.go | 1 - client/wire.go | 12 ++ .../testutil/fakemeshstack/fakemeshstack.go | 2 +- 8 files changed, 17 insertions(+), 225 deletions(-) delete mode 100644 client/buildingblock.go create mode 100644 client/wire.go diff --git a/client/building_block_definition.go b/client/building_block_definition.go index 3c062e02..ba1a951e 100644 --- a/client/building_block_definition.go +++ b/client/building_block_definition.go @@ -151,7 +151,7 @@ type meshBuildingBlockDefinitionClient struct { func newBuildingBlockDefinitionClient(ctx context.Context, httpClient internal.HttpClient) meshBuildingBlockDefinitionClient { return meshBuildingBlockDefinitionClient{ - meshObject: internal.NewMeshObjectClient[MeshBuildingBlockDefinition](ctx, httpClient, "v1-preview"), + meshObject: internal.NewMeshObjectClient[MeshBuildingBlockDefinition](ctx, httpClient, "v1"), } } diff --git a/client/building_block_definition_version.go b/client/building_block_definition_version.go index df33344c..dadddc73 100644 --- a/client/building_block_definition_version.go +++ b/client/building_block_definition_version.go @@ -223,7 +223,7 @@ type meshBuildingBlockDefinitionVersionClient struct { func newBuildingBlockDefinitionVersionClient(ctx context.Context, httpClient internal.HttpClient) meshBuildingBlockDefinitionVersionClient { return meshBuildingBlockDefinitionVersionClient{ - meshObject: internal.NewMeshObjectClient[MeshBuildingBlockDefinitionVersion](ctx, httpClient, "v1-preview"), + meshObject: internal.NewMeshObjectClient[MeshBuildingBlockDefinitionVersion](ctx, httpClient, "v1"), } } diff --git a/client/building_block_v2.go b/client/building_block_v2.go index d8803dfe..f492a056 100644 --- a/client/building_block_v2.go +++ b/client/building_block_v2.go @@ -2,7 +2,6 @@ package client import ( "context" - "encoding/json/jsontext" "encoding/json/v2" "errors" "fmt" @@ -57,127 +56,6 @@ type MeshBuildingBlockV2Spec struct { // Inputs as pointer MeshBuildingBlockInput to support mocking secret responses. Inputs map[string]*MeshBuildingBlockInput `json:"inputs" tfsdk:"inputs"` ParentBuildingBlockRefs types.Set[UuidRef] `json:"parentBuildingBlockRefs" tfsdk:"parent_building_block_refs"` - - // ParentBuildingBlocks is the deprecated parentBuildingBlocks field, which MarshalJSON and - // UnmarshalJSON carry. The deprecated meshstack_building_block_v2 surfaces of the Terraform - // provider read the definition uuid they report from it. - ParentBuildingBlocks types.Set[MeshBuildingBlockV2Parent] `json:"-" tfsdk:"-"` -} - -// MeshBuildingBlockV2Parent is an entry of the deprecated parentBuildingBlocks field. -type MeshBuildingBlockV2Parent struct { - UuidRef - - // BuildingBlockUuid always holds the same value as Uuid. - BuildingBlockUuid string `json:"buildingBlockUuid"` - // DefinitionUuid is the parent's building block definition. The backend derives it from the - // referenced block, so every response carries it and a request never does. - DefinitionUuid string `json:"definitionUuid,omitempty"` -} - -// UnmarshalJSON fills Uuid from the deprecated buildingBlockUuid, which is where a response carries -// the parent's identity. -func (p *MeshBuildingBlockV2Parent) UnmarshalJSON(data []byte) error { - type wire MeshBuildingBlockV2Parent - var target wire - if err := json.Unmarshal(data, &target); err != nil { - return err - } - - *p = MeshBuildingBlockV2Parent(target) - if p.Uuid == "" { - p.Uuid = p.BuildingBlockUuid - } - p.BuildingBlockUuid = p.Uuid - if p.Kind == "" { - p.Kind = MeshObjectKind.BuildingBlock - } - - return nil -} - -// wireCompatibility repeats the options internal/json marshals every request with: a v1-style -// MarshalJSON receives none of its caller's, so without it a nested value would go out in a -// different shape than the request around it. -var wireCompatibility = json.JoinOptions( - json.Deterministic(true), - json.FormatNilSliceAsNull(true), - json.FormatNilMapAsNull(true), -) - -// MarshalJSON sends the parents under both field names: parentBuildingBlockRefs, and the deprecated -// parentBuildingBlocks for a backend that does not know the new field yet. A newer backend accepts -// both as long as they name the same building blocks, and an older one ignores the field it does not -// know, because the meshObject API does not reject unknown properties. -// -// Together with UnmarshalJSON this is the whole compatibility window. Once every backend still in use -// knows parentBuildingBlockRefs, both methods can go. -func (s MeshBuildingBlockV2Spec) MarshalJSON() ([]byte, error) { - type wire MeshBuildingBlockV2Spec - w := wire(s) - if len(w.ParentBuildingBlockRefs) == 0 { - w.ParentBuildingBlockRefs = parentRefsFromDeprecated(w.ParentBuildingBlocks) - } - - var fields map[string]jsontext.Value - if encoded, err := json.Marshal(w, wireCompatibility); err != nil { - return nil, err - } else if err := json.Unmarshal(encoded, &fields); err != nil { - return nil, err - } - - // The deprecated entry sends only buildingBlockUuid: every backend in the supported range reads - // the parent from it, and the definition uuid is always derived from the referenced block. - parents := make([]struct { - BuildingBlockUuid string `json:"buildingBlockUuid"` - }, 0, len(s.ParentBuildingBlockRefs)) - for _, ref := range s.ParentBuildingBlockRefs { - parents = append(parents, struct { - BuildingBlockUuid string `json:"buildingBlockUuid"` - }{BuildingBlockUuid: ref.Uuid}) - } - var err error - if fields["parentBuildingBlocks"], err = json.Marshal(parents); err != nil { - return nil, err - } - - return json.Marshal(fields, wireCompatibility) -} - -func parentRefsFromDeprecated(parents types.Set[MeshBuildingBlockV2Parent]) types.Set[UuidRef] { - refs := make(types.Set[UuidRef], 0, len(parents)) - for _, parent := range parents { - refs = append(refs, UuidRef{Uuid: parent.Uuid, Kind: MeshObjectKind.BuildingBlock}) - } - return refs -} - -// UnmarshalJSON reads the parents from parentBuildingBlockRefs, or from the deprecated -// parentBuildingBlocks when a backend does not serve the new field yet. Terraform then sees the same -// elements against either backend and set hashing stays stable. -func (s *MeshBuildingBlockV2Spec) UnmarshalJSON(data []byte) error { - type wire MeshBuildingBlockV2Spec - var target struct { - wire - - ParentBuildingBlocks types.Set[MeshBuildingBlockV2Parent] `json:"parentBuildingBlocks"` - } - if err := json.Unmarshal(data, &target); err != nil { - return err - } - - *s = MeshBuildingBlockV2Spec(target.wire) - s.ParentBuildingBlocks = target.ParentBuildingBlocks - if len(s.ParentBuildingBlockRefs) == 0 { - s.ParentBuildingBlockRefs = parentRefsFromDeprecated(s.ParentBuildingBlocks) - } - for i := range s.ParentBuildingBlockRefs { - if s.ParentBuildingBlockRefs[i].Kind == "" { - s.ParentBuildingBlockRefs[i].Kind = MeshObjectKind.BuildingBlock - } - } - - return nil } type MeshBuildingBlockInput struct { @@ -305,7 +183,7 @@ type meshBuildingBlockV2Client struct { } func newBuildingBlockV2Client(ctx context.Context, httpClient internal.HttpClient) meshBuildingBlockV2Client { - return meshBuildingBlockV2Client{internal.NewMeshObjectClient[MeshBuildingBlockV2](ctx, httpClient, "v2-preview")} + return meshBuildingBlockV2Client{internal.NewMeshObjectClient[MeshBuildingBlockV2](ctx, httpClient, "v2")} } func (c meshBuildingBlockV2Client) Read(ctx context.Context, uuid string) (*MeshBuildingBlockV2, error) { diff --git a/client/buildingblock.go b/client/buildingblock.go deleted file mode 100644 index abe02215..00000000 --- a/client/buildingblock.go +++ /dev/null @@ -1,95 +0,0 @@ -package client - -import ( - "context" - - "github.com/meshcloud/meshstack-cli/client/internal" -) - -//nolint:staticcheck // ST1003: deprecated names, kept as published; the v2 building block client and the IO type enum supersede them -const ( - MESH_BUILDING_BLOCK_IO_TYPE_STRING = "STRING" - MESH_BUILDING_BLOCK_IO_TYPE_INTEGER = "INTEGER" - MESH_BUILDING_BLOCK_IO_TYPE_BOOLEAN = "BOOLEAN" - MESH_BUILDING_BLOCK_IO_TYPE_SINGLE_SELECT = "SINGLE_SELECT" - MESH_BUILDING_BLOCK_IO_TYPE_MULTI_SELECT = "MULTI_SELECT" - MESH_BUILDING_BLOCK_IO_TYPE_FILE = "FILE" - MESH_BUILDING_BLOCK_IO_TYPE_LIST = "LIST" - MESH_BUILDING_BLOCK_IO_TYPE_CODE = "CODE" -) - -type MeshBuildingBlock struct { - Metadata MeshBuildingBlockMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshBuildingBlockSpec `json:"spec" tfsdk:"spec"` - Status MeshBuildingBlockStatus `json:"status" tfsdk:"status"` -} - -type MeshBuildingBlockMetadata struct { - Uuid string `json:"uuid" tfsdk:"uuid"` - DefinitionUuid string `json:"definitionUuid" tfsdk:"definition_uuid"` - DefinitionVersion int64 `json:"definitionVersion" tfsdk:"definition_version"` - TenantIdentifier string `json:"tenantIdentifier" tfsdk:"tenant_identifier"` - ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` - CreatedOn string `json:"createdOn" tfsdk:"created_on"` - MarkedForDeletionOn *string `json:"markedForDeletionOn" tfsdk:"marked_for_deletion_on"` - MarkedForDeletionBy *string `json:"markedForDeletionBy" tfsdk:"marked_for_deletion_by"` -} - -type MeshBuildingBlockSpec struct { - DisplayName string `json:"displayName" tfsdk:"display_name"` - Inputs []MeshBuildingBlockIO `json:"inputs" tfsdk:"inputs"` - ParentBuildingBlocks []MeshBuildingBlockParent `json:"parentBuildingBlocks" tfsdk:"parent_building_blocks"` -} - -type MeshBuildingBlockIO struct { - Key string `json:"key" tfsdk:"key"` - Value any `json:"value" tfsdk:"value"` - ValueType string `json:"valueType" tfsdk:"value_type"` -} - -type MeshBuildingBlockParent struct { - BuildingBlockUuid string `json:"buildingBlockUuid" tfsdk:"buildingblock_uuid"` - DefinitionUuid string `json:"definitionUuid" tfsdk:"definition_uuid"` -} - -type MeshBuildingBlockStatus struct { - Status string `json:"status" tfsdk:"status"` - Outputs []MeshBuildingBlockIO `json:"outputs" tfsdk:"outputs"` -} - -type MeshBuildingBlockCreate struct { - Metadata MeshBuildingBlockCreateMetadata `json:"metadata" tfsdk:"metadata"` - Spec MeshBuildingBlockSpec `json:"spec" tfsdk:"spec"` -} - -type MeshBuildingBlockCreateMetadata struct { - DefinitionUuid string `json:"definitionUuid" tfsdk:"definition_uuid"` - DefinitionVersion int64 `json:"definitionVersion" tfsdk:"definition_version"` - TenantIdentifier string `json:"tenantIdentifier" tfsdk:"tenant_identifier"` -} - -type MeshBuildingBlockClient interface { - Read(ctx context.Context, uuid string) (*MeshBuildingBlock, error) - Create(ctx context.Context, bb *MeshBuildingBlockCreate) (*MeshBuildingBlock, error) - Delete(ctx context.Context, uuid string) error -} - -type meshBuildingBlockClient struct { - meshObject internal.MeshObjectClient[MeshBuildingBlock] -} - -func newBuildingBlockClient(ctx context.Context, httpClient internal.HttpClient) MeshBuildingBlockClient { - return meshBuildingBlockClient{internal.NewMeshObjectClient[MeshBuildingBlock](ctx, httpClient, "v1")} -} - -func (c meshBuildingBlockClient) Read(ctx context.Context, uuid string) (*MeshBuildingBlock, error) { - return c.meshObject.Get(ctx, uuid) -} - -func (c meshBuildingBlockClient) Create(ctx context.Context, bb *MeshBuildingBlockCreate) (*MeshBuildingBlock, error) { - return c.meshObject.Post(ctx, bb) -} - -func (c meshBuildingBlockClient) Delete(ctx context.Context, uuid string) error { - return c.meshObject.Delete(ctx, uuid) -} diff --git a/client/client.go b/client/client.go index 36e1b13b..2b839436 100644 --- a/client/client.go +++ b/client/client.go @@ -9,7 +9,7 @@ import ( "github.com/meshcloud/meshstack-cli/internal/version" ) -var MinMeshStackVersion = version.MustParse("2026.36.0") +var MinMeshStackVersion = version.MustParse("2026.42.0") // Version is re-exported because MinMeshStackVersion is one and internal/version is closed to // another module. @@ -21,7 +21,6 @@ type Authorization = http.Authorization type Client struct { ApiKey MeshApiKeyClient - BuildingBlock MeshBuildingBlockClient BuildingBlockV2 MeshBuildingBlockV2Client BuildingBlockRun MeshBuildingBlockRunClient BuildingBlockDefinition MeshBuildingBlockDefinitionClient @@ -61,7 +60,6 @@ func New(ctx context.Context, endpoint xurl.URL, client http.Client, auth Author workspace := newWorkspaceClient(ctx, authorizedClient) return Client{ ApiKey: newApiKeyClient(ctx, authorizedClient), - BuildingBlock: newBuildingBlockClient(ctx, authorizedClient), BuildingBlockV2: buildingBlockV2, BuildingBlockRun: buildingBlockRun, BuildingBlockDefinition: buildingBlockDefinition, diff --git a/client/client_kind_test.go b/client/client_kind_test.go index ecc66293..7d43cf6f 100644 --- a/client/client_kind_test.go +++ b/client/client_kind_test.go @@ -10,7 +10,6 @@ import ( func TestKind(t *testing.T) { assert.Equal(t, internal.InferKind[MeshApiKey](), MeshObjectKind.ApiKey) - assert.Equal(t, internal.InferKind[MeshBuildingBlock](), MeshObjectKind.BuildingBlock) assert.Equal(t, internal.InferKind[MeshBuildingBlockV2](), MeshObjectKind.BuildingBlock) assert.Equal(t, internal.InferKind[MeshBuildingBlockDefinition](), MeshObjectKind.BuildingBlockDefinition) assert.Equal(t, internal.InferKind[MeshBuildingBlockDefinitionVersion](), MeshObjectKind.BuildingBlockDefinitionVersion) diff --git a/client/wire.go b/client/wire.go new file mode 100644 index 00000000..781c4a27 --- /dev/null +++ b/client/wire.go @@ -0,0 +1,12 @@ +package client + +import "encoding/json/v2" + +// wireCompatibility repeats the options internal/json marshals every request with: a v1-style +// MarshalJSON receives none of its caller's, so without it a nested value would go out in a +// different shape than the request around it. +var wireCompatibility = json.JoinOptions( + json.Deterministic(true), + json.FormatNilSliceAsNull(true), + json.FormatNilMapAsNull(true), +) diff --git a/internal/testutil/fakemeshstack/fakemeshstack.go b/internal/testutil/fakemeshstack/fakemeshstack.go index 11b60027..af7bd000 100644 --- a/internal/testutil/fakemeshstack/fakemeshstack.go +++ b/internal/testutil/fakemeshstack/fakemeshstack.go @@ -28,7 +28,7 @@ import ( ) // DefaultVersion is at least client.MinMeshStackVersion, or the CLI refuses the backend. -const DefaultVersion = "2026.39.0" +const DefaultVersion = "2026.42.0" // Token is an unsigned JWT that expires in 2100, so the CLI sends it rather than asking for a new // one: a token without an expiry counts as expired. Every Server honors it. From c99b890b4ada9b2501316fdc563449024ee9a616 Mon Sep 17 00:00:00 2001 From: Andreas Grub Date: Wed, 14 Oct 2026 11:40:35 +0200 Subject: [PATCH 3/3] refactor(client): marshal through internal/json in MarshalJSON methods The client and the variant package each kept a copy of the options internal/json marshals every request with. Marshaling through internal/json.Marshal keeps a single option set. Co-Authored-By: Claude Opus 5.5 --- ...ing_block_definition_version_implementation.go | 4 ++-- client/integration_config.go | 4 ++-- client/types/variant/variant.go | 15 ++++----------- client/wire.go | 12 ------------ internal/json/marshal.go | 8 +++----- 5 files changed, 11 insertions(+), 32 deletions(-) delete mode 100644 client/wire.go diff --git a/client/building_block_definition_version_implementation.go b/client/building_block_definition_version_implementation.go index a0ebf028..45022519 100644 --- a/client/building_block_definition_version_implementation.go +++ b/client/building_block_definition_version_implementation.go @@ -1,12 +1,12 @@ package client import ( - "encoding/json/v2" "fmt" "github.com/meshcloud/meshstack-cli/client/types" "github.com/meshcloud/meshstack-cli/client/types/enum" "github.com/meshcloud/meshstack-cli/client/types/variant" + "github.com/meshcloud/meshstack-cli/internal/json" ) type MeshBuildingBlockImplementationType string @@ -100,7 +100,7 @@ func (m MeshBuildingBlockDefinitionImplementation) MarshalJSON() ([]byte, error) } w.Type = inferred } - return json.Marshal(w, wireCompatibility) + return json.Marshal(w) } func (m *MeshBuildingBlockDefinitionImplementation) UnmarshalJSON(bytes []byte) error { diff --git a/client/integration_config.go b/client/integration_config.go index 1cce3f57..af45d1ac 100644 --- a/client/integration_config.go +++ b/client/integration_config.go @@ -1,12 +1,12 @@ package client import ( - "encoding/json/v2" "fmt" "github.com/meshcloud/meshstack-cli/client/types" "github.com/meshcloud/meshstack-cli/client/types/enum" "github.com/meshcloud/meshstack-cli/client/types/variant" + "github.com/meshcloud/meshstack-cli/internal/json" ) type MeshIntegrationConfigType string @@ -82,7 +82,7 @@ func (m MeshIntegrationConfig) MarshalJSON() ([]byte, error) { } w.Type = inferred } - return json.Marshal(w, wireCompatibility) + return json.Marshal(w) } func (m *MeshIntegrationConfig) UnmarshalJSON(bytes []byte) error { diff --git a/client/types/variant/variant.go b/client/types/variant/variant.go index 7f18367f..00f42219 100644 --- a/client/types/variant/variant.go +++ b/client/types/variant/variant.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" "reflect" + + wirejson "github.com/meshcloud/meshstack-cli/internal/json" ) // A Variant decodes one JSON value into X or into Y. When both decode, X wins, so Y can be a type @@ -19,20 +21,11 @@ var ( _ json.Marshaler = Variant[int, string]{} ) -// wireCompatibility repeats the options internal/json marshals every request with: a v1-style -// MarshalJSON receives none of its caller's. A Y decoded from JSON is a map, so without -// Deterministic it would leave this method in a random member order. -var wireCompatibility = json.JoinOptions( - json.Deterministic(true), - json.FormatNilSliceAsNull(true), - json.FormatNilMapAsNull(true), -) - func (v Variant[X, Y]) MarshalJSON() ([]byte, error) { if v.HasX() { - return json.Marshal(v.X, wireCompatibility) + return wirejson.Marshal(v.X) } else if v.HasY() { - return json.Marshal(v.Y, wireCompatibility) + return wirejson.Marshal(v.Y) } return json.Marshal(nil) } diff --git a/client/wire.go b/client/wire.go deleted file mode 100644 index 781c4a27..00000000 --- a/client/wire.go +++ /dev/null @@ -1,12 +0,0 @@ -package client - -import "encoding/json/v2" - -// wireCompatibility repeats the options internal/json marshals every request with: a v1-style -// MarshalJSON receives none of its caller's, so without it a nested value would go out in a -// different shape than the request around it. -var wireCompatibility = json.JoinOptions( - json.Deterministic(true), - json.FormatNilSliceAsNull(true), - json.FormatNilMapAsNull(true), -) diff --git a/internal/json/marshal.go b/internal/json/marshal.go index 762bb4fe..89445f82 100644 --- a/internal/json/marshal.go +++ b/internal/json/marshal.go @@ -11,11 +11,9 @@ import ( "path/filepath" ) -// wireCompatibility holds the output to the shape encoding/json v1 produced: a nil slice and a -// nil map go out as null rather than as [] and {}, and object members are sorted. json/v2 would -// write [] and {} and leave map order to chance, and neither change is ours to make — the -// meshObject API may read null and an empty collection differently on a PUT, and the Terraform -// provider hashes this JSON to decide whether a resource changed. +// wireCompatibility keeps the output in the shape encoding/json v1 wrote, because the meshObject API +// reads a null collection differently from an empty one. Sorted map keys keep the files written here +// stable. A MarshalJSON method gets none of its caller's options, so it marshals through [Marshal]. var wireCompatibility = json.JoinOptions( json.Deterministic(true), json.FormatNilSliceAsNull(true),