Skip to content

Commit 4073c63

Browse files
committed
Fix null bodies in schema
Signed-off-by: Israel Blancas <iblancasa@gmail.com>
1 parent be178a7 commit 4073c63

3 files changed

Lines changed: 158 additions & 0 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: bug_fix
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. receiver/otlp)
7+
component: cmd/schemagen
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Accept components written without a configuration body in the generated Collector configuration schema
11+
12+
# One or more tracking issues or pull requests related to the change
13+
issues: [15728]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# Optional: The change log or logs in which this entry should be included.
21+
# e.g. '[user]' or '[user, api]'
22+
# Include 'user' if the change is relevant to end users.
23+
# Include 'api' if there is a change to a library API.
24+
# Default: '[user]'
25+
change_logs: []

internal/schemagen/combiner.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package schemagen // import "go.opentelemetry.io/collector/internal/schemagen"
55

66
import (
77
"fmt"
8+
"reflect"
89
"regexp"
910
)
1011

@@ -99,6 +100,11 @@ func addCollectorComponentPattern(section *JSONSchema, componentType string, sch
99100
}
100101

101102
patternSchema := cloneOrEmptySchema(schema)
103+
if schema != nil && !reflect.DeepEqual(*schema, JSONSchema{}) {
104+
patternSchema = &JSONSchema{
105+
AnyOf: []*JSONSchema{patternSchema, {Type: "null"}},
106+
}
107+
}
102108
if deprecated {
103109
patternSchema.Deprecated = true
104110
}

internal/schemagen/combiner_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/santhosh-tekuri/jsonschema/v6"
1111
"github.com/stretchr/testify/require"
12+
"go.yaml.in/yaml/v3"
1213
)
1314

1415
func TestCombineCollectorSchema_LayoutAndValidation(t *testing.T) {
@@ -173,6 +174,123 @@ func TestCombineCollectorSchema_DeprecatedTypeDuplicatesMainType(t *testing.T) {
173174
require.ErrorContains(t, err, `duplicate component identifier "health_check"`)
174175
}
175176

177+
func TestCombineCollectorSchema_NullComponentBodyIsAccepted(t *testing.T) {
178+
t.Parallel()
179+
180+
schema, err := CombineCollectorSchema(CollectorSchemaParts{
181+
Receivers: []CollectorComponentSchema{
182+
{
183+
Type: "otlp",
184+
Schema: &JSONSchema{
185+
Type: "object",
186+
Properties: map[string]*JSONSchema{
187+
"endpoint": {Type: "string"},
188+
},
189+
},
190+
},
191+
},
192+
})
193+
require.NoError(t, err)
194+
195+
compiled := compileSchema(t, schema)
196+
197+
require.NoError(t, compiled.Validate(unmarshalJSON(t, `{
198+
"receivers": {"otlp": null}
199+
}`)))
200+
201+
require.NoError(t, compiled.Validate(unmarshalYAML(t, "receivers:\n otlp:\n")))
202+
203+
require.NoError(t, compiled.Validate(unmarshalJSON(t, `{
204+
"receivers": {"otlp/secondary": null}
205+
}`)))
206+
207+
require.NoError(t, compiled.Validate(unmarshalJSON(t, `{
208+
"receivers": {"otlp": {"endpoint": "localhost:4317"}}
209+
}`)))
210+
}
211+
212+
func TestCombineCollectorSchema_NullBranchDoesNotSwallowTypeErrors(t *testing.T) {
213+
t.Parallel()
214+
215+
schema, err := CombineCollectorSchema(CollectorSchemaParts{
216+
Receivers: []CollectorComponentSchema{
217+
{
218+
Type: "otlp",
219+
Schema: &JSONSchema{
220+
Type: "object",
221+
Properties: map[string]*JSONSchema{
222+
"endpoint": {Type: "string"},
223+
},
224+
Required: []string{"endpoint"},
225+
},
226+
},
227+
},
228+
})
229+
require.NoError(t, err)
230+
231+
compiled := compileSchema(t, schema)
232+
233+
for name, config := range map[string]string{
234+
"scalar body": `{"receivers": {"otlp": 5}}`,
235+
"string body": `{"receivers": {"otlp": "enabled"}}`,
236+
"array body": `{"receivers": {"otlp": []}}`,
237+
"wrong property type": `{"receivers": {"otlp": {"endpoint": 4317}}}`,
238+
"missing required": `{"receivers": {"otlp": {}}}`,
239+
} {
240+
t.Run(name, func(t *testing.T) {
241+
require.Error(t, compiled.Validate(unmarshalJSON(t, config)))
242+
})
243+
}
244+
}
245+
246+
func TestCombineCollectorSchema_NilSchemaIsNotWrapped(t *testing.T) {
247+
t.Parallel()
248+
249+
schema, err := CombineCollectorSchema(CollectorSchemaParts{
250+
Exporters: []CollectorComponentSchema{
251+
{Type: "debug"},
252+
},
253+
})
254+
require.NoError(t, err)
255+
256+
debug := schema.Properties[string(CollectorSectionExporters)].PatternProperties[collectorComponentPattern("debug")]
257+
require.NotNil(t, debug)
258+
require.Empty(t, debug.AnyOf)
259+
260+
data, err := debug.MarshalJSON()
261+
require.NoError(t, err)
262+
require.JSONEq(t, `true`, string(data))
263+
264+
compiled := compileSchema(t, schema)
265+
require.NoError(t, compiled.Validate(unmarshalJSON(t, `{"exporters": {"debug": null}}`)))
266+
require.NoError(t, compiled.Validate(unmarshalJSON(t, `{"exporters": {"debug": {"verbosity": "detailed"}}}`)))
267+
}
268+
269+
func TestCombineCollectorSchema_NilSchemaDeprecatedKeepsMarker(t *testing.T) {
270+
t.Parallel()
271+
272+
schema, err := CombineCollectorSchema(CollectorSchemaParts{
273+
Extensions: []CollectorComponentSchema{
274+
{Type: "health_check", DeprecatedType: "healthcheck"},
275+
},
276+
})
277+
require.NoError(t, err)
278+
279+
extensions := schema.Properties[string(CollectorSectionExtensions)]
280+
deprecatedSchema := extensions.PatternProperties[collectorComponentPattern("healthcheck")]
281+
require.NotNil(t, deprecatedSchema)
282+
require.True(t, deprecatedSchema.Deprecated)
283+
require.Empty(t, deprecatedSchema.AnyOf)
284+
285+
data, err := deprecatedSchema.MarshalJSON()
286+
require.NoError(t, err)
287+
require.JSONEq(t, `{"deprecated": true}`, string(data))
288+
289+
compiled := compileSchema(t, schema)
290+
require.NoError(t, compiled.Validate(unmarshalJSON(t, `{"extensions": {"healthcheck": null}}`)))
291+
require.NoError(t, compiled.Validate(unmarshalJSON(t, `{"extensions": {"healthcheck/1": {"endpoint": "x"}}}`)))
292+
}
293+
176294
func compileSchema(t *testing.T, schema *JSONSchema) *jsonschema.Schema {
177295
t.Helper()
178296

@@ -198,3 +316,12 @@ func unmarshalJSON(t *testing.T, data string) any {
198316

199317
return value
200318
}
319+
320+
func unmarshalYAML(t *testing.T, data string) any {
321+
t.Helper()
322+
323+
var value any
324+
require.NoError(t, yaml.Unmarshal([]byte(data), &value))
325+
326+
return value
327+
}

0 commit comments

Comments
 (0)