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_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/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/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/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() + "=", 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), 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.