From 271b4f35c3d52aa6bce73a757efde69688724b66 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Mon, 21 Sep 2026 16:23:50 +0200 Subject: [PATCH 1/5] fix: report whether meshStack redacted a definition or a version meshStack answers a request for a building block definition of another workspace with 200 instead of 403 (meshcloud/meshfed-release#10980), and redacts the response instead. Both status objects now carry the flag that says so, so the Terraform provider can keep its workaround for a definition it may only consume. CU-123ymg9utgq Co-Authored-By: Claude Fable 5.1 --- client/building_block_definition.go | 1 + client/building_block_definition_version.go | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/client/building_block_definition.go b/client/building_block_definition.go index f7ec893..06f1fb3 100644 --- a/client/building_block_definition.go +++ b/client/building_block_definition.go @@ -115,6 +115,7 @@ type MeshBuildingBlockDefinitionStatus struct { LatestVersionUuid string `json:"latestVersionUuid"` LatestReleasedVersion *int64 `json:"latestReleasedVersion"` LatestReleasedVersionUuid *string `json:"latestReleasedVersionUuid"` + RedactedForNonOwnerAccess bool `json:"redactedForNonOwnerAccess"` } type MeshBuildingBlockDefinition struct { diff --git a/client/building_block_definition_version.go b/client/building_block_definition_version.go index 98a6c77..417ed25 100644 --- a/client/building_block_definition_version.go +++ b/client/building_block_definition_version.go @@ -208,8 +208,9 @@ type MeshBuildingBlockDefinitionVersionSpec struct { } type MeshBuildingBlockDefinitionVersionStatus struct { - State MeshBuildingBlockDefinitionVersionState `json:"state" tfsdk:"state"` - UsageCount int64 `json:"usageCount" tfsdk:"usage_count"` + State MeshBuildingBlockDefinitionVersionState `json:"state" tfsdk:"state"` + UsageCount int64 `json:"usageCount" tfsdk:"usage_count"` + RedactedForNonOwnerAccess bool `json:"redactedForNonOwnerAccess" tfsdk:"-"` } type MeshBuildingBlockDefinitionVersion struct { From 782bc5024799030f70cd76a16edac8ad335eea3d Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Mon, 21 Sep 2026 16:23:50 +0200 Subject: [PATCH 2/5] fix: marshal an implementation without a variant to an error, not a panic A redacted version comes back from meshStack without an implementation variant and without a usage count. Marshalling it panicked inside the type inference and crashed the caller; MarshalJSON now returns the error, and the usage count is nullable. CU-123ymg9utgq Co-Authored-By: Claude Fable 5.1 --- client/building_block_definition_version.go | 2 +- ...block_definition_version_implementation.go | 30 +++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/client/building_block_definition_version.go b/client/building_block_definition_version.go index 417ed25..3a0e5f6 100644 --- a/client/building_block_definition_version.go +++ b/client/building_block_definition_version.go @@ -209,7 +209,7 @@ type MeshBuildingBlockDefinitionVersionSpec struct { type MeshBuildingBlockDefinitionVersionStatus struct { State MeshBuildingBlockDefinitionVersionState `json:"state" tfsdk:"state"` - UsageCount int64 `json:"usageCount" tfsdk:"usage_count"` + UsageCount *int64 `json:"usageCount,omitzero" tfsdk:"usage_count"` RedactedForNonOwnerAccess bool `json:"redactedForNonOwnerAccess" tfsdk:"-"` } diff --git a/client/building_block_definition_version_implementation.go b/client/building_block_definition_version_implementation.go index 7c2182d..30cf0d1 100644 --- a/client/building_block_definition_version_implementation.go +++ b/client/building_block_definition_version_implementation.go @@ -2,6 +2,7 @@ package client import ( "encoding/json/v2" + "errors" "fmt" "reflect" @@ -74,12 +75,23 @@ type MeshBuildingBlockDefinitionImplementation struct { Terraform *MeshBuildingBlockDefinitionTerraformImplementation `json:"terraform,omitzero" tfsdk:"terraform"` } -func (m MeshBuildingBlockDefinitionImplementation) InferTypeFromNonNilField() (result enum.Entry[MeshBuildingBlockImplementationType]) { +// InferTypeFromNonNilField panics when no variant is set. Callers hold a plan or state, where the schema +// guarantees exactly one variant; a response from the API goes through MarshalJSON, which reports the +// error instead. +func (m MeshBuildingBlockDefinitionImplementation) InferTypeFromNonNilField() enum.Entry[MeshBuildingBlockImplementationType] { + result, err := m.inferType() + if err != nil { + panic(err) + } + return result +} + +func (m MeshBuildingBlockDefinitionImplementation) inferType() (result enum.Entry[MeshBuildingBlockImplementationType], err error) { setResultIfNotNil := func(implType enum.Entry[MeshBuildingBlockImplementationType], v any) { // Manual implementation is an empty struct, so carefully check v for nilness using reflection! if !reflect.ValueOf(v).IsZero() { if len(result) > 0 && result != implType { - panic(fmt.Errorf("inferred implementation type %s but already set to %s", implType, result)) + err = fmt.Errorf("inferred implementation type %s but already set to %s", implType, result) } result = implType } @@ -89,17 +101,25 @@ func (m MeshBuildingBlockDefinitionImplementation) InferTypeFromNonNilField() (r setResultIfNotNil(MeshBuildingBlockImplementationTypeGithubWorkflows, m.GithubWorkflows) setResultIfNotNil(MeshBuildingBlockImplementationTypeGitlabPipeline, m.GitlabPipeline) setResultIfNotNil(MeshBuildingBlockImplementationTypeAzureDevOpsPipeline, m.AzureDevOpsPipeline) + if err != nil { + return "", err + } if len(result) == 0 { - panic("cannot infer implementation type") + // meshStack answers a workspace that may only consume a definition with versions that carry no implementation. + return "", errors.New("cannot infer implementation type: no implementation variant is set") } - return + return result, nil } func (m MeshBuildingBlockDefinitionImplementation) MarshalJSON() ([]byte, error) { type wrapped MeshBuildingBlockDefinitionImplementation w := wrapped(m) if len(w.Type) == 0 { - w.Type = m.InferTypeFromNonNilField() + inferred, err := m.inferType() + if err != nil { + return nil, err + } + w.Type = inferred } return json.Marshal(w, wireCompatibility) } From 84c01a8eea78ef80e2edf03ffb8fc51b7e2ec669 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Mon, 21 Sep 2026 16:23:51 +0200 Subject: [PATCH 3/5] refactor: keep the redaction flag on the definition status only meshStack marks the definition, and a caller that reads the mark never reaches the versions, so the flag on the version status had no reader. CU-123ymg9utgq Co-Authored-By: Claude Fable 5.1 --- client/building_block_definition_version.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/building_block_definition_version.go b/client/building_block_definition_version.go index 3a0e5f6..50952c5 100644 --- a/client/building_block_definition_version.go +++ b/client/building_block_definition_version.go @@ -208,9 +208,8 @@ type MeshBuildingBlockDefinitionVersionSpec struct { } type MeshBuildingBlockDefinitionVersionStatus struct { - State MeshBuildingBlockDefinitionVersionState `json:"state" tfsdk:"state"` - UsageCount *int64 `json:"usageCount,omitzero" tfsdk:"usage_count"` - RedactedForNonOwnerAccess bool `json:"redactedForNonOwnerAccess" tfsdk:"-"` + State MeshBuildingBlockDefinitionVersionState `json:"state" tfsdk:"state"` + UsageCount *int64 `json:"usageCount,omitzero" tfsdk:"usage_count"` } type MeshBuildingBlockDefinitionVersion struct { From 725652c5727a913441f39ccb5716662ffd52fa18 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Mon, 21 Sep 2026 16:23:52 +0200 Subject: [PATCH 4/5] fix: infer a variant type with an error instead of a panic The building block implementation and the integration config both derive their type from the one variant that is set. Both copies of that logic panicked when no variant or several variants were set, which user configuration and redacted API responses can both produce. A shared helper now returns an error, exposed as InferType on both types, and MarshalJSON reports it. The integration config only infers its type when none is set, because the built-in integrations come with a type but no variant. CU-123ymg9utgq Co-Authored-By: Claude Fable 5.1 --- ...block_definition_version_implementation.go | 46 +++++----------- client/integration_config.go | 37 ++++++------- client/variant_type.go | 40 ++++++++++++++ client/variant_type_test.go | 53 +++++++++++++++++++ 4 files changed, 124 insertions(+), 52 deletions(-) create mode 100644 client/variant_type.go create mode 100644 client/variant_type_test.go diff --git a/client/building_block_definition_version_implementation.go b/client/building_block_definition_version_implementation.go index 30cf0d1..bdb440b 100644 --- a/client/building_block_definition_version_implementation.go +++ b/client/building_block_definition_version_implementation.go @@ -2,9 +2,7 @@ package client import ( "encoding/json/v2" - "errors" "fmt" - "reflect" "github.com/meshcloud/meshstack-cli/client/types" "github.com/meshcloud/meshstack-cli/client/types/enum" @@ -75,38 +73,18 @@ type MeshBuildingBlockDefinitionImplementation struct { Terraform *MeshBuildingBlockDefinitionTerraformImplementation `json:"terraform,omitzero" tfsdk:"terraform"` } -// InferTypeFromNonNilField panics when no variant is set. Callers hold a plan or state, where the schema -// guarantees exactly one variant; a response from the API goes through MarshalJSON, which reports the -// error instead. -func (m MeshBuildingBlockDefinitionImplementation) InferTypeFromNonNilField() enum.Entry[MeshBuildingBlockImplementationType] { - result, err := m.inferType() +// InferType derives the implementation type from the one variant that is set. A version without any variant +// is what meshStack answers a workspace that may only consume the definition. +func (m MeshBuildingBlockDefinitionImplementation) InferType() (enum.Entry[MeshBuildingBlockImplementationType], error) { + result, err := inferVariantType( + variant(MeshBuildingBlockImplementationTypeManual, m.Manual), + variant(MeshBuildingBlockImplementationTypeTerraform, m.Terraform), + variant(MeshBuildingBlockImplementationTypeGithubWorkflows, m.GithubWorkflows), + variant(MeshBuildingBlockImplementationTypeGitlabPipeline, m.GitlabPipeline), + variant(MeshBuildingBlockImplementationTypeAzureDevOpsPipeline, m.AzureDevOpsPipeline), + ) if err != nil { - panic(err) - } - return result -} - -func (m MeshBuildingBlockDefinitionImplementation) inferType() (result enum.Entry[MeshBuildingBlockImplementationType], err error) { - setResultIfNotNil := func(implType enum.Entry[MeshBuildingBlockImplementationType], v any) { - // Manual implementation is an empty struct, so carefully check v for nilness using reflection! - if !reflect.ValueOf(v).IsZero() { - if len(result) > 0 && result != implType { - err = fmt.Errorf("inferred implementation type %s but already set to %s", implType, result) - } - result = implType - } - } - setResultIfNotNil(MeshBuildingBlockImplementationTypeManual, m.Manual) - setResultIfNotNil(MeshBuildingBlockImplementationTypeTerraform, m.Terraform) - setResultIfNotNil(MeshBuildingBlockImplementationTypeGithubWorkflows, m.GithubWorkflows) - setResultIfNotNil(MeshBuildingBlockImplementationTypeGitlabPipeline, m.GitlabPipeline) - setResultIfNotNil(MeshBuildingBlockImplementationTypeAzureDevOpsPipeline, m.AzureDevOpsPipeline) - if err != nil { - return "", err - } - if len(result) == 0 { - // meshStack answers a workspace that may only consume a definition with versions that carry no implementation. - return "", errors.New("cannot infer implementation type: no implementation variant is set") + return "", fmt.Errorf("cannot infer implementation type: %w", err) } return result, nil } @@ -115,7 +93,7 @@ func (m MeshBuildingBlockDefinitionImplementation) MarshalJSON() ([]byte, error) type wrapped MeshBuildingBlockDefinitionImplementation w := wrapped(m) if len(w.Type) == 0 { - inferred, err := m.inferType() + inferred, err := m.InferType() if err != nil { return nil, err } diff --git a/client/integration_config.go b/client/integration_config.go index 874553e..1f0d9a2 100644 --- a/client/integration_config.go +++ b/client/integration_config.go @@ -3,7 +3,6 @@ package client import ( "encoding/json/v2" "fmt" - "reflect" "github.com/meshcloud/meshstack-cli/client/types" "github.com/meshcloud/meshstack-cli/client/types/enum" @@ -57,30 +56,32 @@ type MeshIntegrationConfig struct { EntraId *MeshIntegrationEntraIdConfig `json:"entraid,omitzero" tfsdk:"entraid"` } -func (m MeshIntegrationConfig) InferTypeFromNonNilField() (result enum.Entry[MeshIntegrationConfigType]) { - setResultIfNotNil := func(implType enum.Entry[MeshIntegrationConfigType], v any) { - if !reflect.ValueOf(v).IsZero() { - if len(result) > 0 && result != implType { - panic(fmt.Errorf("inferred config type %s but already set to %s", implType, result)) - } - result = implType - } - } - setResultIfNotNil(MeshIntegrationConfigTypeGithub, m.Github) - setResultIfNotNil(MeshIntegrationConfigTypeGitlab, m.Gitlab) - setResultIfNotNil(MeshIntegrationConfigTypeAzureDevops, m.AzureDevops) - setResultIfNotNil(MeshIntegrationConfigTypeEntraId, m.EntraId) - if len(result) == 0 { - panic("cannot infer config type") +func (m MeshIntegrationConfig) InferType() (enum.Entry[MeshIntegrationConfigType], error) { + result, err := inferVariantType( + variant(MeshIntegrationConfigTypeGithub, m.Github), + variant(MeshIntegrationConfigTypeGitlab, m.Gitlab), + variant(MeshIntegrationConfigTypeAzureDevops, m.AzureDevops), + variant(MeshIntegrationConfigTypeEntraId, m.EntraId), + ) + if err != nil { + return "", fmt.Errorf("cannot infer integration config type: %w", err) } - return + return result, nil } func (m MeshIntegrationConfig) MarshalJSON() ([]byte, error) { // Using wrapped type avoids calling MarshalJSON recursively! type wrapped MeshIntegrationConfig w := wrapped(m) - w.Type = m.InferTypeFromNonNilField() + // Built-in integrations (replicator, metering) come with a type but no variant, so the type is only + // inferred when it is missing. + if len(w.Type) == 0 { + inferred, err := m.InferType() + if err != nil { + return nil, err + } + w.Type = inferred + } return json.Marshal(w, wireCompatibility) } diff --git a/client/variant_type.go b/client/variant_type.go new file mode 100644 index 0000000..7f7d9d8 --- /dev/null +++ b/client/variant_type.go @@ -0,0 +1,40 @@ +package client + +import ( + "errors" + "fmt" + "reflect" + + "github.com/meshcloud/meshstack-cli/client/types/enum" +) + +// variantCandidate pairs a variant's type with the field that holds it. +type variantCandidate[T ~string] struct { + Type enum.Entry[T] + Value any +} + +func variant[T ~string](typ enum.Entry[T], value any) variantCandidate[T] { + return variantCandidate[T]{Type: typ, Value: value} +} + +// inferVariantType returns the type of the one candidate whose field is set. None and several are errors, so +// both a request built from an incomplete plan and a response meshStack has redacted end in a diagnostic +// instead of a crash. +func inferVariantType[T ~string](candidates ...variantCandidate[T]) (enum.Entry[T], error) { + var result enum.Entry[T] + for _, candidate := range candidates { + // A variant may be a pointer to an empty struct, so nil-ness has to be checked by reflection. + if reflect.ValueOf(candidate.Value).IsZero() { + continue + } + if len(result) > 0 { + return "", fmt.Errorf("more than one variant is set: %s and %s", result, candidate.Type) + } + result = candidate.Type + } + if len(result) == 0 { + return "", errors.New("no variant is set") + } + return result, nil +} diff --git a/client/variant_type_test.go b/client/variant_type_test.go new file mode 100644 index 0000000..5f0088e --- /dev/null +++ b/client/variant_type_test.go @@ -0,0 +1,53 @@ +package client + +import ( + "encoding/json/v2" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMeshBuildingBlockDefinitionImplementation_InferType(t *testing.T) { + t.Run("an empty manual struct counts as set", func(t *testing.T) { + got, err := MeshBuildingBlockDefinitionImplementation{Manual: &MeshBuildingBlockDefinitionManualImplementation{}}.InferType() + require.NoError(t, err) + assert.Equal(t, MeshBuildingBlockImplementationTypeManual, got) + }) + + t.Run("no variant", func(t *testing.T) { + _, err := MeshBuildingBlockDefinitionImplementation{}.InferType() + require.EqualError(t, err, "cannot infer implementation type: no variant is set") + }) + + t.Run("several variants", func(t *testing.T) { + _, err := MeshBuildingBlockDefinitionImplementation{ + Terraform: &MeshBuildingBlockDefinitionTerraformImplementation{}, + GitlabPipeline: &MeshBuildingBlockDefinitionGitLabPipelineImplementation{}, + }.InferType() + require.EqualError(t, err, "cannot infer implementation type: more than one variant is set: terraform and gitlabPipeline") + }) +} + +func TestMeshIntegrationConfig_InferType(t *testing.T) { + t.Run("one variant", func(t *testing.T) { + got, err := MeshIntegrationConfig{Gitlab: &MeshIntegrationGitlabConfig{}}.InferType() + require.NoError(t, err) + assert.Equal(t, MeshIntegrationConfigTypeGitlab, got) + }) + + t.Run("no variant", func(t *testing.T) { + _, err := MeshIntegrationConfig{}.InferType() + require.EqualError(t, err, "cannot infer integration config type: no variant is set") + }) + + t.Run("several variants", func(t *testing.T) { + _, err := MeshIntegrationConfig{Github: &MeshIntegrationGithubConfig{}, EntraId: &MeshIntegrationEntraIdConfig{}}.InferType() + require.EqualError(t, err, "cannot infer integration config type: more than one variant is set: github and entraid") + }) + + t.Run("marshalling without a variant reports the error", func(t *testing.T) { + _, err := json.Marshal(MeshIntegrationConfig{}) + require.ErrorContains(t, err, "cannot infer integration config type") + }) +} From 0cc1fca16d7fe7c1584021fe616db6e72be73b10 Mon Sep 17 00:00:00 2001 From: Fabian Muscariello Date: Tue, 22 Sep 2026 10:32:42 +0200 Subject: [PATCH 5/5] refactor: avoid usage of `reflect` --- ...ding_block_definition_version_implementation.go | 10 +++++----- client/integration_config.go | 8 ++++---- client/variant_type.go | 14 ++++---------- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/client/building_block_definition_version_implementation.go b/client/building_block_definition_version_implementation.go index bdb440b..8ce53da 100644 --- a/client/building_block_definition_version_implementation.go +++ b/client/building_block_definition_version_implementation.go @@ -77,11 +77,11 @@ type MeshBuildingBlockDefinitionImplementation struct { // is what meshStack answers a workspace that may only consume the definition. func (m MeshBuildingBlockDefinitionImplementation) InferType() (enum.Entry[MeshBuildingBlockImplementationType], error) { result, err := inferVariantType( - variant(MeshBuildingBlockImplementationTypeManual, m.Manual), - variant(MeshBuildingBlockImplementationTypeTerraform, m.Terraform), - variant(MeshBuildingBlockImplementationTypeGithubWorkflows, m.GithubWorkflows), - variant(MeshBuildingBlockImplementationTypeGitlabPipeline, m.GitlabPipeline), - variant(MeshBuildingBlockImplementationTypeAzureDevOpsPipeline, m.AzureDevOpsPipeline), + variant(MeshBuildingBlockImplementationTypeManual, m.Manual != nil), + variant(MeshBuildingBlockImplementationTypeTerraform, m.Terraform != nil), + variant(MeshBuildingBlockImplementationTypeGithubWorkflows, m.GithubWorkflows != nil), + variant(MeshBuildingBlockImplementationTypeGitlabPipeline, m.GitlabPipeline != nil), + variant(MeshBuildingBlockImplementationTypeAzureDevOpsPipeline, m.AzureDevOpsPipeline != nil), ) if err != nil { return "", fmt.Errorf("cannot infer implementation type: %w", err) diff --git a/client/integration_config.go b/client/integration_config.go index 1f0d9a2..9735a7d 100644 --- a/client/integration_config.go +++ b/client/integration_config.go @@ -58,10 +58,10 @@ type MeshIntegrationConfig struct { func (m MeshIntegrationConfig) InferType() (enum.Entry[MeshIntegrationConfigType], error) { result, err := inferVariantType( - variant(MeshIntegrationConfigTypeGithub, m.Github), - variant(MeshIntegrationConfigTypeGitlab, m.Gitlab), - variant(MeshIntegrationConfigTypeAzureDevops, m.AzureDevops), - variant(MeshIntegrationConfigTypeEntraId, m.EntraId), + variant(MeshIntegrationConfigTypeGithub, m.Github != nil), + variant(MeshIntegrationConfigTypeGitlab, m.Gitlab != nil), + variant(MeshIntegrationConfigTypeAzureDevops, m.AzureDevops != nil), + variant(MeshIntegrationConfigTypeEntraId, m.EntraId != nil), ) if err != nil { return "", fmt.Errorf("cannot infer integration config type: %w", err) diff --git a/client/variant_type.go b/client/variant_type.go index 7f7d9d8..994af1e 100644 --- a/client/variant_type.go +++ b/client/variant_type.go @@ -3,29 +3,23 @@ package client import ( "errors" "fmt" - "reflect" "github.com/meshcloud/meshstack-cli/client/types/enum" ) -// variantCandidate pairs a variant's type with the field that holds it. type variantCandidate[T ~string] struct { Type enum.Entry[T] - Value any + IsSet bool } -func variant[T ~string](typ enum.Entry[T], value any) variantCandidate[T] { - return variantCandidate[T]{Type: typ, Value: value} +func variant[T ~string](typ enum.Entry[T], isSet bool) variantCandidate[T] { + return variantCandidate[T]{Type: typ, IsSet: isSet} } -// inferVariantType returns the type of the one candidate whose field is set. None and several are errors, so -// both a request built from an incomplete plan and a response meshStack has redacted end in a diagnostic -// instead of a crash. func inferVariantType[T ~string](candidates ...variantCandidate[T]) (enum.Entry[T], error) { var result enum.Entry[T] for _, candidate := range candidates { - // A variant may be a pointer to an empty struct, so nil-ness has to be checked by reflection. - if reflect.ValueOf(candidate.Value).IsZero() { + if !candidate.IsSet { continue } if len(result) > 0 {