Files
Jan KoronaandGitHub 8bbee0176d [chore][cmd/mdatagen] Add exported_configs for reusable config schemas (#15304)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue.
Ex. Adding a feature - Explain what this achieves.-->
#### Description

Adds `exported_configs` as the metadata.yaml field for config schemas
that should be exported from a component or package.

This keeps source metadata from using `config.$defs` directly while
still emitting JSON Schema `$defs` in generated schema output. The
loader maps `exported_configs` into internal schema defs for resolution,
and mdatagen does the same before config generation.

Also updates generated/internal schema handling so mdatagen-injected
defs are marked internal-only and removed after resolution.
User-authored exported defs are preserved, while generated
telemetry/resource/event helper defs no longer inflate generated
component config schemas or duplicate Go types.

<!-- Issue number if applicable -->
#### Link to tracking issue
Fixes #15304

<!--Describe what testing was performed and which tests were added.-->
#### Testing

- Sample components amended with new functionality
- Unit test coverage
2026-05-18 18:27:13 +00:00

1726 lines
46 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package schemagen
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestResolver_ResolveSchema_BasicMetadata(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/receiver/otlpreceiver",
class: "receiver",
name: "otlp",
loader: NewLoader(""),
}
src := &ConfigMetadata{
Description: "OTLP receiver configuration",
Type: "object",
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.Equal(t, schemaVersion, result.Schema)
require.Equal(t, "go.opentelemetry.io/collector/receiver/otlpreceiver", result.ID)
require.Equal(t, "receiver/otlp", result.Title)
require.Equal(t, "OTLP receiver configuration", result.Description)
require.Equal(t, "object", result.Type)
}
func TestResolver_ResolveSchema_InternalReference(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: NewLoader(""),
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"config": {
Ref: "target_type",
},
},
Defs: map[string]*ConfigMetadata{
"target_type": {
Type: "string",
Description: "Target type description",
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.Equal(t, "object", result.Type)
require.NotNil(t, result.Properties["config"])
require.Equal(t, "string", result.Properties["config"].Type)
require.Equal(t, "Target type description", result.Properties["config"].Description)
}
func TestResolver_ResolveSchema_DefsOnlyPreservesDefs(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/pkg",
class: "pkg",
name: "testpkg",
loader: NewLoader(""),
}
src := &ConfigMetadata{
Defs: map[string]*ConfigMetadata{
"sample_config": {
Type: "object",
Properties: map[string]*ConfigMetadata{
"endpoint": {Type: "string"},
},
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.Empty(t, result.Type)
require.Empty(t, result.Properties)
require.Contains(t, result.Defs, "sample_config")
require.Equal(t, "object", result.Defs["sample_config"].Type)
require.Contains(t, result.Defs["sample_config"].Properties, "endpoint")
}
func TestResolver_ResolveSchema_PreservesInlineDefsWithProperties(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: NewLoader(""),
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"endpoint": {Type: "string"},
},
Defs: map[string]*ConfigMetadata{
"sample_config": {
Type: "object",
Properties: map[string]*ConfigMetadata{
"host_name": {Type: "string"},
},
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.Contains(t, result.Properties, "endpoint")
require.Contains(t, result.Defs, "sample_config")
require.Equal(t, "object", result.Defs["sample_config"].Type)
require.Contains(t, result.Defs["sample_config"].Properties, "host_name")
}
func TestResolver_ResolveSchema_DropsInternalOnlyDefs(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: NewLoader(""),
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"endpoint": {Type: "string"},
},
Defs: map[string]*ConfigMetadata{
"metrics_builder_config": {
Type: "object",
InternalOnly: true,
Properties: map[string]*ConfigMetadata{
"metrics": {Type: "object"},
},
},
"sample_config": {
Type: "object",
Properties: map[string]*ConfigMetadata{
"host_name": {Type: "string"},
},
},
"exported_metrics_config": {
Ref: "metrics_builder_config",
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.Contains(t, result.Properties, "endpoint")
require.NotContains(t, result.Defs, "metrics_builder_config")
require.Contains(t, result.Defs, "sample_config")
require.Contains(t, result.Defs, "exported_metrics_config")
require.Contains(t, result.Defs["exported_metrics_config"].Properties, "metrics")
}
func TestResolver_ResolveSchema_UnknownInternalReference(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: NewLoader(""),
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"config": {
Ref: "unknown_type",
},
},
}
// Should use "any" type because the internal reference doesn't exist
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.Empty(t, result.Properties["config"].Type)
}
func TestResolver_ResolveSchema_AliasChain_InternalToExternal(t *testing.T) {
externalSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"controller_config": {
Type: "object",
Properties: map[string]*ConfigMetadata{
"timeout": {
Type: "string",
GoType: "time.Duration",
},
},
},
},
}
ml := &mockLoader{
schemas: map[string]*ConfigMetadata{
"go.opentelemetry.io/collector/scraper/scraperhelper.controller_config": externalSchema,
},
}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/cmd/mdatagen/internal/samplescraper",
class: "scraper",
name: "sample",
loader: ml,
}
src := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
// alias: internal name → external type
"helper": {
Ref: "go.opentelemetry.io/collector/scraper/scraperhelper.controller_config",
},
},
Properties: map[string]*ConfigMetadata{
"controller_config": {
Ref: "helper",
Embed: true,
Default: map[string]any{"timeout": "30s"},
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.Len(t, result.AllOf, 1)
embedded := result.AllOf[0]
require.Equal(t, "go.opentelemetry.io/collector/scraper/scraperhelper.controller_config", embedded.ResolvedFrom)
require.NotNil(t, embedded.Properties["timeout"])
require.Equal(t, "time.Duration", embedded.Properties["timeout"].GoType)
require.Equal(t, map[string]any{"timeout": "30s"}, embedded.Default)
}
func TestResolver_ResolveSchema_AliasChain_PreservesCustomExtensions(t *testing.T) {
externalSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"base": {
Type: "object",
Properties: map[string]*ConfigMetadata{
"level": {Type: "integer"},
},
},
},
}
ml := &mockLoader{
schemas: map[string]*ConfigMetadata{
"go.opentelemetry.io/collector/config/configbase.base": externalSchema,
},
}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: ml,
}
src := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"alias": {Ref: "go.opentelemetry.io/collector/config/configbase.base"},
},
Properties: map[string]*ConfigMetadata{
"cfg": {
Ref: "alias",
Description: "overridden description",
Default: map[string]any{"level": 5},
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
cfg := result.Properties["cfg"]
require.NotNil(t, cfg)
require.Equal(t, "go.opentelemetry.io/collector/config/configbase.base", cfg.ResolvedFrom)
require.Equal(t, "overridden description", cfg.Description)
require.Equal(t, map[string]any{"level": 5}, cfg.Default)
require.NotNil(t, cfg.Properties["level"])
}
func TestResolver_ResolveSchema_InternalAliasChain(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: &mockLoader{schemas: map[string]*ConfigMetadata{}},
}
src := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"alias": {Ref: "base"},
"base": {
Type: "object",
Description: "Base configuration",
Properties: map[string]*ConfigMetadata{
"endpoint": {Type: "string"},
},
},
},
Properties: map[string]*ConfigMetadata{
"cfg": {
Ref: "alias",
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
cfg := result.Properties["cfg"]
require.NotNil(t, cfg)
require.Equal(t, "base", cfg.ResolvedFrom)
require.Equal(t, "object", cfg.Type)
require.Equal(t, "Base configuration", cfg.Description)
require.Contains(t, cfg.Properties, "endpoint")
}
func TestResolver_ResolveSchema_DefsOnlyLocalAliasWithSameDefName(t *testing.T) {
controllerSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"controller_config": {
Type: "object",
Description: "Controller configuration",
Properties: map[string]*ConfigMetadata{
"collection_interval": {Type: "string"},
},
},
},
}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/scraper/scraperhelper",
class: "pkg",
name: "scraperhelper",
loader: &mockLoader{schemas: map[string]*ConfigMetadata{
"./internal/controller.controller_config": controllerSchema,
}},
}
src := &ConfigMetadata{
Defs: map[string]*ConfigMetadata{
"controller_config": {
Ref: "./internal/controller.controller_config",
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
controller := result.Defs["controller_config"]
require.NotNil(t, controller)
require.Equal(t, "./internal/controller.controller_config", controller.ResolvedFrom)
require.Equal(t, "object", controller.Type)
require.Equal(t, "Controller configuration", controller.Description)
require.Contains(t, controller.Properties, "collection_interval")
}
func TestResolver_ResolveSchema_ExternalRefDoesNotUseRootDefWithSameName(t *testing.T) {
externalSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"shared_config": {
Type: "object",
Description: "External shared config",
Properties: map[string]*ConfigMetadata{
"external": {Type: "string"},
},
},
},
}
ml := &mockLoader{
schemas: map[string]*ConfigMetadata{
"go.opentelemetry.io/collector/config/shared.shared_config": externalSchema,
},
}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: ml,
}
src := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"shared_config": {
Type: "object",
Description: "Local shared config",
Properties: map[string]*ConfigMetadata{
"local": {Type: "string"},
},
},
},
Properties: map[string]*ConfigMetadata{
"cfg": {
Ref: "go.opentelemetry.io/collector/config/shared.shared_config",
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
cfg := result.Properties["cfg"]
require.NotNil(t, cfg)
require.Equal(t, "go.opentelemetry.io/collector/config/shared.shared_config", cfg.ResolvedFrom)
require.Equal(t, "External shared config", cfg.Description)
require.Contains(t, cfg.Properties, "external")
require.NotContains(t, cfg.Properties, "local")
}
func TestResolver_ResolveSchema_NestedStructures(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: NewLoader(""),
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"nested": {
Type: "object",
Properties: map[string]*ConfigMetadata{
"field1": {
Type: "string",
},
"field2": {
Type: "integer",
},
},
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.Equal(t, "object", result.Type)
require.NotNil(t, result.Properties["nested"])
require.Equal(t, "object", result.Properties["nested"].Type)
require.NotNil(t, result.Properties["nested"].Properties["field1"])
require.Equal(t, "string", result.Properties["nested"].Properties["field1"].Type)
require.NotNil(t, result.Properties["nested"].Properties["field2"])
require.Equal(t, "integer", result.Properties["nested"].Properties["field2"].Type)
}
func TestResolver_ResolveSchema_AllOf(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: NewLoader(""),
}
src := &ConfigMetadata{
Type: "object",
AllOf: []*ConfigMetadata{
{
Type: "object",
Properties: map[string]*ConfigMetadata{
"field1": {Type: "string"},
},
},
{
Type: "object",
Properties: map[string]*ConfigMetadata{
"field2": {Type: "integer"},
},
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.Len(t, result.AllOf, 2)
require.NotNil(t, result.AllOf[0].Properties["field1"])
require.NotNil(t, result.AllOf[1].Properties["field2"])
}
func TestResolver_ResolveSchema_EmbeddedProperties(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: NewLoader(""),
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"anonymous_config": {
Ref: "anonymous_config",
Embed: true,
GoStruct: GoStructConfig{Anonymous: true},
},
"named_config": {
Ref: "named_config",
Embed: true,
},
"regular": {
Type: "string",
},
},
Defs: map[string]*ConfigMetadata{
"anonymous_config": {Type: "object"},
"named_config": {Type: "object"},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.Len(t, result.Properties, 1)
require.Contains(t, result.Properties, "regular")
require.Len(t, result.AllOf, 2)
named := findSchema(t, result.AllOf, func(md *ConfigMetadata) bool {
return md.ResolvedFrom == "named_config"
})
require.NotNil(t, named)
require.True(t, named.Embed)
require.Equal(t, "named_config", named.EmbeddedName)
anonymous := findSchema(t, result.AllOf, func(md *ConfigMetadata) bool {
return md.ResolvedFrom == "anonymous_config"
})
require.NotNil(t, anonymous)
require.True(t, anonymous.Embed)
require.True(t, anonymous.GoStruct.Anonymous)
require.Empty(t, anonymous.EmbeddedName)
}
func TestResolver_ResolveSchema_EmbeddedReferencePreservesExtensions(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: NewLoader(""),
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"base": {
Ref: "base_config",
Embed: true,
GoStruct: GoStructConfig{Anonymous: true},
Description: "Embedded base config",
},
},
Defs: map[string]*ConfigMetadata{
"base_config": {
Type: "object",
Properties: map[string]*ConfigMetadata{
"endpoint": {Type: "string"},
},
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.Empty(t, result.Properties)
require.Len(t, result.AllOf, 1)
embedded := result.AllOf[0]
require.Equal(t, "base_config", embedded.ResolvedFrom)
require.True(t, embedded.Embed)
require.True(t, embedded.GoStruct.Anonymous)
require.Empty(t, embedded.EmbeddedName)
require.Equal(t, "Embedded base config", embedded.Description)
require.Contains(t, embedded.Properties, "endpoint")
}
func TestResolver_ResolveSchema_ArrayItems(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: NewLoader(""),
}
src := &ConfigMetadata{
Type: "array",
Items: &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"name": {Type: "string"},
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.Equal(t, "array", result.Type)
require.NotNil(t, result.Items)
require.Equal(t, "object", result.Items.Type)
require.NotNil(t, result.Items.Properties["name"])
}
func TestResolver_LoadExternalRef_Success(t *testing.T) {
ml := &mockLoader{
schemas: map[string]*ConfigMetadata{
"go.opentelemetry.io/collector/scraper/scraperhelper.controller_config": {
Type: "object",
Defs: map[string]*ConfigMetadata{
"controller_config": {
Type: "object",
Properties: map[string]*ConfigMetadata{
"timeout": {
Type: "string",
},
},
},
},
},
},
}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: ml,
}
ref := NewRef("go.opentelemetry.io/collector/scraper/scraperhelper.controller_config")
result, err := resolver.loadExternalRef(ref)
require.NoError(t, err)
require.Equal(t, "object", result.Type)
require.NotNil(t, result.Properties["timeout"])
require.Equal(t, "string", result.Properties["timeout"].Type)
}
func TestResolver_LoadExternalRef_InvalidPath(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: NewLoader(""),
}
ref := NewRef("invalid/path/without/namespace")
result, err := resolver.loadExternalRef(ref)
require.Error(t, err)
require.Nil(t, result)
}
func TestResolver_LoadExternalRef_TypeNotFound(t *testing.T) {
ml := &mockLoader{
schemas: map[string]*ConfigMetadata{
"go.opentelemetry.io/collector/scraper/scraperhelper.controller_config": {
Type: "object",
Defs: map[string]*ConfigMetadata{
"other_type": {
Type: "string",
},
},
},
},
}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: ml,
}
ref := NewRef("go.opentelemetry.io/collector/scraper/scraperhelper.controller_config")
result, err := resolver.loadExternalRef(ref)
require.Error(t, err)
require.Contains(t, err.Error(), "type \"controller_config\" not found")
require.Nil(t, result)
}
func TestResolver_IsExternalRef(t *testing.T) {
tests := []struct {
name string
ref string
expected bool
}{
{
name: "collector external ref - known namespace",
ref: "go.opentelemetry.io/collector/scraper/scraperhelper.controller_config",
expected: true,
},
{
name: "contrib external ref - known namespace",
ref: "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/mysqlreceiver.config",
expected: true,
},
{
name: "relative path - local not external",
ref: "./internal/metadata.metrics_builder_config",
expected: false,
},
{
name: "internal ref - simple name",
ref: "target_type",
expected: false,
},
{
name: "unsupported namespace - still external (not internal/local)",
ref: "github.com/example/custom.config",
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := NewRef(tt.ref)
result := r.IsExternal()
require.Equal(t, tt.expected, result)
})
}
}
func TestResolver_ResolveSchema_ExternalReference_Integration(t *testing.T) {
// Use mockLoader instead of real file loading to avoid repo root dependency
confighttpSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"client_config": {
Type: "object",
Properties: map[string]*ConfigMetadata{
"endpoint": {
Type: "string",
Description: "HTTP endpoint",
},
"timeout": {
Type: "string",
Description: "Request timeout",
},
},
},
},
}
ml := &mockLoader{
schemas: map[string]*ConfigMetadata{
"go.opentelemetry.io/collector/config/confighttp.client_config": confighttpSchema,
},
}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/receiver/otlpreceiver",
class: "receiver",
name: "otlp",
loader: ml,
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"http": {
Ref: "go.opentelemetry.io/collector/config/confighttp.client_config",
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.Equal(t, "object", result.Type)
require.NotNil(t, result.Properties["http"])
require.Equal(t, "object", result.Properties["http"].Type)
require.NotNil(t, result.Properties["http"].Properties["endpoint"])
require.Equal(t, "HTTP endpoint", result.Properties["http"].Properties["endpoint"].Description)
require.NotNil(t, result.Properties["http"].Properties["timeout"])
require.Equal(t, "Request timeout", result.Properties["http"].Properties["timeout"].Description)
}
func TestResolver_ResolveSchema_DurationFormat(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: NewLoader(""),
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"timeout": {
Type: "string",
Format: "duration",
Description: "Request timeout",
},
"interval": {
Type: "string",
Format: "duration",
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
// Check timeout field - format should be cleared, GoType and Pattern set
require.NotNil(t, result.Properties["timeout"])
require.Equal(t, "string", result.Properties["timeout"].Type)
require.Empty(t, result.Properties["timeout"].Format, "format should be cleared")
require.Equal(t, "time.Duration", result.Properties["timeout"].GoType)
require.Equal(t, `^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$`, result.Properties["timeout"].Pattern)
require.Equal(t, "Request timeout", result.Properties["timeout"].Description)
// Check interval field - format should be cleared, GoType and Pattern set
require.NotNil(t, result.Properties["interval"])
require.Equal(t, "string", result.Properties["interval"].Type)
require.Empty(t, result.Properties["interval"].Format)
require.Equal(t, "time.Duration", result.Properties["interval"].GoType)
require.Equal(t, `^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$`, result.Properties["interval"].Pattern)
}
// mockLoader is a test helper that returns pre-configured schemas keyed by cache key.
type mockLoader struct {
schemas map[string]*ConfigMetadata
}
func findSchema(t *testing.T, schemas []*ConfigMetadata, match func(*ConfigMetadata) bool) *ConfigMetadata {
t.Helper()
for _, md := range schemas {
if match(md) {
return md
}
}
require.FailNow(t, "schema not found")
return nil
}
func (m *mockLoader) Load(ref Ref) (*ConfigMetadata, error) {
cacheKey := ref.CacheKey()
if md, ok := m.schemas[cacheKey]; ok {
return md, nil
}
return nil, fmt.Errorf("schema not found for ref: %s", cacheKey)
}
func TestResolver_ResolveSchema_OriginConvertsLocalRefToExternal(t *testing.T) {
// confighttp schema contains a local absolute ref to /config/configauth.config
// When loaded as an external ref from the collector namespace, the local ref
// should be converted to go.opentelemetry.io/collector/config/configauth.config
configauthSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"config": {
Type: "object",
Description: "Auth configuration",
Properties: map[string]*ConfigMetadata{
"token": {Type: "string"},
},
},
},
}
confighttpSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"client_config": {
Type: "object",
Properties: map[string]*ConfigMetadata{
"endpoint": {Type: "string"},
"auth": {
// This is the key: a local absolute ref inside an externally-loaded schema
Ref: "/config/configauth.config",
},
},
},
},
}
ml := &mockLoader{
schemas: map[string]*ConfigMetadata{
// When loading the reference to confighttp.client_config, we get the whole schema with all defs
"go.opentelemetry.io/collector/config/confighttp.client_config": confighttpSchema,
// When resolving the local ref /config/configauth.config -> go.opentelemetry.io/collector/config/configauth.config
"go.opentelemetry.io/collector/config/configauth.config": configauthSchema,
},
}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/receiver/otlpreceiver",
class: "receiver",
name: "otlp",
loader: ml,
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"http": {
Ref: "go.opentelemetry.io/collector/config/confighttp.client_config",
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.NotNil(t, result.Properties["http"])
require.Equal(t, "object", result.Properties["http"].Type)
require.NotNil(t, result.Properties["http"].Properties["endpoint"])
require.Equal(t, "string", result.Properties["http"].Properties["endpoint"].Type)
// The auth property should have been resolved through origin-aware conversion
require.NotNil(t, result.Properties["http"].Properties["auth"])
require.Equal(t, "object", result.Properties["http"].Properties["auth"].Type)
require.Equal(t, "Auth configuration", result.Properties["http"].Properties["auth"].Description)
require.NotNil(t, result.Properties["http"].Properties["auth"].Properties["token"])
}
func TestResolver_ResolveSchema_LocalRefWithOriginConversion(t *testing.T) {
// When a local ref is encountered in an externally-loaded schema, it should be converted
// using the origin namespace
configauthSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"config": {
Type: "object",
Description: "Auth config",
},
},
}
confighttpSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"client_config": {
Type: "object",
Properties: map[string]*ConfigMetadata{
"auth": {
// This is a local absolute ref inside an externally-loaded schema
Ref: "/config/configauth.config",
},
},
},
},
}
ml := &mockLoader{
schemas: map[string]*ConfigMetadata{
"go.opentelemetry.io/collector/config/confighttp.client_config": confighttpSchema,
// After origin conversion, /config/configauth.config becomes:
"go.opentelemetry.io/collector/config/configauth.config": configauthSchema,
},
}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/receiver/otlpreceiver",
class: "receiver",
name: "otlp",
loader: ml,
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"http": {
Ref: "go.opentelemetry.io/collector/config/confighttp.client_config",
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.NotNil(t, result.Properties["http"])
require.Equal(t, "object", result.Properties["http"].Type)
// auth should be resolved through origin-aware conversion
require.NotNil(t, result.Properties["http"].Properties["auth"])
require.Equal(t, "object", result.Properties["http"].Properties["auth"].Type)
require.Equal(t, "Auth config", result.Properties["http"].Properties["auth"].Description)
}
func TestResolver_ResolveSchema_NestedOriginPropagation(t *testing.T) {
// Schema A (remote) → local ref → Schema B (also remote) → local ref → Schema C
// Verify the origin propagates through all levels.
schemaC := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"tls_config": {
Type: "object",
Description: "TLS configuration",
},
},
}
schemaB := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"auth_config": {
Type: "object",
Properties: map[string]*ConfigMetadata{
"tls": {
// Nested local ref — should also be converted using origin
Ref: "/config/configtls.tls_config",
},
},
},
},
}
schemaA := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"client_config": {
Type: "object",
Properties: map[string]*ConfigMetadata{
"auth": {
Ref: "/config/configauth.auth_config",
},
},
},
},
}
ml := &mockLoader{
schemas: map[string]*ConfigMetadata{
"go.opentelemetry.io/collector/config/confighttp.client_config": schemaA,
"go.opentelemetry.io/collector/config/configauth.auth_config": schemaB,
"go.opentelemetry.io/collector/config/configtls.tls_config": schemaC,
},
}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/receiver/otlpreceiver",
class: "receiver",
name: "otlp",
loader: ml,
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"http": {
Ref: "go.opentelemetry.io/collector/config/confighttp.client_config",
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.NotNil(t, result.Properties["http"])
// auth should be resolved through origin-aware conversion
auth := result.Properties["http"].Properties["auth"]
require.NotNil(t, auth)
require.Equal(t, "object", auth.Type)
// tls inside auth should also be resolved through origin propagation
tls := auth.Properties["tls"]
require.NotNil(t, tls)
require.Equal(t, "object", tls.Type)
require.Equal(t, "TLS configuration", tls.Description)
}
func TestResolver_ResolveSchema_RelativeRefWithOrigin(t *testing.T) {
metadataSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"metrics_config": {
Type: "object",
Description: "Metrics configuration",
},
},
}
confighttpSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"client_config": {
Type: "object",
Properties: map[string]*ConfigMetadata{
"metrics": {
Ref: "./internal/metadata.metrics_config",
},
},
},
},
}
ml := &mockLoader{
schemas: map[string]*ConfigMetadata{
"go.opentelemetry.io/collector/config/confighttp.client_config": confighttpSchema,
"go.opentelemetry.io/collector/config/confighttp/internal/metadata.metrics_config": metadataSchema,
},
}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/receiver/otlpreceiver",
class: "receiver",
name: "otlp",
loader: ml,
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"http": {
Ref: "go.opentelemetry.io/collector/config/confighttp.client_config",
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.NotNil(t, result.Properties["http"])
metrics := result.Properties["http"].Properties["metrics"]
require.NotNil(t, metrics)
require.Equal(t, "object", metrics.Type)
require.Equal(t, "Metrics configuration", metrics.Description)
}
func TestResolver_ResolveSchema_ParentRelativeRefWithOrigin(t *testing.T) {
configtlsSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"tls_config": {
Type: "object",
Description: "TLS settings",
},
},
}
confighttpSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"client_config": {
Type: "object",
Properties: map[string]*ConfigMetadata{
"tls": {
Ref: "../configtls.tls_config",
},
},
},
},
}
ml := &mockLoader{
schemas: map[string]*ConfigMetadata{
"go.opentelemetry.io/collector/config/confighttp.client_config": confighttpSchema,
"go.opentelemetry.io/collector/config/configtls.tls_config": configtlsSchema,
},
}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/receiver/otlpreceiver",
class: "receiver",
name: "otlp",
loader: ml,
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"http": {
Ref: "go.opentelemetry.io/collector/config/confighttp.client_config",
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.NotNil(t, result.Properties["http"])
tls := result.Properties["http"].Properties["tls"]
require.NotNil(t, tls)
require.Equal(t, "object", tls.Type)
require.Equal(t, "TLS settings", tls.Description)
}
func TestNewResolver(t *testing.T) {
dir := t.TempDir()
r := NewResolver("go.opentelemetry.io/collector/receiver/otlp", "receiver", "otlp", dir)
require.NotNil(t, r)
require.Equal(t, "go.opentelemetry.io/collector/receiver/otlp", r.pkgID)
require.Equal(t, "receiver", r.class)
require.Equal(t, "otlp", r.name)
require.NotNil(t, r.loader)
}
func TestResolver_ResolveSchema_UnknownNamespaceFallback(t *testing.T) {
// An external ref with an unsupported namespace should fall back to "any" type
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: NewLoader(""),
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"custom": {
Ref: "github.com/example/custom.config",
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.NotNil(t, result.Properties["custom"])
require.Equal(t, "github.com/example/custom.config", result.Properties["custom"].GoType)
require.Contains(t, result.Properties["custom"].Comment, "any")
}
func TestResolver_ResolveSchema_LoaderError(t *testing.T) {
ml := &mockLoader{schemas: map[string]*ConfigMetadata{}}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: ml,
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"http": {
Ref: "go.opentelemetry.io/collector/config/confighttp.client_config",
},
},
}
result, err := resolver.ResolveSchema(src)
require.Error(t, err)
require.Nil(t, result)
}
func TestResolver_ResolveRef_InvalidRefFormat(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: NewLoader(""),
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"bad": {
Ref: "/",
},
},
}
_, err := resolver.ResolveSchema(src)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid reference format")
}
func TestResolver_LoadExternalRef_NilResult(t *testing.T) {
ml := &nilResultLoader{}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: ml,
}
ref := NewRef("go.opentelemetry.io/collector/config/confighttp.client_config")
result, err := resolver.loadExternalRef(ref)
require.Error(t, err)
require.Contains(t, err.Error(), "no loader could resolve external reference")
require.Nil(t, result)
}
// nilResultLoader returns (nil, nil) for any ref.
type nilResultLoader struct{}
func (n *nilResultLoader) Load(_ Ref) (*ConfigMetadata, error) { return nil, nil }
func TestResolver_LoadExternalRef_InternalResolutionError(t *testing.T) {
brokenSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"config": {
Type: "object",
Properties: map[string]*ConfigMetadata{
"field": {
Ref: "/",
},
},
},
},
}
ml := &mockLoader{
schemas: map[string]*ConfigMetadata{
"go.opentelemetry.io/collector/config/confighttp.config": brokenSchema,
},
}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: ml,
}
ref := NewRef("go.opentelemetry.io/collector/config/confighttp.config")
result, err := resolver.loadExternalRef(ref)
require.Error(t, err)
require.Contains(t, err.Error(), "failed to resolve internal references in external schema")
require.Nil(t, result)
}
func TestResolver_ResolveSchema_LocalRef(t *testing.T) {
localSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"target": {
Type: "object",
Description: "Local target",
},
},
}
ml := &mockLoader{
schemas: map[string]*ConfigMetadata{
"/config/localtype.target": localSchema,
},
}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: ml,
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"local": {
Ref: "/config/localtype.target",
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.NotNil(t, result.Properties["local"])
require.Equal(t, "object", result.Properties["local"].Type)
require.Equal(t, "Local target", result.Properties["local"].Description)
}
func TestResolver_ResolveSchema_LocalRefUsesRootDef(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: &mockLoader{schemas: map[string]*ConfigMetadata{}},
}
src := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"resource_attributes_config": {
Type: "object",
Description: "Injected resource attributes config",
Properties: map[string]*ConfigMetadata{
"service.name": {
Type: "string",
},
},
},
},
Properties: map[string]*ConfigMetadata{
"resource_attributes": {
Ref: "/config/metadata.resource_attributes_config",
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
resourceAttributes := result.Properties["resource_attributes"]
require.Equal(t, "object", resourceAttributes.Type)
require.Equal(t, "Injected resource attributes config", resourceAttributes.Description)
require.Contains(t, resourceAttributes.Properties, "service.name")
require.Equal(t, "string", resourceAttributes.Properties["service.name"].Type)
}
func TestResolver_ResolveSchema_MapValueError(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: &mockLoader{schemas: map[string]*ConfigMetadata{}},
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"field": {
// Ref to an unknown external schema → loader returns error
Ref: "go.opentelemetry.io/collector/missing/pkg.config",
},
},
}
_, err := resolver.ResolveSchema(src)
require.Error(t, err)
}
func TestResolver_ResolveSchema_AllOfError(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: &mockLoader{schemas: map[string]*ConfigMetadata{}},
}
src := &ConfigMetadata{
Type: "object",
AllOf: []*ConfigMetadata{
{
Ref: "go.opentelemetry.io/collector/missing/pkg.config",
},
},
}
_, err := resolver.ResolveSchema(src)
require.Error(t, err)
}
func TestResolver_ResolveSchema_PtrFieldError(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: &mockLoader{schemas: map[string]*ConfigMetadata{}},
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"body": {
Type: "string",
ContentSchema: &ConfigMetadata{
Ref: "/",
},
},
},
}
_, err := resolver.ResolveSchema(src)
require.Error(t, err)
}
func TestResolver_ResolveSchema_PointerFields(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: NewLoader(""),
}
minItems := 1
maxItems := 10
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"tags": {
Type: "array",
Items: &ConfigMetadata{Type: "string"},
MinItems: &minItems,
MaxItems: &maxItems,
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.NotNil(t, result.Properties["tags"])
require.Equal(t, "array", result.Properties["tags"].Type)
require.NotNil(t, result.Properties["tags"].Items)
require.Equal(t, "string", result.Properties["tags"].Items.Type)
}
func TestResolver_ResolveSchema_ContentSchema(t *testing.T) {
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: NewLoader(""),
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"body": {
Type: "string",
ContentMediaType: "application/json",
ContentSchema: &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"name": {Type: "string"},
},
},
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.NotNil(t, result.Properties["body"])
require.NotNil(t, result.Properties["body"].ContentSchema)
require.Equal(t, "object", result.Properties["body"].ContentSchema.Type)
}
func TestResolver_ResolveSchema_PreservesCustomExtensions(t *testing.T) {
// When a node has both a $ref and custom extensions (GoType, IsPointer,
// IsOptional, Description, Default, Enum), the custom extensions should
// be preserved after resolution instead of being overwritten by the
// resolved schema's values.
targetSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"duration_type": {
Type: "string",
Description: "A generic duration type",
},
},
}
ml := &mockLoader{
schemas: map[string]*ConfigMetadata{
"go.opentelemetry.io/collector/config/configbase.duration_type": targetSchema,
},
}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: ml,
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"timeout": {
Ref: "go.opentelemetry.io/collector/config/configbase.duration_type",
GoType: "time.Duration",
IsPointer: true,
IsOptional: true,
Description: "Request timeout for the endpoint",
Default: defaultValue("30s"),
Enum: []any{"10s", "30s", "60s"},
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.NotNil(t, result.Properties["timeout"])
timeout := result.Properties["timeout"]
// GoType should be preserved from the referencing node
require.Equal(t, "time.Duration", timeout.GoType)
// IsPointer should be preserved
require.True(t, timeout.IsPointer)
// IsOptional should be preserved
require.True(t, timeout.IsOptional)
// Description should come from the referencing node, not the target
require.Equal(t, "Request timeout for the endpoint", timeout.Description)
// Default should be preserved
require.Equal(t, "30s", timeout.Default)
// Enum should be preserved
require.Equal(t, []any{"10s", "30s", "60s"}, timeout.Enum)
// Type should come from the resolved schema
require.Equal(t, "string", timeout.Type)
}
func TestResolver_ResolveSchema_RefWithoutCustomExtensions(t *testing.T) {
// When a node has a $ref but NO custom extensions, the resolved schema's
// values should be used as-is (no overriding).
targetSchema := &ConfigMetadata{
Type: "object",
Defs: map[string]*ConfigMetadata{
"base_config": {
Type: "object",
Description: "Base configuration from the target schema",
GoType: "BaseConfig",
},
},
}
ml := &mockLoader{
schemas: map[string]*ConfigMetadata{
"go.opentelemetry.io/collector/config/configbase.base_config": targetSchema,
},
}
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: ml,
}
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"base": {
Ref: "go.opentelemetry.io/collector/config/configbase.base_config",
// No custom extensions set
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
require.NotNil(t, result.Properties["base"])
base := result.Properties["base"]
// Values should come from the resolved target
require.Equal(t, "object", base.Type)
require.Equal(t, "Base configuration from the target schema", base.Description)
require.Equal(t, "BaseConfig", base.GoType)
}
func TestResolver_ResolveSchema_PreservesIntAndFloatPointers(t *testing.T) {
// Regression test: *int and *float64 pointer fields must be copied by the resolver.
// Previously, only *ConfigMetadata pointers were handled; all other pointer types
// were silently dropped, causing MinLength, MaxLength, Minimum, Maximum, etc. to be nil.
resolver := &Resolver{
pkgID: "go.opentelemetry.io/collector/test/component",
class: "receiver",
name: "test",
loader: NewLoader(""),
}
minLen, maxLen := 1, 255
minItems, maxItems := 2, 10
minProps, maxProps := 1, 5
minimum, maximum := 0.5, 100.0
exclMin, exclMax := 0.0, 101.0
multipleOf := 0.5
src := &ConfigMetadata{
Type: "object",
Properties: map[string]*ConfigMetadata{
"name": {
Type: "string",
MinLength: &minLen,
MaxLength: &maxLen,
},
"tags": {
Type: "array",
MinItems: &minItems,
MaxItems: &maxItems,
},
"meta": {
Type: "object",
MinProperties: &minProps,
MaxProperties: &maxProps,
},
"score": {
Type: "number",
Minimum: &minimum,
Maximum: &maximum,
ExclusiveMinimum: &exclMin,
ExclusiveMaximum: &exclMax,
MultipleOf: &multipleOf,
},
},
}
result, err := resolver.ResolveSchema(src)
require.NoError(t, err)
name := result.Properties["name"]
require.NotNil(t, name.MinLength)
require.Equal(t, minLen, *name.MinLength)
require.NotNil(t, name.MaxLength)
require.Equal(t, maxLen, *name.MaxLength)
tags := result.Properties["tags"]
require.NotNil(t, tags.MinItems)
require.Equal(t, minItems, *tags.MinItems)
require.NotNil(t, tags.MaxItems)
require.Equal(t, maxItems, *tags.MaxItems)
meta := result.Properties["meta"]
require.NotNil(t, meta.MinProperties)
require.Equal(t, minProps, *meta.MinProperties)
require.NotNil(t, meta.MaxProperties)
require.Equal(t, maxProps, *meta.MaxProperties)
score := result.Properties["score"]
require.NotNil(t, score.Minimum)
require.InEpsilon(t, minimum, *score.Minimum, 1e-9)
require.NotNil(t, score.Maximum)
require.InEpsilon(t, maximum, *score.Maximum, 1e-9)
require.NotNil(t, score.ExclusiveMinimum)
require.InDelta(t, exclMin, *score.ExclusiveMinimum, 1e-9)
require.NotNil(t, score.ExclusiveMaximum)
require.InEpsilon(t, exclMax, *score.ExclusiveMaximum, 1e-9)
require.NotNil(t, score.MultipleOf)
require.InEpsilon(t, multipleOf, *score.MultipleOf, 1e-9)
}