diff --git a/.chloggen/builder-skip-go-mod.yaml b/.chloggen/builder-skip-go-mod.yaml new file mode 100644 index 00000000000..619e29e3489 --- /dev/null +++ b/.chloggen/builder-skip-go-mod.yaml @@ -0,0 +1,25 @@ +# Use this changelog template to create an entry for release notes. + +# 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: builder + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add a --skip-new-go-module flag to skip creating a module in the output directory. + +# One or more tracking issues or pull requests related to the change +issues: [9252] + +# (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: + +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] diff --git a/cmd/builder/Makefile b/cmd/builder/Makefile index 988ed9e64a5..4c4549c747a 100644 --- a/cmd/builder/Makefile +++ b/cmd/builder/Makefile @@ -1,5 +1,7 @@ include ../../Makefile.Common +GOTEST_TIMEOUT=360s + .PHONY: ocb ocb: CGO_ENABLED=0 $(GOCMD) build -trimpath -o ../../bin/ocb_$(GOOS)_$(GOARCH) . diff --git a/cmd/builder/README.md b/cmd/builder/README.md index b64a4d4cdc0..a0226b5ed2e 100644 --- a/cmd/builder/README.md +++ b/cmd/builder/README.md @@ -157,6 +157,24 @@ ocb --skip-generate --skip-get-modules --config=config.yaml ``` to only execute the compilation step. +### Avoiding the use of a new go.mod file + +You can optionally skip creating a new `go.mod` file. This is helpful when +using a monorepo setup with a shared go.mod file. When the `--skip-new-go-module` +command-line flag is supplied, the build process issues a `go get` command for +each component, relying on the Go toolchain to update the enclosing Go module +appropriately. + +This command will avoid downgrading a dependency in the enclosing +module, according to +[`semver.Compare()`](https://pkg.go.dev/golang.org/x/mod/semver#Compare), +and will instead issue a log indicating that the component was not +upgraded. + +`--skip-new-go-module` is incompatible with `replaces`, `excludes`, +and the component `path` override. For each of these features, users +are expected to modify the enclosing `go.mod` directly. + ### Strict versioning checks The builder checks the relevant `go.mod` diff --git a/cmd/builder/go.mod b/cmd/builder/go.mod index e724b8c175d..9f27326220d 100644 --- a/cmd/builder/go.mod +++ b/cmd/builder/go.mod @@ -31,7 +31,7 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect golang.org/x/sys v0.21.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/cmd/builder/go.sum b/cmd/builder/go.sum index dfdbd91a46b..4dd87472a19 100644 --- a/cmd/builder/go.sum +++ b/cmd/builder/go.sum @@ -37,8 +37,8 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= diff --git a/cmd/builder/internal/builder/config.go b/cmd/builder/internal/builder/config.go index b1103fe70ba..7f8fe523cd6 100644 --- a/cmd/builder/internal/builder/config.go +++ b/cmd/builder/internal/builder/config.go @@ -19,8 +19,12 @@ import ( const defaultOtelColVersion = "0.107.0" -// ErrMissingGoMod indicates an empty gomod field -var ErrMissingGoMod = errors.New("missing gomod specification for module") +var ( + // ErrMissingGoMod indicates an empty gomod field + ErrMissingGoMod = errors.New("missing gomod specification for module") + // ErrIncompatibleConfigurationValues indicates that there is configuration that cannot be combined + ErrIncompatibleConfigurationValues = errors.New("cannot combine configuration values") +) // Config holds the builder's configuration type Config struct { @@ -29,6 +33,7 @@ type Config struct { SkipGenerate bool `mapstructure:"-"` SkipCompilation bool `mapstructure:"-"` SkipGetModules bool `mapstructure:"-"` + SkipNewGoModule bool `mapstructure:"-"` SkipStrictVersioning bool `mapstructure:"-"` LDFlags string `mapstructure:"-"` Verbose bool `mapstructure:"-"` @@ -116,14 +121,15 @@ func NewDefaultConfig() Config { func (c *Config) Validate() error { var providersError error if c.Providers != nil { - providersError = validateModules("provider", *c.Providers) + providersError = c.validateModules("provider", *c.Providers) } return multierr.Combine( - validateModules("extension", c.Extensions), - validateModules("receiver", c.Receivers), - validateModules("exporter", c.Exporters), - validateModules("processor", c.Processors), - validateModules("connector", c.Connectors), + c.validateModules("extension", c.Extensions), + c.validateModules("receiver", c.Receivers), + c.validateModules("exporter", c.Exporters), + c.validateModules("processor", c.Processors), + c.validateModules("connector", c.Connectors), + c.validateFlags(), providersError, ) } @@ -240,11 +246,21 @@ func (c *Config) ParseModules() error { return nil } -func validateModules(name string, mods []Module) error { +func (c *Config) validateFlags() error { + if c.SkipNewGoModule && (len(c.Replaces) != 0 || len(c.Excludes) != 0) { + return fmt.Errorf("%w excludes or replaces with --skip-new-go-module; please modify the enclosing go.mod file directly", ErrIncompatibleConfigurationValues) + } + return nil +} + +func (c *Config) validateModules(name string, mods []Module) error { for i, mod := range mods { if mod.GoMod == "" { return fmt.Errorf("%s module at index %v: %w", name, i, ErrMissingGoMod) } + if mod.Path != "" && c.SkipNewGoModule { + return fmt.Errorf("%w cannot modify mod.path %q combined with --skip-new-go-module; please modify the enclosing go.mod file directly", ErrIncompatibleConfigurationValues, mod.Path) + } } return nil } diff --git a/cmd/builder/internal/builder/config_test.go b/cmd/builder/internal/builder/config_test.go index 9daf158cb8e..bb99cf849a4 100644 --- a/cmd/builder/internal/builder/config_test.go +++ b/cmd/builder/internal/builder/config_test.go @@ -4,7 +4,6 @@ package builder import ( - "errors" "os" "strings" "testing" @@ -140,10 +139,37 @@ func TestMissingModule(t *testing.T) { }, err: ErrMissingGoMod, }, + { + cfg: Config{ + Logger: zap.NewNop(), + SkipNewGoModule: true, + Extensions: []Module{{ + GoMod: "some-module", + Path: "invalid", + }}, + }, + err: ErrIncompatibleConfigurationValues, + }, + { + cfg: Config{ + Logger: zap.NewNop(), + SkipNewGoModule: true, + Replaces: []string{"", ""}, + }, + err: ErrIncompatibleConfigurationValues, + }, + { + cfg: Config{ + Logger: zap.NewNop(), + SkipNewGoModule: true, + Excludes: []string{"", ""}, + }, + err: ErrIncompatibleConfigurationValues, + }, } for _, test := range configurations { - assert.True(t, errors.Is(test.cfg.Validate(), test.err)) + assert.ErrorIs(t, test.cfg.Validate(), test.err) } } diff --git a/cmd/builder/internal/builder/main.go b/cmd/builder/internal/builder/main.go index ba54c40e591..e13dcec53a7 100644 --- a/cmd/builder/internal/builder/main.go +++ b/cmd/builder/internal/builder/main.go @@ -13,7 +13,6 @@ import ( "slices" "strings" "text/template" - "time" "go.uber.org/zap" "golang.org/x/mod/modfile" @@ -25,7 +24,6 @@ var ( ErrGoNotFound = errors.New("go binary not found") ErrDepNotFound = errors.New("dependency not found in go mod file") ErrVersionMismatch = errors.New("mismatch in go.mod and builder configuration versions") - errDownloadFailed = errors.New("failed to download go modules") errCompileFailed = errors.New("failed to compile the OpenTelemetry Collector distribution") skipStrictMsg = "Use --skip-strict-versioning to temporarily disable this check. This flag will be removed in a future minor version" ) @@ -86,18 +84,30 @@ func Generate(cfg Config) error { return fmt.Errorf("failed to create output path: %w", err) } - for _, tmpl := range []*template.Template{ + allTemplates := []*template.Template{ mainTemplate, mainOthersTemplate, mainWindowsTemplate, componentsTemplate, - goModTemplate, - } { + } + + // Add the go.mod template unless that file is skipped. + if !cfg.SkipNewGoModule { + allTemplates = append(allTemplates, goModTemplate) + } + + for _, tmpl := range allTemplates { if err := processAndWrite(cfg, tmpl, tmpl.Name(), cfg); err != nil { return fmt.Errorf("failed to generate source file %q: %w", tmpl.Name(), err) } } + // when not creating a new go.mod file, update modules one-by-one in the + // enclosing go module. + if err := cfg.updateModules(); err != nil { + return err + } + cfg.Logger.Info("Sources created", zap.String("path", cfg.Distribution.OutputPath)) return nil } @@ -145,7 +155,7 @@ func GetModules(cfg Config) error { } if cfg.SkipStrictVersioning { - return downloadModules(cfg) + return nil } // Perform strict version checking. For each component listed and the @@ -185,22 +195,7 @@ func GetModules(cfg Config) error { } } - return downloadModules(cfg) -} - -func downloadModules(cfg Config) error { - cfg.Logger.Info("Getting go modules") - failReason := "unknown" - for i := 1; i <= cfg.downloadModules.numRetries; i++ { - if _, err := runGoCommand(cfg, "mod", "download"); err != nil { - failReason = err.Error() - cfg.Logger.Info("Failed modules download", zap.String("retry", fmt.Sprintf("%d/%d", i, cfg.downloadModules.numRetries))) - time.Sleep(cfg.downloadModules.wait) - continue - } - return nil - } - return fmt.Errorf("%w: %s", errDownloadFailed, failReason) + return nil } func processAndWrite(cfg Config, tmpl *template.Template, outFile string, tmplParams any) error { @@ -226,6 +221,68 @@ func (c *Config) allComponents() []Module { c.Extensions, c.Connectors, *c.Providers) } +func (c *Config) updateModules() error { + if !c.SkipNewGoModule { + return nil + } + + // Build the main service dependency + coremod, corever := c.coreModuleAndVersion() + corespec := coremod + " " + corever + + if err := c.updateGoModule(corespec); err != nil { + return err + } + + for _, comp := range c.allComponents() { + if err := c.updateGoModule(comp.GoMod); err != nil { + return err + } + } + return nil +} + +func (c *Config) updateGoModule(modspec string) error { + mod, ver, found := strings.Cut(modspec, " ") + if !found { + return fmt.Errorf("ill-formatted modspec %q: missing space separator", modspec) + } + + // Re-parse the go.mod file on each iteration, since it can + // change each time. + modulePath, dependencyVersions, err := c.readGoModFile() + if err != nil { + return err + } + + if mod == modulePath { + // this component is part of the same module, nothing to update. + return nil + } + + // check for exact match + hasVer, ok := dependencyVersions[mod] + if ok && hasVer == ver { + c.Logger.Info("Component version match", zap.String("module", mod), zap.String("version", ver)) + return nil + } + + scomp := semver.Compare(hasVer, ver) + if scomp > 0 { + // version in enclosing module is newer, do not change + c.Logger.Info("Not upgrading component, enclosing module is newer.", zap.String("module", mod), zap.String("existing", hasVer), zap.String("config_version", ver)) + return nil + } + + // upgrading or changing version + updatespec := "-require=" + mod + "@" + ver + + if _, err := runGoCommand(*c, "mod", "edit", updatespec); err != nil { + return err + } + return nil +} + func (c *Config) readGoModFile() (string, map[string]string, error) { var modPath string stdout, err := runGoCommand(*c, "mod", "edit", "-print") diff --git a/cmd/builder/internal/builder/main_test.go b/cmd/builder/internal/builder/main_test.go index 146c58b1d17..93151739252 100644 --- a/cmd/builder/internal/builder/main_test.go +++ b/cmd/builder/internal/builder/main_test.go @@ -19,23 +19,7 @@ import ( "golang.org/x/mod/modfile" ) -const ( - goModTestFile = `// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 -module go.opentelemetry.io/collector/cmd/builder/internal/tester -go 1.20 -require ( - go.opentelemetry.io/collector/component v0.96.0 - go.opentelemetry.io/collector/connector v0.94.1 - go.opentelemetry.io/collector/exporter v0.94.1 - go.opentelemetry.io/collector/extension v0.94.1 - go.opentelemetry.io/collector/otelcol v0.94.1 - go.opentelemetry.io/collector/processor v0.94.1 - go.opentelemetry.io/collector/receiver v0.94.1 - go.opentelemetry.io/collector v0.96.0 -)` - modulePrefix = "go.opentelemetry.io/collector" -) +const modulePrefix = "go.opentelemetry.io/collector" var ( replaceModules = []string{ @@ -107,18 +91,6 @@ func newInitializedConfig(t *testing.T) Config { return cfg } -func TestGenerateDefault(t *testing.T) { - require.NoError(t, Generate(newInitializedConfig(t))) -} - -func TestGenerateInvalidOutputPath(t *testing.T) { - cfg := newInitializedConfig(t) - cfg.Distribution.OutputPath = ":/invalid" - err := Generate(cfg) - require.Error(t, err) - require.Contains(t, err.Error(), "failed to create output path") -} - func TestVersioning(t *testing.T) { replaces := generateReplaces() tests := []struct { @@ -245,7 +217,7 @@ func TestSkipGenerate(t *testing.T) { cfg.SkipGenerate = true err := Generate(cfg) require.NoError(t, err) - outputFile, err := os.Open(cfg.Distribution.OutputPath) + outputFile, err := os.Open(filepath.Clean(cfg.Distribution.OutputPath)) defer func() { require.NoError(t, outputFile.Close()) }() @@ -256,10 +228,13 @@ func TestSkipGenerate(t *testing.T) { func TestGenerateAndCompile(t *testing.T) { replaces := generateReplaces() - testCases := []struct { - testCase string - cfgBuilder func(t *testing.T) Config - }{ + type testDesc struct { + testCase string + cfgBuilder func(t *testing.T) Config + verifyFiles func(t *testing.T, dir string) + expectedErr string + } + testCases := []testDesc{ { testCase: "Default Configuration Compilation", cfgBuilder: func(t *testing.T) Config { @@ -270,6 +245,68 @@ func TestGenerateAndCompile(t *testing.T) { cfg.Replaces = append(cfg.Replaces, replaces...) return cfg }, + }, { + testCase: "Skip New Gomod Configuration Compilation", + cfgBuilder: func(t *testing.T) Config { + cfg := newTestConfig() + err := cfg.SetBackwardsCompatibility() + require.NoError(t, err) + cfg.Receivers = append(cfg.Receivers, + Module{ + GoMod: "go.opentelemetry.io/collector/receiver/otlpreceiver v0.106.0", + }, + ) + cfg.Exporters = append(cfg.Exporters, + Module{ + GoMod: "go.opentelemetry.io/collector/exporter/otlpexporter v0.106.0", + }, + ) + tempDir := t.TempDir() + err = makeModule(tempDir, []byte(goModTestFile)) + require.NoError(t, err) + cfg.Distribution.OutputPath = filepath.Clean(filepath.Join(tempDir, "output")) + cfg.Replaces = nil + cfg.Excludes = nil + cfg.SkipNewGoModule = true + return cfg + }, + verifyFiles: func(t *testing.T, dir string) { + assert.FileExists(t, filepath.Clean(filepath.Join(dir, mainTemplate.Name()))) + assert.NoFileExists(t, filepath.Clean(filepath.Join(dir, "go.mod"))) + }, + }, + { + testCase: "Generate Only", + cfgBuilder: func(t *testing.T) Config { + cfg := newInitializedConfig(t) + cfg.SkipCompilation = true + cfg.SkipGetModules = true + return cfg + }, + }, + { + testCase: "Skip Everything", + cfgBuilder: func(_ *testing.T) Config { + cfg := NewDefaultConfig() + cfg.Replaces = nil + cfg.Excludes = nil + cfg.SkipCompilation = true + cfg.SkipGenerate = true + cfg.SkipGetModules = true + cfg.SkipNewGoModule = true + return cfg + }, + verifyFiles: func(t *testing.T, dir string) { + // gosec linting error: G304 Potential file inclusion via variable + // we are setting the dir + outputFile, err := os.Open(dir) //nolint:gosec + defer func() { + require.NoError(t, outputFile.Close()) + }() + require.NoError(t, err) + _, err = outputFile.Readdirnames(1) + require.ErrorIs(t, err, io.EOF, "skip generate should leave output directory empty") + }, }, { testCase: "LDFlags Compilation", @@ -348,6 +385,53 @@ func TestGenerateAndCompile(t *testing.T) { return cfg }, }, + { + testCase: "Invalid Output Path", + cfgBuilder: func(t *testing.T) Config { + cfg := newInitializedConfig(t) + cfg.Distribution.OutputPath = ":/invalid" + return cfg + }, + expectedErr: "failed to create output path", + }, + { + testCase: "Malformed Receiver", + cfgBuilder: func(t *testing.T) Config { + cfg := NewDefaultConfig() + cfg.Receivers = append(cfg.Receivers, + Module{ + Name: "missing version", + GoMod: "go.opentelemetry.io/collector/cmd/builder/unittests", + }, + ) + tempDir := t.TempDir() + err := makeModule(tempDir, []byte(goModTestFile)) + require.NoError(t, err) + cfg.Distribution.OutputPath = filepath.Clean(filepath.Join(tempDir, "output")) + cfg.Replaces = nil + cfg.Excludes = nil + cfg.SkipNewGoModule = true + return cfg + }, + expectedErr: "ill-formatted modspec", + }, + } + + // file permissions don't work the same on windows systems, so this test always passes. + if runtime.GOOS != "windows" { + testCases = append(testCases, testDesc{ + testCase: "No Dir Permissions", + cfgBuilder: func(t *testing.T) Config { + cfg := newTestConfig() + err := cfg.SetBackwardsCompatibility() + require.NoError(t, err) + cfg.Distribution.OutputPath = t.TempDir() + assert.NoError(t, os.Chmod(cfg.Distribution.OutputPath, 0400)) + cfg.Replaces = append(cfg.Replaces, replaces...) + return cfg + }, + expectedErr: "failed to generate source file", + }) } for _, tt := range testCases { @@ -356,7 +440,221 @@ func TestGenerateAndCompile(t *testing.T) { assert.NoError(t, cfg.Validate()) assert.NoError(t, cfg.SetGoPath()) assert.NoError(t, cfg.ParseModules()) - require.NoError(t, GenerateAndCompile(cfg)) + err := GenerateAndCompile(cfg) + if len(tt.expectedErr) == 0 { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, tt.expectedErr) + } + if tt.verifyFiles != nil { + tt.verifyFiles(t, cfg.Distribution.OutputPath) + } + }) + } +} + +func TestGetModules(t *testing.T) { + testCases := []struct { + description string + cfgBuilder func(t *testing.T) Config + expectedErr string + }{ + { + description: "Skip New Gomod Success", + cfgBuilder: func(t *testing.T) Config { + cfg := newTestConfig() + cfg.Distribution.Go = "go" + tempDir := t.TempDir() + require.NoError(t, makeModule(tempDir, []byte(goModTestFile))) + outputDir := filepath.Clean(filepath.Join(tempDir, "output")) + cfg.Distribution.OutputPath = outputDir + cfg.Replaces = nil + cfg.Excludes = nil + cfg.SkipNewGoModule = true + return cfg + }, + }, + { + description: "Core Version Mismatch", + cfgBuilder: func(t *testing.T) Config { + cfg := newTestConfig() + cfg.Distribution.Go = "go" + cfg.Distribution.OtelColVersion = "0.100.0" + tempDir := t.TempDir() + require.NoError(t, makeModule(tempDir, []byte(goModTestFile))) + outputDir := filepath.Clean(filepath.Join(tempDir, "output")) + cfg.Distribution.OutputPath = outputDir + cfg.Replaces = nil + cfg.Excludes = nil + cfg.SkipNewGoModule = true + return cfg + }, + expectedErr: ErrVersionMismatch.Error(), + }, + { + description: "No Go Distribution", + cfgBuilder: func(_ *testing.T) Config { + cfg := NewDefaultConfig() + cfg.downloadModules.wait = 0 + return cfg + }, + expectedErr: "failed to update go.mod", + }, + { + description: "Invalid Dependency", + cfgBuilder: func(t *testing.T) Config { + cfg := NewDefaultConfig() + cfg.downloadModules.wait = 0 + cfg.Distribution.Go = "go" + tempDir := t.TempDir() + require.NoError(t, makeModule(tempDir, []byte(invalidDependencyGoMod))) + outputDir := filepath.Clean(filepath.Join(tempDir, "output")) + cfg.Distribution.OutputPath = outputDir + cfg.Replaces = nil + cfg.Excludes = nil + cfg.SkipNewGoModule = true + return cfg + }, + expectedErr: "failed to update go.mod", + }, + { + description: "Malformed Go Mod", + cfgBuilder: func(t *testing.T) Config { + cfg := NewDefaultConfig() + cfg.downloadModules.wait = 0 + cfg.Distribution.Go = "go" + tempDir := t.TempDir() + require.NoError(t, makeModule(tempDir, []byte(malformedGoMod))) + outputDir := filepath.Clean(filepath.Join(tempDir, "output")) + cfg.Distribution.OutputPath = outputDir + cfg.Replaces = nil + cfg.Excludes = nil + cfg.SkipNewGoModule = true + return cfg + }, + expectedErr: "go subcommand failed with args '[mod edit -print]'", + }, + { + description: "Receiver Version Mismatch - Configured Lower", + cfgBuilder: func(t *testing.T) Config { + cfg := NewDefaultConfig() + cfg.Distribution.Go = "go" + cfg.Receivers = append(cfg.Receivers, + Module{ + GoMod: "go.opentelemetry.io/collector/receiver/otlpreceiver v0.105.0", + }, + ) + tempDir := t.TempDir() + err := makeModule(tempDir, []byte(goModTestFile)) + require.NoError(t, err) + cfg.Distribution.OutputPath = filepath.Clean(filepath.Join(tempDir, "output")) + cfg.Replaces = nil + cfg.Excludes = nil + cfg.SkipNewGoModule = true + return cfg + }, + expectedErr: ErrVersionMismatch.Error(), + }, + { + description: "Receiver Version Mismatch - Configured Higher", + cfgBuilder: func(t *testing.T) Config { + cfg := NewDefaultConfig() + cfg.Distribution.Go = "go" + cfg.Receivers = append(cfg.Receivers, + Module{ + GoMod: "go.opentelemetry.io/collector/receiver/otlpreceiver v0.106.1", + }, + ) + tempDir := t.TempDir() + err := makeModule(tempDir, []byte(goModTestFile)) + require.NoError(t, err) + cfg.Distribution.OutputPath = filepath.Clean(filepath.Join(tempDir, "output")) + cfg.Replaces = nil + cfg.Excludes = nil + cfg.SkipNewGoModule = true + return cfg + }, + }, + { + description: "Exporter Not in Gomod", + cfgBuilder: func(t *testing.T) Config { + cfg := NewDefaultConfig() + cfg.Distribution.Go = "go" + cfg.Exporters = append(cfg.Exporters, + Module{ + GoMod: "go.opentelemetry.io/collector/exporter/otlpexporter v0.106.0", + }, + ) + tempDir := t.TempDir() + err := makeModule(tempDir, []byte(goModTestFile)) + require.NoError(t, err) + cfg.Distribution.OutputPath = filepath.Clean(filepath.Join(tempDir, "output")) + cfg.Replaces = nil + cfg.Excludes = nil + cfg.SkipNewGoModule = true + return cfg + }, + }, + { + description: "Receiver Nonexistent Version", + cfgBuilder: func(t *testing.T) Config { + cfg := NewDefaultConfig() + cfg.Distribution.Go = "go" + cfg.Receivers = append(cfg.Receivers, + Module{ + GoMod: "go.opentelemetry.io/collector/receiver/otlpreceiver v0.106.2", + }, + ) + tempDir := t.TempDir() + err := makeModule(tempDir, []byte(goModTestFile)) + require.NoError(t, err) + cfg.Distribution.OutputPath = filepath.Clean(filepath.Join(tempDir, "output")) + cfg.Replaces = nil + cfg.Excludes = nil + cfg.SkipNewGoModule = true + return cfg + }, + expectedErr: "failed to update go.mod", + }, + { + description: "Receiver In Current Module", + cfgBuilder: func(t *testing.T) Config { + cfg := NewDefaultConfig() + cfg.Distribution.Go = "go" + cfg.Receivers = append(cfg.Receivers, + Module{ + GoMod: "go.opentelemetry.io/collector/cmd/builder/unittests v0.0.0", + }, + ) + tempDir := t.TempDir() + err := makeModule(tempDir, []byte(goModTestFile)) + require.NoError(t, err) + cfg.Distribution.OutputPath = filepath.Clean(filepath.Join(tempDir, "output")) + cfg.Replaces = nil + cfg.Excludes = nil + cfg.SkipNewGoModule = true + return cfg + }, + expectedErr: "failed to update go.mod", + }, + } + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + cfg := tc.cfgBuilder(t) + require.NoError(t, cfg.SetBackwardsCompatibility()) + require.NoError(t, cfg.Validate()) + require.NoError(t, cfg.ParseModules()) + // GenerateAndCompile calls GetModules(). We want to call Generate() + // first so our dependencies stay in the gomod after go mod tidy. + err := GenerateAndCompile(cfg) + if len(tc.expectedErr) == 0 { + if !assert.NoError(t, err) { + mf, mvm, readErr := cfg.readGoModFile() + t.Log("go mod file", mf, mvm, readErr) + } + return + } + assert.ErrorContains(t, err, tc.expectedErr) }) } } diff --git a/cmd/builder/internal/builder/modfiles_test.go b/cmd/builder/internal/builder/modfiles_test.go new file mode 100644 index 00000000000..45d478975ba --- /dev/null +++ b/cmd/builder/internal/builder/modfiles_test.go @@ -0,0 +1,85 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package builder + +const ( + goModTestFile = `// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +module go.opentelemetry.io/collector/cmd/builder/unittests + +go 1.21.0 + +require ( + go.opentelemetry.io/collector/component v0.106.0 + go.opentelemetry.io/collector/confmap v0.106.0 + go.opentelemetry.io/collector/confmap/converter/expandconverter v0.106.0 + go.opentelemetry.io/collector/confmap/provider/envprovider v0.106.0 + go.opentelemetry.io/collector/confmap/provider/fileprovider v0.106.0 + go.opentelemetry.io/collector/confmap/provider/httpprovider v0.106.0 + go.opentelemetry.io/collector/confmap/provider/httpsprovider v0.106.0 + go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.106.0 + go.opentelemetry.io/collector/connector v0.106.0 + go.opentelemetry.io/collector/exporter v0.106.0 + go.opentelemetry.io/collector/exporter/otlpexporter v0.106.0 + go.opentelemetry.io/collector/extension v0.106.0 + go.opentelemetry.io/collector/otelcol v0.106.0 + go.opentelemetry.io/collector/processor v0.106.0 + go.opentelemetry.io/collector/receiver v0.106.0 + go.opentelemetry.io/collector/receiver/otlpreceiver v0.106.0 + golang.org/x/sys v0.20.0 +)` + + invalidDependencyGoMod = `// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +module go.opentelemetry.io/collector/cmd/builder/unittests + +go 1.21.0 + +require ( + go.opentelemetry.io/collector/bad/otelcol v0.94.1 + go.opentelemetry.io/collector/component v0.102.1 + go.opentelemetry.io/collector/confmap v0.102.1 + go.opentelemetry.io/collector/confmap/converter/expandconverter v0.102.1 + go.opentelemetry.io/collector/confmap/provider/envprovider v0.102.1 + go.opentelemetry.io/collector/confmap/provider/fileprovider v0.102.1 + go.opentelemetry.io/collector/confmap/provider/httpprovider v0.102.1 + go.opentelemetry.io/collector/confmap/provider/httpsprovider v0.102.1 + go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.102.1 + go.opentelemetry.io/collector/connector v0.102.1 + go.opentelemetry.io/collector/exporter v0.102.1 + go.opentelemetry.io/collector/exporter/otlpexporter v0.102.1 + go.opentelemetry.io/collector/extension v0.102.1 + go.opentelemetry.io/collector/otelcol v0.102.1 + go.opentelemetry.io/collector/processor v0.102.1 + go.opentelemetry.io/collector/receiver v0.102.1 + go.opentelemetry.io/collector/receiver/otlpreceiver v0.102.1 + golang.org/x/sys v0.20.0 +)` + + malformedGoMod = `// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +module go.opentelemetry.io/collector/cmd/builder/unittests + +go 1.21.0 + +require ( + go.opentelemetry.io/collector/componentv0.102.1 + go.opentelemetry.io/collector/confmap v0.102.1 + go.opentelemetry.io/collector/confmap/converter/expandconverter v0.102.1 + go.opentelemetry.io/collector/confmap/provider/envprovider v0.102.1 + go.opentelemetry.io/collector/confmap/provider/fileprovider v0.102.1 + go.opentelemetry.io/collector/confmap/provider/httpprovider v0.102.1 + go.opentelemetry.io/collector/confmap/provider/httpsprovider v0.102.1 + go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.102.1 + go.opentelemetry.io/collector/connector v0.102.1 + go.opentelemetry.io/collector/exporter v0.102.1 + go.opentelemetry.io/collector/exporter/otlpexporter v0.102.1 + go.opentelemetry.io/collector/extension v0.102.1 + go.opentelemetry.io/collector/otelcol v0.102.1 + go.opentelemetry.io/collector/processor v0.102.1 + go.opentelemetry.io/collector/receiver v0.102.1 + go.opentelemetry.io/collector/receiver/otlpreceiver v0.102.1 + golang.org/x/sys v0.20.0 +)` +) diff --git a/cmd/builder/internal/command.go b/cmd/builder/internal/command.go index a738fb1c1f1..41b7d6e4d95 100644 --- a/cmd/builder/internal/command.go +++ b/cmd/builder/internal/command.go @@ -23,6 +23,7 @@ const ( skipGenerateFlag = "skip-generate" skipCompilationFlag = "skip-compilation" skipGetModulesFlag = "skip-get-modules" + skipNewGoModuleFlag = "skip-new-go-module" skipStrictVersioningFlag = "skip-strict-versioning" ldflagsFlag = "ldflags" distributionNameFlag = "name" @@ -84,6 +85,7 @@ configuration is provided, ocb will generate a default Collector. cmd.Flags().BoolVar(&cfg.SkipGenerate, skipGenerateFlag, false, "Whether builder should skip generating go code (default false)") cmd.Flags().BoolVar(&cfg.SkipCompilation, skipCompilationFlag, false, "Whether builder should only generate go code with no compile of the collector (default false)") cmd.Flags().BoolVar(&cfg.SkipGetModules, skipGetModulesFlag, false, "Whether builder should skip updating go.mod and retrieve Go module list (default false)") + cmd.Flags().BoolVar(&cfg.SkipNewGoModule, skipNewGoModuleFlag, false, "Whether builder should skip generating a new go.mod file, using the enclosing Go module instead (default false)") cmd.Flags().BoolVar(&cfg.SkipStrictVersioning, skipStrictVersioningFlag, false, "Whether builder should skip strictly checking the calculated versions following dependency resolution") cmd.Flags().BoolVar(&cfg.Verbose, verboseFlag, false, "Whether builder should print verbose output (default false)") cmd.Flags().StringVar(&cfg.LDFlags, ldflagsFlag, "", `ldflags to include in the "go build" command`) @@ -185,6 +187,9 @@ func applyCfgFromFile(flags *flag.FlagSet, cfgFromFile builder.Config) { if !flags.Changed(skipGetModulesFlag) && cfgFromFile.SkipGetModules { cfg.SkipGetModules = cfgFromFile.SkipGetModules } + if !flags.Changed(skipNewGoModuleFlag) && cfgFromFile.SkipNewGoModule { + cfg.SkipNewGoModule = cfgFromFile.SkipNewGoModule + } if !flags.Changed(skipStrictVersioningFlag) && cfgFromFile.SkipStrictVersioning { cfg.SkipStrictVersioning = cfgFromFile.SkipStrictVersioning } diff --git a/cmd/builder/internal/command_test.go b/cmd/builder/internal/command_test.go index d071efb312d..c7d1b27e535 100644 --- a/cmd/builder/internal/command_test.go +++ b/cmd/builder/internal/command_test.go @@ -248,6 +248,54 @@ func Test_applyCfgFromFile(t *testing.T) { }, wantErr: false, }, + { + name: "Skip new go mod false", + args: args{ + flags: flag.NewFlagSet("version=1.0.0", 1), + cfgFromFile: builder.Config{ + Logger: zap.NewNop(), + SkipGenerate: true, + SkipCompilation: true, + SkipGetModules: true, + SkipNewGoModule: false, + Distribution: testDistribution, + }, + }, + want: builder.Config{ + Logger: zap.NewNop(), + SkipGenerate: true, + SkipCompilation: true, + SkipGetModules: true, + SkipStrictVersioning: true, + SkipNewGoModule: false, + Distribution: testDistribution, + }, + wantErr: false, + }, + { + name: "Skip new go mod true", + args: args{ + flags: flag.NewFlagSet("version=1.0.0", 1), + cfgFromFile: builder.Config{ + Logger: zap.NewNop(), + SkipGenerate: true, + SkipCompilation: true, + SkipGetModules: true, + SkipNewGoModule: true, + Distribution: testDistribution, + }, + }, + want: builder.Config{ + Logger: zap.NewNop(), + SkipGenerate: true, + SkipCompilation: true, + SkipGetModules: true, + SkipStrictVersioning: true, + SkipNewGoModule: true, + Distribution: testDistribution, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {