diff --git a/.chloggen/add-dry-run-flag-validate-all-fields.yaml b/.chloggen/add-dry-run-flag-validate-all-fields.yaml new file mode 100644 index 00000000000..d98a4e18250 --- /dev/null +++ b/.chloggen/add-dry-run-flag-validate-all-fields.yaml @@ -0,0 +1,16 @@ +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver) +component: service + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Added dry run flag to validate config file without running collector. + +# One or more tracking issues or pull requests related to the change +issues: [4671] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: diff --git a/otelcol/collector.go b/otelcol/collector.go index 31672eab240..788c022e9f2 100644 --- a/otelcol/collector.go +++ b/otelcol/collector.go @@ -192,6 +192,15 @@ func (col *Collector) reloadConfiguration(ctx context.Context) error { return nil } +func (col *Collector) DryRun(ctx context.Context) error { + cfg, err := col.set.ConfigProvider.Get(ctx, col.set.Factories) + if err != nil { + return fmt.Errorf("failed to get config: %w", err) + } + + return cfg.Validate() +} + // Run starts the collector according to the given configuration, and waits for it to complete. // Consecutive calls to Run are not allowed, Run shouldn't be called once a collector is shut down. func (col *Collector) Run(ctx context.Context) error { diff --git a/otelcol/collector_test.go b/otelcol/collector_test.go index 2ec05c37282..e9ba23719cc 100644 --- a/otelcol/collector_test.go +++ b/otelcol/collector_test.go @@ -350,6 +350,25 @@ func TestCollectorClosedStateOnStartUpError(t *testing.T) { assert.Equal(t, StateClosed, col.GetState()) } +func TestCollectorDryRun(t *testing.T) { + factories, err := nopFactories() + require.NoError(t, err) + + cfgProvider, err := NewConfigProvider(newDefaultConfigProviderSettings([]string{filepath.Join("testdata", "otelcol-invalid.yaml")})) + require.NoError(t, err) + + // Load a bad config causing startup to fail + set := CollectorSettings{ + BuildInfo: component.NewDefaultBuildInfo(), + Factories: factories, + ConfigProvider: cfgProvider, + } + col, err := NewCollector(set) + require.NoError(t, err) + + require.Error(t, col.DryRun(context.Background())) +} + func startCollector(ctx context.Context, t *testing.T, col *Collector) *sync.WaitGroup { wg := &sync.WaitGroup{} wg.Add(1) diff --git a/otelcol/command.go b/otelcol/command.go index fca6286c951..9350b8b68fe 100644 --- a/otelcol/command.go +++ b/otelcol/command.go @@ -28,6 +28,7 @@ func NewCommand(set CollectorSettings) *cobra.Command { }, } rootCmd.AddCommand(newBuildSubCommand(set)) + rootCmd.AddCommand(newValidateSubCommand(set, flagSet)) rootCmd.Flags().AddGoFlagSet(flagSet) return rootCmd } diff --git a/otelcol/command_validate.go b/otelcol/command_validate.go new file mode 100644 index 00000000000..b64cb282d1d --- /dev/null +++ b/otelcol/command_validate.go @@ -0,0 +1,42 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package otelcol // import "go.opentelemetry.io/collector/otelcol" + +import ( + "errors" + "flag" + + "github.com/spf13/cobra" +) + +// newValidateSubCommand constructs a new validate sub command using the given CollectorSettings. +func newValidateSubCommand(set CollectorSettings, flagSet *flag.FlagSet) *cobra.Command { + validateCmd := &cobra.Command{ + Use: "validate", + Short: "Validates the config without running the collector", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + if set.ConfigProvider == nil { + var err error + + configFlags := getConfigFlag(flagSet) + if len(configFlags) == 0 { + return errors.New("at least one config flag must be provided") + } + + set.ConfigProvider, err = NewConfigProvider(newDefaultConfigProviderSettings(configFlags)) + if err != nil { + return err + } + } + col, err := NewCollector(set) + if err != nil { + return err + } + return col.DryRun(cmd.Context()) + }, + } + validateCmd.Flags().AddGoFlagSet(flagSet) + return validateCmd +} diff --git a/otelcol/command_validate_test.go b/otelcol/command_validate_test.go new file mode 100644 index 00000000000..4511eb73097 --- /dev/null +++ b/otelcol/command_validate_test.go @@ -0,0 +1,46 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package otelcol + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/confmap/converter/expandconverter" + "go.opentelemetry.io/collector/confmap/provider/fileprovider" + "go.opentelemetry.io/collector/featuregate" +) + +func TestValidateSubCommandNoConfig(t *testing.T) { + factories, err := nopFactories() + require.NoError(t, err) + + cmd := newValidateSubCommand(CollectorSettings{Factories: factories}, flags(featuregate.GlobalRegistry())) + err = cmd.Execute() + require.Error(t, err) + require.Contains(t, err.Error(), "at least one config flag must be provided") +} + +func TestValidateSubCommandInvalidComponents(t *testing.T) { + factories, err := nopFactories() + require.NoError(t, err) + + cfgProvider, err := NewConfigProvider( + ConfigProviderSettings{ + ResolverSettings: confmap.ResolverSettings{ + URIs: []string{filepath.Join("testdata", "otelcol-invalid-components.yaml")}, + Providers: map[string]confmap.Provider{"file": fileprovider.New()}, + Converters: []confmap.Converter{expandconverter.New()}, + }, + }) + require.NoError(t, err) + + cmd := newValidateSubCommand(CollectorSettings{Factories: factories, ConfigProvider: cfgProvider}, flags(featuregate.GlobalRegistry())) + err = cmd.Execute() + require.Error(t, err) + require.Contains(t, err.Error(), "unknown type: \"nosuchprocessor\"") +} diff --git a/otelcol/testdata/otelcol-invalid-components.yaml b/otelcol/testdata/otelcol-invalid-components.yaml new file mode 100644 index 00000000000..37a1ea4b3dc --- /dev/null +++ b/otelcol/testdata/otelcol-invalid-components.yaml @@ -0,0 +1,12 @@ +receivers: + nop: +exporters: + nop: +processors: + nosuchprocessor: +service: + pipelines: + traces: + receivers: [nop] + exporters: [nop] + processors: [nop] diff --git a/service/README.md b/service/README.md index 30bebe03f05..645ac3fa4d4 100644 --- a/service/README.md +++ b/service/README.md @@ -143,5 +143,10 @@ exporters: extensions: - zpages - memory_ballast +``` + +## How to validate configuration file and return all errors without running collector -``` \ No newline at end of file +```bash + ./otelcorecol validate --config=file:examples/local/otel-config.yaml +```