Skip to content

Commit a17c1d2

Browse files
committed
[cmd/mdatagen] Localize same-repo schema refs using import root path
This change make mdatagen to rewrite same-repo schema references to absolute local refs during schema generation and resolution. Instead of keeping refs like go.opentelemetry.io/collector/filter.config as versioned external references, mdatagen now converts them to /filter.config when the ref belongs to the current repo import root. This avoids generator failures in release/version-bump PRs where mdatagen would otherwise try to resolve schemas from a not-yet-published version tag or .schemas/<next-version> cache. See https://github.com/open-telemetry/opentelemetry-collector/actions/runs/23166843771/job/67309001982?pr=14772
1 parent 7cd8f58 commit a17c1d2

13 files changed

Lines changed: 228 additions & 121 deletions

File tree

cmd/mdatagen/internal/cfggen/namespace.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,10 @@ func (r *Ref) String() string {
188188
func (r *Ref) CacheKey() string {
189189
return r.String()
190190
}
191+
192+
func LocalizeRef(refPath, importRootPath string) string {
193+
if importRootPath == "" || !strings.HasPrefix(refPath, importRootPath+"/") {
194+
return refPath
195+
}
196+
return strings.TrimPrefix(refPath, importRootPath)
197+
}

cmd/mdatagen/internal/cfggen/namespace_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,3 +339,31 @@ func TestNamespaceOf_NoSlash(t *testing.T) {
339339
result := namespaceOf("noslash")
340340
require.Empty(t, result)
341341
}
342+
343+
func TestLocalizeRef(t *testing.T) {
344+
tests := []struct {
345+
name string
346+
refPath string
347+
importRootPath string
348+
expected string
349+
}{
350+
{
351+
name: "same root collector ref becomes local absolute",
352+
refPath: "go.opentelemetry.io/collector/filter.config",
353+
importRootPath: "go.opentelemetry.io/collector",
354+
expected: "/filter.config",
355+
},
356+
{
357+
name: "different root ref stays external",
358+
refPath: "go.opentelemetry.io/collector/filter.config",
359+
importRootPath: "github.com/open-telemetry/opentelemetry-collector-contrib",
360+
expected: "go.opentelemetry.io/collector/filter.config",
361+
},
362+
}
363+
364+
for _, tt := range tests {
365+
t.Run(tt.name, func(t *testing.T) {
366+
require.Equal(t, tt.expected, LocalizeRef(tt.refPath, tt.importRootPath))
367+
})
368+
}
369+
}

cmd/mdatagen/internal/cfggen/resolver.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,22 @@ const (
1616
)
1717

1818
type Resolver struct {
19-
pkgID string
20-
class string
21-
name string
22-
loader Loader
19+
pkgID string
20+
class string
21+
name string
22+
importRootPath string
23+
loader Loader
2324
}
2425

25-
func NewResolver(pkgID, class, name, dir string) *Resolver {
26+
func NewResolver(pkgID, class, name, dir, importRootPath string) *Resolver {
2627
loader := NewLoader(dir)
2728

2829
return &Resolver{
29-
loader: loader,
30-
pkgID: pkgID,
31-
class: class,
32-
name: name,
30+
loader: loader,
31+
pkgID: pkgID,
32+
class: class,
33+
name: name,
34+
importRootPath: importRootPath,
3335
}
3436
}
3537

@@ -178,7 +180,7 @@ func (r *Resolver) resolveSchema(root, current, target *ConfigMetadata, origin *
178180
// The origin parameter tracks which namespace the current schema was loaded from,
179181
// enabling local refs in remotely-fetched schemas to be converted to external refs.
180182
func (r *Resolver) resolveRef(root, current *ConfigMetadata, origin *Ref) (*ConfigMetadata, error) {
181-
ref := WithOrigin(current.Ref, origin)
183+
ref := WithOrigin(LocalizeRef(current.Ref, r.importRootPath), origin)
182184

183185
if err := ref.Validate(); err != nil {
184186
return nil, fmt.Errorf("invalid reference format %q: %w", current.Ref, err)

cmd/mdatagen/internal/cfggen/resolver_test.go

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -735,14 +735,43 @@ func TestResolver_ResolveSchema_ParentRelativeRefWithOrigin(t *testing.T) {
735735
require.Equal(t, "TLS settings", tls.Description)
736736
}
737737

738-
func TestNewResolver(t *testing.T) {
739-
dir := t.TempDir()
740-
r := NewResolver("go.opentelemetry.io/collector/receiver/otlp", "receiver", "otlp", dir)
741-
require.NotNil(t, r)
742-
require.Equal(t, "go.opentelemetry.io/collector/receiver/otlp", r.pkgID)
743-
require.Equal(t, "receiver", r.class)
744-
require.Equal(t, "otlp", r.name)
745-
require.NotNil(t, r.loader)
738+
func TestResolver_ResolveSchema_SameRootExternalRefLocalized(t *testing.T) {
739+
ml := &mockLoader{
740+
schemas: map[string]*ConfigMetadata{
741+
"/config/confighttp.client_config": {
742+
Defs: map[string]*ConfigMetadata{
743+
"client_config": {
744+
Type: "object",
745+
Properties: map[string]*ConfigMetadata{
746+
"endpoint": {Type: "string"},
747+
},
748+
},
749+
},
750+
},
751+
},
752+
}
753+
754+
resolver := &Resolver{
755+
pkgID: "go.opentelemetry.io/collector/receiver/otlpreceiver",
756+
class: "receiver",
757+
name: "otlp",
758+
importRootPath: "go.opentelemetry.io/collector",
759+
loader: ml,
760+
}
761+
762+
src := &ConfigMetadata{
763+
Type: "object",
764+
Properties: map[string]*ConfigMetadata{
765+
"http": {
766+
Ref: "go.opentelemetry.io/collector/config/confighttp.client_config",
767+
},
768+
},
769+
}
770+
771+
result, err := resolver.ResolveSchema(src)
772+
require.NoError(t, err)
773+
require.Equal(t, "object", result.Properties["http"].Type)
774+
require.Equal(t, "string", result.Properties["http"].Properties["endpoint"].Type)
746775
}
747776

748777
func TestResolver_ResolveSchema_UnknownNamespaceFallback(t *testing.T) {

cmd/mdatagen/internal/command.go

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ func run(ymlPath string) error {
8181

8282
ymlDir := filepath.Dir(ymlPath)
8383
packageName := filepath.Base(ymlDir)
84+
importRootPath, err := helpers.RootPackage(ymlDir)
85+
if err != nil {
86+
return fmt.Errorf("unable to determine import root path: %w", err)
87+
}
8488

8589
raw, readErr := os.ReadFile(filepath.Clean(ymlPath))
8690
if readErr != nil {
@@ -104,7 +108,7 @@ func run(ymlPath string) error {
104108
if !slices.Contains(nonComponents, md.Status.Class) {
105109
toGenerate[filepath.Join(tmplDir, "status.go.tmpl")] = filepath.Join(codeDir, "generated_status.go")
106110
err = generateFile(filepath.Join(tmplDir, "component_test.go.tmpl"),
107-
filepath.Join(ymlDir, "generated_component_test.go"), md, packageName)
111+
filepath.Join(ymlDir, "generated_component_test.go"), md, packageName, importRootPath)
108112
if err != nil {
109113
return err
110114
}
@@ -124,7 +128,7 @@ func run(ymlPath string) error {
124128
}
125129

126130
err = generateFile(filepath.Join(tmplDir, "package_test.go.tmpl"),
127-
filepath.Join(ymlDir, "generated_package_test.go"), md, packageName)
131+
filepath.Join(ymlDir, "generated_package_test.go"), md, packageName, importRootPath)
128132
if err != nil {
129133
return err
130134
}
@@ -133,7 +137,7 @@ func run(ymlPath string) error {
133137
err = inlineReplace(
134138
filepath.Join(tmplDir, "readme.md.tmpl"),
135139
filepath.Join(ymlDir, "README.md"),
136-
md, statusStart, statusEnd, md.GeneratedPackageName)
140+
md, statusStart, statusEnd, md.GeneratedPackageName, importRootPath)
137141
if err != nil {
138142
return err
139143
}
@@ -225,19 +229,19 @@ func run(ymlPath string) error {
225229
}
226230

227231
for tmpl, dst := range toGenerate {
228-
if err := generateFile(tmpl, dst, md, md.GeneratedPackageName); err != nil {
232+
if err := generateFile(tmpl, dst, md, md.GeneratedPackageName, importRootPath); err != nil {
229233
return err
230234
}
231235
}
232236

233-
if err := generateConfigFiles(md, ymlDir); err != nil {
237+
if err := generateConfigFiles(md, ymlDir, importRootPath); err != nil {
234238
return fmt.Errorf("failed to generate config files: %w", err)
235239
}
236240

237241
return nil
238242
}
239243

240-
func getTemplateFuncMap(md Metadata) template.FuncMap {
244+
func getTemplateFuncMap(md Metadata, importRootPath string) template.FuncMap {
241245
return template.FuncMap{
242246
"publicVar": func(s string) (string, error) {
243247
return helpers.FormatIdentifier(s, true)
@@ -344,6 +348,9 @@ func getTemplateFuncMap(md Metadata) template.FuncMap {
344348
"toLowerCamelCase": func(s string) string {
345349
return joinCamelCase(strings.Split(s, "_"), false)
346350
},
351+
"schemaRef": func(ref string) string {
352+
return cfggen.LocalizeRef(ref, importRootPath)
353+
},
347354
"inc": func(i int) int { return i + 1 },
348355
"distroURL": distroURL,
349356
"isExporter": func() bool {
@@ -406,21 +413,21 @@ func templatize(tmplFile string, funcMap template.FuncMap) *template.Template {
406413
ParseFS(TemplateFS, "templates/helper.tmpl", strings.ReplaceAll(tmplFile, "\\", "/")))
407414
}
408415

409-
func executeTemplate(tmplFile string, md Metadata, goPackage string, fns template.FuncMap) ([]byte, error) {
416+
func executeTemplate(tmplFile string, md Metadata, goPackage, importRootPath string, fns template.FuncMap) ([]byte, error) {
410417
tmpl := templatize(tmplFile, fns)
411418
buf := bytes.Buffer{}
412419

413-
if err := tmpl.Execute(&buf, TemplateContext{Metadata: md, Package: goPackage}); err != nil {
420+
if err := tmpl.Execute(&buf, TemplateContext{Metadata: md, Package: goPackage, ImportRootPath: importRootPath}); err != nil {
414421
return []byte{}, fmt.Errorf("failed executing template: %w", err)
415422
}
416423
return buf.Bytes(), nil
417424
}
418425

419-
func generateFile(tmplFile, outputFile string, md Metadata, goPackage string) error {
420-
return generateFileWithFns(tmplFile, outputFile, md, goPackage, getTemplateFuncMap(md))
426+
func generateFile(tmplFile, outputFile string, md Metadata, goPackage, importRootPath string) error {
427+
return generateFileWithFns(tmplFile, outputFile, md, goPackage, importRootPath, getTemplateFuncMap(md, importRootPath))
421428
}
422429

423-
func inlineReplace(tmplFile, outputFile string, md Metadata, start, end, goPackage string) error {
430+
func inlineReplace(tmplFile, outputFile string, md Metadata, start, end, goPackage, importRootPath string) error {
424431
var readmeContents []byte
425432
var err error
426433
if readmeContents, err = os.ReadFile(filepath.Clean(outputFile)); err != nil {
@@ -436,7 +443,7 @@ func inlineReplace(tmplFile, outputFile string, md Metadata, start, end, goPacka
436443
md.GithubProject = "open-telemetry/opentelemetry-collector-contrib"
437444
}
438445

439-
buf, err := executeTemplate(tmplFile, md, goPackage, getTemplateFuncMap(md))
446+
buf, err := executeTemplate(tmplFile, md, goPackage, importRootPath, getTemplateFuncMap(md, importRootPath))
440447
if err != nil {
441448
return err
442449
}
@@ -449,12 +456,12 @@ func inlineReplace(tmplFile, outputFile string, md Metadata, start, end, goPacka
449456
return nil
450457
}
451458

452-
func generateFileWithFns(tmplFile, outputFile string, md Metadata, goPackage string, fns template.FuncMap) error {
459+
func generateFileWithFns(tmplFile, outputFile string, md Metadata, goPackage, importRootPath string, fns template.FuncMap) error {
453460
if err := os.Remove(outputFile); err != nil && !errors.Is(err, fs.ErrNotExist) {
454461
return fmt.Errorf("unable to remove generated file %q: %w", outputFile, err)
455462
}
456463

457-
result, err := executeTemplate(tmplFile, md, goPackage, fns)
464+
result, err := executeTemplate(tmplFile, md, goPackage, importRootPath, fns)
458465
if err != nil {
459466
return err
460467
}
@@ -534,9 +541,9 @@ func validateYAMLKeyOrder(raw []byte) error {
534541
return nil
535542
}
536543

537-
func generateConfigFiles(md Metadata, mdDir string) error {
544+
func generateConfigFiles(md Metadata, mdDir, importRootPath string) error {
538545
if md.Config != nil {
539-
resolver := cfggen.NewResolver(md.PackageName, md.Status.Class, md.Type, mdDir)
546+
resolver := cfggen.NewResolver(md.PackageName, md.Status.Class, md.Type, mdDir, importRootPath)
540547
resolvedSchema, err := resolver.ResolveSchema(md.Config)
541548
if err != nil {
542549
return fmt.Errorf("failed to resolve config schema: %w", err)
@@ -564,8 +571,8 @@ func generateConfigGoStruct(md Metadata, outputDir string) error {
564571
tmplFile := filepath.Join("templates", "config_from_cfggen.go.tmpl")
565572
dstFile := filepath.Join(outputDir, "generated_config.go")
566573

567-
fns := cfggen.WithCfgFns(getTemplateFuncMap(md), rootPkg, md.PackageName)
568-
return generateFileWithFns(tmplFile, dstFile, md, packageName, fns)
574+
fns := cfggen.WithCfgFns(getTemplateFuncMap(md, rootPkg), rootPkg, md.PackageName)
575+
return generateFileWithFns(tmplFile, dstFile, md, packageName, rootPkg, fns)
569576
}
570577

571578
func joinCamelCase(parts []string, exported bool) string {

cmd/mdatagen/internal/command_test.go

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ func TestGenerateConfigFiles(t *testing.T) {
528528

529529
gitInit(t, root)
530530
require.NoError(t, os.WriteFile(filepath.Join(root, "go.mod"), []byte("module testmodule\n"), 0o600))
531-
err := generateConfigFiles(tt.md, tmpdir)
531+
err := generateConfigFiles(tt.md, tmpdir, "testmodule")
532532
if tt.wantErr {
533533
require.Error(t, err)
534534
return
@@ -564,7 +564,7 @@ func TestGenerateConfigFiles_GoStructError(t *testing.T) {
564564
Status: &Status{Class: "receiver"},
565565
Config: &cfggen.ConfigMetadata{Type: "object"},
566566
}
567-
err := generateConfigFiles(md, t.TempDir())
567+
err := generateConfigFiles(md, t.TempDir(), "testmodule")
568568
require.Error(t, err)
569569
require.Contains(t, err.Error(), "failed to generate config Go struct")
570570
}
@@ -580,7 +580,7 @@ func TestGenerateConfigFiles_WriteError(t *testing.T) {
580580
Type: "object",
581581
},
582582
}
583-
err := generateConfigFiles(md, "/nonexistent/path/that/does/not/exist")
583+
err := generateConfigFiles(md, "/nonexistent/path/that/does/not/exist", "testmodule")
584584
require.Error(t, err)
585585
require.Contains(t, err.Error(), "failed to write config schema")
586586
}
@@ -855,7 +855,7 @@ Some info about a component
855855
readmeFile := filepath.Join(tmpdir, "README.md")
856856
require.NoError(t, os.WriteFile(readmeFile, []byte(tt.markdown), 0o600))
857857

858-
err := inlineReplace("templates/readme.md.tmpl", readmeFile, md, statusStart, statusEnd, "metadata")
858+
err := inlineReplace("templates/readme.md.tmpl", readmeFile, md, statusStart, statusEnd, "metadata", "go.opentelemetry.io/collector")
859859
require.NoError(t, err)
860860

861861
require.FileExists(t, filepath.Join(tmpdir, "README.md"))
@@ -977,7 +977,7 @@ const (
977977
t.Run(tt.name, func(t *testing.T) {
978978
tmpdir := t.TempDir()
979979
err := generateFile("templates/status.go.tmpl",
980-
filepath.Join(tmpdir, "generated_status.go"), tt.md, "metadata")
980+
filepath.Join(tmpdir, "generated_status.go"), tt.md, "metadata", "go.opentelemetry.io/collector")
981981
require.NoError(t, err)
982982
actual, err := os.ReadFile(filepath.Clean(filepath.Join(tmpdir, "generated_status.go")))
983983
require.NoError(t, err)
@@ -1063,7 +1063,7 @@ func Tracer(settings component.TelemetrySettings) trace.Tracer {
10631063
t.Run(tt.name, func(t *testing.T) {
10641064
tmpdir := t.TempDir()
10651065
err := generateFile("templates/telemetry.go.tmpl",
1066-
filepath.Join(tmpdir, "generated_telemetry.go"), tt.md, "metadata")
1066+
filepath.Join(tmpdir, "generated_telemetry.go"), tt.md, "metadata", "go.opentelemetry.io/collector")
10671067
require.NoError(t, err)
10681068
actual, err := os.ReadFile(filepath.Clean(filepath.Join(tmpdir, "generated_telemetry.go")))
10691069
require.NoError(t, err)
@@ -1072,6 +1072,38 @@ func Tracer(settings component.TelemetrySettings) trace.Tracer {
10721072
}
10731073
}
10741074

1075+
func TestGenerateConfigSchema_LocalizesSameRootRefs(t *testing.T) {
1076+
enabled := true
1077+
md := Metadata{
1078+
Type: "foo",
1079+
ResourceAttributes: map[AttributeName]Attribute{
1080+
"resource.attr": {
1081+
Description: "resource attr",
1082+
EnabledPtr: &enabled,
1083+
FullName: "resource.attr",
1084+
},
1085+
},
1086+
Events: map[EventName]Event{
1087+
"default.event": {
1088+
Signal: Signal{
1089+
Enabled: true,
1090+
Description: "event description",
1091+
},
1092+
},
1093+
},
1094+
}
1095+
1096+
tmpdir := t.TempDir()
1097+
outputFile := filepath.Join(tmpdir, "config.schema.yaml")
1098+
err := generateFile("templates/config.schema.yaml.tmpl", outputFile, md, "metadata", "go.opentelemetry.io/collector")
1099+
require.NoError(t, err)
1100+
1101+
actual, err := os.ReadFile(filepath.Clean(outputFile))
1102+
require.NoError(t, err)
1103+
require.Contains(t, string(actual), "$ref: /filter.config")
1104+
require.NotContains(t, string(actual), "$ref: go.opentelemetry.io/collector/filter.config")
1105+
}
1106+
10751107
func gitInit(t *testing.T, dir string) {
10761108
t.Helper()
10771109
cmd := exec.Command("git", "init")

cmd/mdatagen/internal/loader.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ type TemplateContext struct {
2929
Metadata
3030
// Package name for generated code.
3131
Package string
32+
// ImportRootPath is the repo-local import prefix used to localize same-tree schema references.
33+
ImportRootPath string
3234
}
3335

3436
func LoadMetadata(filePath string) (Metadata, error) {

0 commit comments

Comments
 (0)