diff --git a/.golangci.yml b/.golangci.yml index 19cf480e..33a66408 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -178,6 +178,11 @@ linters: # field order follows what reads well, not what packs tightest - fieldalignment + recvcheck: + # Extends the built-in exclusion of UnmarshalJSON to its json/v2 counterpart. + exclusions: + - "*.UnmarshalJSONFrom" + staticcheck: checks: - all @@ -232,6 +237,14 @@ linters: depguard: rules: + # The $ keeps encoding/json/v2 and encoding/json/jsontext out of the match. + json: + files: + - $all + deny: + - pkg: encoding/json$ + desc: Use encoding/json/v2 and internal/json instead + # cobra is used in cmd/ and nowhere else. cmd: files: @@ -249,14 +262,12 @@ linters: deny: - pkg: log desc: Use log/slog instead - - pkg: encoding/json - desc: Use encoding/json/v2 and internal/json instead allow: - $gostd - log/slog - # the deny on encoding/json matches its subpackages too; a listing yields raw items + # a listing yields raw items, and reads a field out of one, such as the uuid a run + # listing needs per block - encoding/json/jsontext - # and reads a field out of a raw item, such as the uuid a run listing needs per block - encoding/json/v2 # no direct access to internal/ - github.com/meshcloud/meshstack-cli/pkg @@ -274,8 +285,6 @@ linters: deny: - pkg: log desc: Use log/slog instead - - pkg: encoding/json - desc: Use encoding/json/v2 and internal/json instead allow: - $gostd - log/slog @@ -292,13 +301,10 @@ linters: deny: - pkg: log desc: Use log/slog instead - - pkg: encoding/json - desc: Use encoding/json/v2 and internal/json instead allow: - $gostd - log/slog - # the deny on encoding/json matches its subpackages too; a request body is checked as - # JSON, and api-docs writes a part of the OpenAPI document + # a request body is checked as JSON, and api-docs writes a part of the OpenAPI document - encoding/json/jsontext - encoding/json/v2 - github.com/meshcloud/meshstack-cli/cmd/internal @@ -326,8 +332,6 @@ linters: deny: - pkg: log desc: Use log/slog instead - - pkg: encoding/json - desc: Use encoding/json/v2 and internal/json instead allow: - $gostd - log/slog @@ -349,8 +353,6 @@ linters: deny: - pkg: log desc: Use log/slog instead - - pkg: encoding/json - desc: Use encoding/json/v2 and internal/json instead allow: - $gostd - log/slog @@ -371,8 +373,6 @@ linters: deny: - pkg: log desc: Use log/slog instead - - pkg: encoding/json - desc: Use encoding/json/v2 and internal/json instead allow: - $gostd - log/slog @@ -408,14 +408,11 @@ linters: deny: - pkg: log desc: Use log/slog instead - - pkg: encoding/json - desc: Use encoding/json/v2 and internal/json instead allow: - $gostd - log/slog - # the deny on encoding/json matches its subpackages too; a listing writes raw items + # a listing writes raw items, and a command that shows one object marshals it - encoding/json/jsontext - # and a command that shows one object marshals it, or writes it as Markdown - encoding/json/v2 - github.com/meshcloud/meshstack-cli/cmd/internal/markdown - github.com/meshcloud/meshstack-cli/cmd/internal/color @@ -448,8 +445,6 @@ linters: deny: - pkg: log desc: Use log/slog instead - - pkg: encoding/json - desc: Use encoding/json/v2 and internal/json instead allow: - $gostd - encoding/json/v2 @@ -468,8 +463,6 @@ linters: deny: - pkg: log desc: Use log/slog instead - - pkg: encoding/json - desc: Use encoding/json/v2 and internal/json instead allow: - $gostd - log/slog @@ -484,9 +477,6 @@ linters: - "**/internal/json/*.go" - "!$test" list-mode: strict - deny: - - pkg: encoding/json - desc: Use encoding/json/v2 and internal/json instead allow: - $gostd - encoding/json/v2 @@ -514,8 +504,6 @@ linters: deny: - pkg: log desc: Use log/slog instead - - pkg: encoding/json - desc: Use encoding/json/v2 and internal/json instead allow: - $gostd - encoding/json/v2 @@ -531,9 +519,6 @@ linters: files: - "**/internal/testutil/**/*.go" list-mode: strict - deny: - - pkg: encoding/json - desc: Use encoding/json/v2 and internal/json instead allow: - $gostd - encoding/json/v2 @@ -569,9 +554,6 @@ linters: files: - "$test" list-mode: strict - deny: - - pkg: encoding/json - desc: Use encoding/json/v2 and internal/json instead allow: - $gostd - encoding/json/v2 diff --git a/client/building_block_definition_version.go b/client/building_block_definition_version.go index df33344c..014d761b 100644 --- a/client/building_block_definition_version.go +++ b/client/building_block_definition_version.go @@ -2,6 +2,7 @@ package client import ( "context" + "encoding/json/jsontext" "encoding/json/v2" "errors" "fmt" @@ -121,8 +122,8 @@ type MeshBuildingBlockDefinitionInput struct { IsEnvironment bool `json:"isEnvironment" tfsdk:"is_environment"` IsSensitive bool `json:"isSensitive" tfsdk:"-"` // Argument and DefaultValue hold a [types.Secret] when IsSensitive is true and a [types.Any] - // otherwise. [types.Variant] decodes into X first, so UnmarshalJSON moves a non-sensitive value - // that also decodes as a Secret over to Y. + // otherwise. [types.Variant] decodes into X first, so UnmarshalJSONFrom moves a non-sensitive + // value that also decodes as a Secret over to Y. Argument types.SecretOrAny `json:"argument" tfsdk:"argument"` DefaultValue types.SecretOrAny `json:"defaultValue" tfsdk:"default_value"` UpdateableByConsumer bool `json:"updateableByConsumer" tfsdk:"updateable_by_consumer"` @@ -141,10 +142,10 @@ type MeshBuildingBlockDefinitionInput struct { DisplayOrder int64 `json:"displayOrder" tfsdk:"display_order"` } -func (m *MeshBuildingBlockDefinitionInput) UnmarshalJSON(bytes []byte) error { +func (m *MeshBuildingBlockDefinitionInput) UnmarshalJSONFrom(dec *jsontext.Decoder) error { type wrapped MeshBuildingBlockDefinitionInput var target wrapped - if err := json.Unmarshal(bytes, &target); err != nil { + if err := json.UnmarshalDecode(dec, &target, dec.Options()); err != nil { return err } *m = MeshBuildingBlockDefinitionInput(target) @@ -192,10 +193,9 @@ type MeshBuildingBlockDefinitionVersionSpec struct { VersionNumber *int64 `json:"versionNumber,omitzero" tfsdk:"version_number"` State *MeshBuildingBlockDefinitionVersionState `json:"state,omitzero" tfsdk:"state"` RunnerRef *UuidRef `json:"runnerRef" tfsdk:"runner_ref"` - // Replaces the deprecated bare-UUID dependencyDefinitionUuids; requires a backend serving it. - DependencyDefinitionRefs types.Set[UuidRef] `json:"dependencyDefinitionRefs,omitempty" tfsdk:"dependency_refs"` - Implementation MeshBuildingBlockDefinitionImplementation `json:"implementation" tfsdk:"implementation"` - Inputs map[string]*MeshBuildingBlockDefinitionInput `json:"inputs" tfsdk:"inputs"` + DependencyDefinitionRefs types.Set[UuidRef] `json:"dependencyDefinitionRefs,omitempty" tfsdk:"dependency_refs"` + Implementation MeshBuildingBlockDefinitionImplementation `json:"implementation" tfsdk:"implementation"` + Inputs map[string]*MeshBuildingBlockDefinitionInput `json:"inputs" tfsdk:"inputs"` } type MeshBuildingBlockDefinitionVersionStatus struct { diff --git a/client/building_block_definition_version_implementation.go b/client/building_block_definition_version_implementation.go index a0ebf028..edc65eee 100644 --- a/client/building_block_definition_version_implementation.go +++ b/client/building_block_definition_version_implementation.go @@ -1,6 +1,7 @@ package client import ( + "encoding/json/jsontext" "encoding/json/v2" "fmt" @@ -90,23 +91,23 @@ func (m MeshBuildingBlockDefinitionImplementation) InferType() (enum.Entry[MeshB return result, nil } -func (m MeshBuildingBlockDefinitionImplementation) MarshalJSON() ([]byte, error) { +func (m MeshBuildingBlockDefinitionImplementation) MarshalJSONTo(enc *jsontext.Encoder) error { type wrapped MeshBuildingBlockDefinitionImplementation w := wrapped(m) if len(w.Type) == 0 { inferred, err := m.InferType() if err != nil { - return nil, err + return err } w.Type = inferred } - return json.Marshal(w, wireCompatibility) + return json.MarshalEncode(enc, w, enc.Options()) } -func (m *MeshBuildingBlockDefinitionImplementation) UnmarshalJSON(bytes []byte) error { +func (m *MeshBuildingBlockDefinitionImplementation) UnmarshalJSONFrom(dec *jsontext.Decoder) error { type wrapped MeshBuildingBlockDefinitionImplementation var target wrapped - if err := json.Unmarshal(bytes, &target); err != nil { + if err := json.UnmarshalDecode(dec, &target, dec.Options()); err != nil { return err } *m = MeshBuildingBlockDefinitionImplementation(target) diff --git a/client/building_block_run.go b/client/building_block_run.go index 6ee02b87..565cb7bf 100644 --- a/client/building_block_run.go +++ b/client/building_block_run.go @@ -15,6 +15,10 @@ type MeshBuildingBlockRun struct { Links MeshBuildingBlockRunLinks `json:"_links,omitzero"` } +func (run MeshBuildingBlockRun) IsRunning() bool { + return run.Status == "PENDING" || run.Status == "IN_PROGRESS" +} + type MeshBuildingBlockRunMetadata struct { Uuid string `json:"uuid"` CreatedAt time.Time `json:"createdAt"` diff --git a/client/building_block_v2.go b/client/building_block_v2.go index ae13eafd..01d204dc 100644 --- a/client/building_block_v2.go +++ b/client/building_block_v2.go @@ -23,21 +23,6 @@ var ( BuildingBlockLifecycleStateDeleted = BuildingBlockLifecycleStates.Entry("DELETED") ) -type BuildingBlockStatus string - -var ( - BuildingBlockStatuses = enum.Enum[BuildingBlockStatus]{} - BuildingBlockStatusWaitingForDependentInput = BuildingBlockStatuses.Entry("WAITING_FOR_DEPENDENT_INPUT") - BuildingBlockStatusWaitingForOperatorInput = BuildingBlockStatuses.Entry("WAITING_FOR_OPERATOR_INPUT") - BuildingBlockStatusWaitingForUserInput = BuildingBlockStatuses.Entry("WAITING_FOR_USER_INPUT") - BuildingBlockStatusWaitingForApproval = BuildingBlockStatuses.Entry("WAITING_FOR_APPROVAL") - BuildingBlockStatusPending = BuildingBlockStatuses.Entry("PENDING") - BuildingBlockStatusInProgress = BuildingBlockStatuses.Entry("IN_PROGRESS") - BuildingBlockStatusSucceeded = BuildingBlockStatuses.Entry("SUCCEEDED") - BuildingBlockStatusFailed = BuildingBlockStatuses.Entry("FAILED") - BuildingBlockStatusAborted = BuildingBlockStatuses.Entry("ABORTED") -) - type BuildingBlockHealthStatus string var ( @@ -84,127 +69,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 { @@ -213,15 +77,15 @@ type MeshBuildingBlockInput struct { AssignmentType enum.Entry[MeshBuildingBlockInputAssignmentType] `json:"assignmentType,omitempty" tfsdk:"-"` // IsSensitive decides the case of Value: [types.Secret] when true, [types.Any] otherwise. - // [types.Variant] decodes into X first, so UnmarshalJSON moves a non-sensitive value that also + // [types.Variant] decodes into X first, so UnmarshalJSONFrom moves a non-sensitive value that also // decodes as a Secret over to Y. IsSensitive bool `json:"isSensitive" tfsdk:"-"` } -func (m *MeshBuildingBlockInput) UnmarshalJSON(bytes []byte) error { +func (m *MeshBuildingBlockInput) UnmarshalJSONFrom(dec *jsontext.Decoder) error { type wrapped MeshBuildingBlockInput var target wrapped - if err := json.Unmarshal(bytes, &target); err != nil { + if err := json.UnmarshalDecode(dec, &target, dec.Options()); err != nil { return err } *m = MeshBuildingBlockInput(target) @@ -265,7 +129,6 @@ type MeshBuildingBlockV2Lifecycle struct { } type MeshBuildingBlockV2Status struct { - Status enum.Entry[BuildingBlockStatus] `json:"status" tfsdk:"status"` HealthStatus enum.Entry[BuildingBlockHealthStatus] `json:"healthStatus" tfsdk:"health_status"` Action enum.Entry[BuildingBlockAction] `json:"action" tfsdk:"action"` Outputs map[string]MeshBuildingBlockOutput `json:"outputs" tfsdk:"outputs"` @@ -325,7 +188,6 @@ type MeshBuildingBlockV2ListFilter struct { TenantUuid *string `json:"tenantUuid"` // TargetKind is meshTenant or meshWorkspace. TargetKind *string `json:"targetRefKind"` - Status *string `json:"status"` // HealthStatus and Action match a building block that has any one of the values. HealthStatus []string `json:"healthStatus"` Action []string `json:"action"` 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 ea7e0983..413ac16c 100644 --- a/client/client.go +++ b/client/client.go @@ -21,7 +21,6 @@ type Authorization = http.Authorization type Client struct { ApiKey MeshApiKeyClient - BuildingBlock MeshBuildingBlockClient BuildingBlockV2 MeshBuildingBlockV2Client BuildingBlockRun MeshBuildingBlockRunClient BuildingBlockRunAction *MeshBuildingBlockRunActionClient @@ -62,7 +61,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, BuildingBlockRunAction: newBuildingBlockRunActionClient(authorizedClient, buildingBlockRun), 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..5b63eb4e 100644 --- a/client/integration_config.go +++ b/client/integration_config.go @@ -1,6 +1,7 @@ package client import ( + "encoding/json/jsontext" "encoding/json/v2" "fmt" @@ -70,7 +71,7 @@ func (m MeshIntegrationConfig) InferType() (enum.Entry[MeshIntegrationConfigType return result, nil } -func (m MeshIntegrationConfig) MarshalJSON() ([]byte, error) { +func (m MeshIntegrationConfig) MarshalJSONTo(enc *jsontext.Encoder) error { type wrapped MeshIntegrationConfig w := wrapped(m) // Built-in integrations (replicator, metering) come with a type but no variant, so the type is only @@ -78,17 +79,17 @@ func (m MeshIntegrationConfig) MarshalJSON() ([]byte, error) { if len(w.Type) == 0 { inferred, err := m.InferType() if err != nil { - return nil, err + return err } w.Type = inferred } - return json.Marshal(w, wireCompatibility) + return json.MarshalEncode(enc, w, enc.Options()) } -func (m *MeshIntegrationConfig) UnmarshalJSON(bytes []byte) error { +func (m *MeshIntegrationConfig) UnmarshalJSONFrom(dec *jsontext.Decoder) error { type wrapped MeshIntegrationConfig var target wrapped - if err := json.Unmarshal(bytes, &target); err != nil { + if err := json.UnmarshalDecode(dec, &target, dec.Options()); err != nil { return err } *m = MeshIntegrationConfig(target) diff --git a/client/openapi/object.go b/client/openapi/object.go index 34ca2337..a6732ff2 100644 --- a/client/openapi/object.go +++ b/client/openapi/object.go @@ -8,8 +8,6 @@ import ( // object keeps the order of its members, which a map would lose, so that a part of the document // prints as the document has it. -// -//nolint:recvcheck // MarshalJSONTo takes a value, so that json.Marshal finds it on an object that is not addressable type object []member type member struct { diff --git a/client/raw.go b/client/raw.go index ddc2e65f..17bdd4c5 100644 --- a/client/raw.go +++ b/client/raw.go @@ -26,9 +26,7 @@ type SortCriteria []string // to the Terraform provider. type RawClient struct { httpClient internal.HttpClient - // apis is keyed by the Go type rather than the kind, because MeshBuildingBlock and - // MeshBuildingBlockV2 share a kind in different API versions. - apis map[reflect.Type]rawApi + apis map[reflect.Type]rawApi } type rawApi struct { diff --git a/client/types/variant/variant.go b/client/types/variant/variant.go index 7f18367f..127acb34 100644 --- a/client/types/variant/variant.go +++ b/client/types/variant/variant.go @@ -1,6 +1,7 @@ package variant import ( + "encoding/json/jsontext" "encoding/json/v2" "errors" "fmt" @@ -15,26 +16,17 @@ type Variant[X, Y any] struct { } var ( - _ json.Unmarshaler = (*Variant[int, string])(nil) - _ json.Marshaler = Variant[int, string]{} + _ json.UnmarshalerFrom = (*Variant[int, string])(nil) + _ json.MarshalerTo = 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) { +func (v Variant[X, Y]) MarshalJSONTo(enc *jsontext.Encoder) error { if v.HasX() { - return json.Marshal(v.X, wireCompatibility) + return json.MarshalEncode(enc, v.X, enc.Options()) } else if v.HasY() { - return json.Marshal(v.Y, wireCompatibility) + return json.MarshalEncode(enc, v.Y, enc.Options()) } - return json.Marshal(nil) + return enc.WriteToken(jsontext.Null) } func has[T any](xy any) bool { @@ -72,9 +64,13 @@ func (v Variant[X, Y]) WithY(action func(y *Y)) { } } -func (v *Variant[X, Y]) UnmarshalJSON(bytes []byte) error { - errX := json.Unmarshal(bytes, &v.X) - errY := json.Unmarshal(bytes, &v.Y) +func (v *Variant[X, Y]) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + bytes, err := dec.ReadValue() + if err != nil { + return err + } + errX := json.Unmarshal(bytes, &v.X, dec.Options()) + errY := json.Unmarshal(bytes, &v.Y, dec.Options()) switch { case v.HasX() && v.HasY(): // Y is cleared because a Y of type any decodes from every JSON value. diff --git a/cmd/buildingblock/trigger_run_test.go b/cmd/buildingblock/trigger_run_test.go index 76ec755e..90ea4577 100644 --- a/cmd/buildingblock/trigger_run_test.go +++ b/cmd/buildingblock/trigger_run_test.go @@ -25,7 +25,7 @@ func TestTriggerRunPrintsTheBuildingBlockAndFailsWithTheReasonMeshStackCouldNotS ) failure := func(failedOn string) string { return fmt.Sprintf(`{"metadata": {"uuid": %q, "ownedByWorkspace": "my-workspace"}, "spec": {}, `+ - `"status": {"status": "PENDING", "latestRunUuid": "a0000000-0000-4000-8000-000000000001", "runStartFailure": %q, "runStartFailedOn": %q}}`, + `"status": {"action": "PENDING", "latestRunUuid": "a0000000-0000-4000-8000-000000000001", "runStartFailure": %q, "runStartFailedOn": %q}}`, buildingBlockUuid, reason, failedOn) } meshStack := fakemeshstack.Start(t, fakemeshstack.Options{BuildingBlocks: []any{jsontext.Value(failure("2026-10-01T10:05:00Z"))}}) diff --git a/cmd/internal/testacc/testdata/bootstrap/main.tf b/cmd/internal/testacc/testdata/bootstrap/main.tf index 5ada3eb1..0c9a428c 100644 --- a/cmd/internal/testacc/testdata/bootstrap/main.tf +++ b/cmd/internal/testacc/testdata/bootstrap/main.tf @@ -6,8 +6,8 @@ terraform { required_providers { meshstack = { source = "meshcloud/meshstack" - # The first release that reads a profile of the meshStack CLI. - version = ">= 0.26.0" + # The first release that waits through status.action. + version = ">= 0.27.1" } } } @@ -109,8 +109,8 @@ resource "meshstack_building_block" "noop" { lifecycle { postcondition { - condition = self.status.status == "SUCCEEDED" - error_message = "The run of building block ${self.metadata.uuid} is ${self.status.status}." + condition = self.status.succeeded + error_message = "The run of building block ${self.metadata.uuid} did not succeed." } } } diff --git a/docs/demo/fakemeshstack/meshstack.go b/docs/demo/fakemeshstack/meshstack.go index c460a39c..4515d041 100644 --- a/docs/demo/fakemeshstack/meshstack.go +++ b/docs/demo/fakemeshstack/meshstack.go @@ -75,40 +75,40 @@ type buildingBlock struct { DisplayName string `json:"displayName"` } `json:"spec"` Status struct { - Status string `json:"status"` + Action string `json:"action"` } `json:"status"` } // buildingBlocks are more than a demo's --limit, so that the CLI says it listed only the first ones. func buildingBlocks() (objects []any) { for _, block := range [][3]string{ - {"PostgreSQL Database", "platform-team", "SUCCEEDED"}, - {"Azure Storage Account", "data-analytics", "SUCCEEDED"}, - {"GitHub Repository", "customer-portal", "SUCCEEDED"}, + {"PostgreSQL Database", "platform-team", "NO_ACTION"}, + {"Azure Storage Account", "data-analytics", "NO_ACTION"}, + {"GitHub Repository", "customer-portal", "NO_ACTION"}, {"Kubernetes Namespace", "payments", "IN_PROGRESS"}, - {"AWS S3 Bucket", "machine-learning", "SUCCEEDED"}, - {"Key Vault", "security-ops", "FAILED"}, - {"CI/CD Pipeline", "mobile-apps", "SUCCEEDED"}, - {"Redis Cache", "billing", "SUCCEEDED"}, - {"Budget Alert", "marketing-web", "SUCCEEDED"}, - {"Container Registry", "devex", "SUCCEEDED"}, - {"Log Analytics Workspace", "observability", "SUCCEEDED"}, - {"Service Principal", "identity-access", "SUCCEEDED"}, - {"PostgreSQL Database", "logistics", "SUCCEEDED"}, - {"GitHub Repository", "internal-tools", "SUCCEEDED"}, - {"Kubernetes Namespace", "search", "SUCCEEDED"}, - {"DNS Zone", "customer-portal", "SUCCEEDED"}, - {"AWS S3 Bucket", "hr-systems", "WAITING_FOR_OPERATOR_INPUT"}, - {"Budget Alert", "payments", "SUCCEEDED"}, - {"Key Vault", "platform-team", "SUCCEEDED"}, - {"CI/CD Pipeline", "data-analytics", "SUCCEEDED"}, - {"Redis Cache", "customer-portal", "SUCCEEDED"}, - {"Container Registry", "platform-team", "SUCCEEDED"}, - {"DNS Zone", "marketing-web", "SUCCEEDED"}, + {"AWS S3 Bucket", "machine-learning", "NO_ACTION"}, + {"Key Vault", "security-ops", "REVIEW_ERRORS"}, + {"CI/CD Pipeline", "mobile-apps", "NO_ACTION"}, + {"Redis Cache", "billing", "NO_ACTION"}, + {"Budget Alert", "marketing-web", "NO_ACTION"}, + {"Container Registry", "devex", "NO_ACTION"}, + {"Log Analytics Workspace", "observability", "NO_ACTION"}, + {"Service Principal", "identity-access", "NO_ACTION"}, + {"PostgreSQL Database", "logistics", "NO_ACTION"}, + {"GitHub Repository", "internal-tools", "NO_ACTION"}, + {"Kubernetes Namespace", "search", "NO_ACTION"}, + {"DNS Zone", "customer-portal", "NO_ACTION"}, + {"AWS S3 Bucket", "hr-systems", "PROVIDE_INPUT"}, + {"Budget Alert", "payments", "NO_ACTION"}, + {"Key Vault", "platform-team", "NO_ACTION"}, + {"CI/CD Pipeline", "data-analytics", "NO_ACTION"}, + {"Redis Cache", "customer-portal", "NO_ACTION"}, + {"Container Registry", "platform-team", "NO_ACTION"}, + {"DNS Zone", "marketing-web", "NO_ACTION"}, } { var object buildingBlock object.Metadata.Uuid, object.Metadata.OwnedByWorkspace = stableUuid(block[0]+block[1]), block[1] - object.Spec.DisplayName, object.Status.Status = block[0], block[2] + object.Spec.DisplayName, object.Status.Action = block[0], block[2] objects = append(objects, object) } return objects diff --git a/internal/tfstate/lock.go b/internal/tfstate/lock.go index 3701a580..2cbd234f 100644 --- a/internal/tfstate/lock.go +++ b/internal/tfstate/lock.go @@ -57,7 +57,7 @@ func (l Lock) NoUnfinishedRun(ctx context.Context, raw *client.RawClient) error if err = json.Unmarshal(read, &run); err != nil { return err } - if run.Status == string(client.BuildingBlockStatusPending) || run.Status == string(client.BuildingBlockStatusInProgress) { + if run.IsRunning() { return fmt.Errorf("building block run %s, which holds the lock, is %s and still writes the state", runUuid, run.Status) } return nil diff --git a/skills/meshstack-cli/SKILL.md b/skills/meshstack-cli/SKILL.md index cd5e2702..8fdf40fa 100644 --- a/skills/meshstack-cli/SKILL.md +++ b/skills/meshstack-cli/SKILL.md @@ -409,7 +409,8 @@ jq -n '{apiVersion: "v2-preview", kind: "meshBuildingBlock", spec: { displayName: "my-project", buildingBlockDefinitionVersionRef: {kind: "meshBuildingBlockDefinitionVersion", uuid: ""}, targetRef: {kind: "meshWorkspace", name: ""}, - inputs: {name: {value: "my-project", valueType: "STRING"}}}}' | + inputs: {name: {value: "my-project", valueType: "STRING"}}, + parentBuildingBlockRefs: []}}' | meshstack api -X POST /api/meshobjects/meshbuildingblocks --workspace \ --api-version v2-preview --request-json - | jq -c '{uuid: .metadata.uuid, action: .status.action}'