diff --git a/internal/cmd/resolve/resolve.go b/internal/cmd/resolve/resolve.go index 59923783..080d39bb 100644 --- a/internal/cmd/resolve/resolve.go +++ b/internal/cmd/resolve/resolve.go @@ -15,12 +15,14 @@ var ( exclusions = file.Exclusions() verbose bool npmPreferred bool + regenerate int ) const ( ExclusionFlag = "exclusion" VerboseFlag = "verbose" NpmPreferredFlag = "prefer-npm" + RegenerateFlag = "regenerate" ) func NewResolveCmd(resolver resolution.IResolver) *cobra.Command { @@ -53,6 +55,17 @@ Exclude flags could alternatively be set using DEBRICKED_EXCLUSIONS="path1,path2 Example: $ debricked resolve . `+exampleFlags) cmd.Flags().BoolVar(&verbose, VerboseFlag, true, "set to false to disable extensive resolution error messages") + regenerateDoc := strings.Join( + []string{ + "Toggles regeneration of already existing lock files between 3 modes:\n", + "Force Regeneration Level | Meaning", + "------------------------ | -------", + "0 (default) | No regeneration", + "1 | Regenerates existing non package manager native Debricked lock files", + "2 | Regenerates all existing lock files", + "\nExample:\n$ debricked resolve . --regenerate=1", + }, "\n") + cmd.Flags().IntVar(®enerate, RegenerateFlag, 0, regenerateDoc) npmPreferredDoc := strings.Join( []string{ @@ -73,9 +86,13 @@ func RunE(resolver resolution.IResolver) func(_ *cobra.Command, args []string) e if len(args) == 0 { args = append(args, ".") } - - resolver.SetNpmPreferred(viper.GetBool(NpmPreferredFlag)) - _, err := resolver.Resolve(args, viper.GetStringSlice(ExclusionFlag), viper.GetBool(VerboseFlag)) + options := resolution.DebrickedOptions{ + Exclusions: viper.GetStringSlice(ExclusionFlag), + Verbose: viper.GetBool(VerboseFlag), + Regenerate: viper.GetInt(RegenerateFlag), + NpmPreferred: viper.GetBool(NpmPreferredFlag), + } + _, err := resolver.Resolve(args, options) return err } diff --git a/internal/cmd/scan/scan.go b/internal/cmd/scan/scan.go index 7d3f2fa5..7557770f 100644 --- a/internal/cmd/scan/scan.go +++ b/internal/cmd/scan/scan.go @@ -21,6 +21,7 @@ var repositoryUrl string var integrationName string var exclusions = file.Exclusions() var verbose bool +var regenerate int var noResolve bool var noFingerprint bool var passOnDowntime bool @@ -38,6 +39,7 @@ const ( IntegrationFlag = "integration" ExclusionFlag = "exclusion" VerboseFlag = "verbose" + RegenerateFlag = "regenerate" NoResolveFlag = "no-resolve" FingerprintFlag = "fingerprint" PassOnTimeOut = "pass-on-timeout" @@ -95,6 +97,17 @@ Exclude flags could alternatively be set using DEBRICKED_EXCLUSIONS="path1,path2 Examples: $ debricked scan . `+exampleFlags) cmd.Flags().BoolVar(&verbose, VerboseFlag, true, "set to false to disable extensive resolution error messages") + regenerateDoc := strings.Join( + []string{ + "Toggles regeneration of already existing lock files between 3 modes:\n", + "Force Regeneration Level | Meaning", + "------------------------ | -------", + "0 (default) | No regeneration", + "1 | Regenerates existing non package manager native Debricked lock files", + "2 | Regenerates all existing lock files", + "\nExample:\n$ debricked resolve . --regenerate=1", + }, "\n") + cmd.Flags().IntVar(®enerate, RegenerateFlag, 0, regenerateDoc) cmd.Flags().BoolVarP(&passOnDowntime, PassOnTimeOut, "p", false, "pass scan if there is a service access timeout") cmd.Flags().BoolVar(&noResolve, NoResolveFlag, false, `disables resolution of manifest files that lack lock files. Resolving manifest files enables more accurate dependency scanning since the whole dependency tree will be analysed. For example, if there is a "go.mod" in the target path, its dependencies are going to get resolved onto a lock file, and latter scanned.`) @@ -136,6 +149,7 @@ func RunE(s *scan.IScanner) func(_ *cobra.Command, args []string) error { Fingerprint: viper.GetBool(FingerprintFlag), Exclusions: viper.GetStringSlice(ExclusionFlag), Verbose: viper.GetBool(VerboseFlag), + Regenerate: viper.GetInt(RegenerateFlag), RepositoryName: viper.GetString(RepositoryFlag), CommitName: viper.GetString(CommitFlag), BranchName: viper.GetString(BranchFlag), diff --git a/internal/resolution/resolver.go b/internal/resolution/resolver.go index 061bf7b4..e5990c71 100644 --- a/internal/resolution/resolver.go +++ b/internal/resolution/resolver.go @@ -1,8 +1,10 @@ package resolution import ( + "errors" "os" "path" + "regexp" "github.com/debricked/cli/internal/file" resolutionFile "github.com/debricked/cli/internal/resolution/file" @@ -11,9 +13,12 @@ import ( "github.com/debricked/cli/internal/tui" ) +var ( + BadOptsErr = errors.New("failed to type case IOptions") +) + type IResolver interface { - Resolve(paths []string, exclusions []string, verbose bool) (IResolution, error) - SetNpmPreferred(npmPreferred bool) + Resolve(paths []string, options IOptions) (IResolution, error) } type Resolver struct { @@ -24,6 +29,16 @@ type Resolver struct { npmPreferred bool } +type IOptions interface{} + +type DebrickedOptions struct { + Path string + Exclusions []string + Verbose bool + Regenerate int + NpmPreferred bool +} + func NewResolver( finder file.IFinder, batchFactory resolutionFile.IBatchFactory, @@ -39,16 +54,20 @@ func NewResolver( } } -func (r Resolver) SetNpmPreferred(npmPreferred bool) { +func (r Resolver) setNpmPreferred(npmPreferred bool) { r.batchFactory.SetNpmPreferred(npmPreferred) } -func (r Resolver) Resolve(paths []string, exclusions []string, verbose bool) (IResolution, error) { - files, err := r.refinePaths(paths, exclusions) +func (r Resolver) Resolve(paths []string, options IOptions) (IResolution, error) { + dOptions, ok := options.(DebrickedOptions) + if !ok { + return nil, BadOptsErr + } + files, err := r.refinePaths(paths, dOptions.Exclusions, dOptions.Regenerate) if err != nil { return nil, err } - + r.setNpmPreferred(dOptions.NpmPreferred) pmBatches := r.batchFactory.Make(files) var jobs []job.IJob @@ -67,13 +86,13 @@ func (r Resolver) Resolve(paths []string, exclusions []string, verbose bool) (IR if resolution.HasErr() { jobErrList := tui.NewJobsErrorList(os.Stdout, resolution.Jobs()) - err = jobErrList.Render(verbose) + err = jobErrList.Render(dOptions.Verbose) } return resolution, err } -func (r Resolver) refinePaths(paths []string, exclusions []string) ([]string, error) { +func (r Resolver) refinePaths(paths []string, exclusions []string, regenerate int) ([]string, error) { var fileSet = map[string]bool{} var dirs []string for _, arg := range paths { @@ -96,7 +115,7 @@ func (r Resolver) refinePaths(paths []string, exclusions []string) ([]string, er } } - err := r.searchDirs(fileSet, dirs, exclusions) + err := r.searchDirs(fileSet, dirs, exclusions, regenerate) if err != nil { return nil, err } @@ -109,7 +128,7 @@ func (r Resolver) refinePaths(paths []string, exclusions []string) ([]string, er return files, nil } -func (r Resolver) searchDirs(fileSet map[string]bool, dirs []string, exclusions []string) error { +func (r Resolver) searchDirs(fileSet map[string]bool, dirs []string, exclusions []string, regenerate int) error { for _, dir := range dirs { fileGroups, err := r.finder.GetGroups( dir, @@ -121,7 +140,8 @@ func (r Resolver) searchDirs(fileSet map[string]bool, dirs []string, exclusions return err } for _, fileGroup := range fileGroups.ToSlice() { - if fileGroup.HasFile() && !fileGroup.HasLockFiles() { + shouldGenerate := shouldGenerateLock(fileGroup, regenerate) + if shouldGenerate { fileSet[fileGroup.ManifestFile] = true } } @@ -129,3 +149,31 @@ func (r Resolver) searchDirs(fileSet map[string]bool, dirs []string, exclusions return nil } + +func shouldGenerateLock(fileGroup file.Group, regenerate int) bool { + if !fileGroup.HasFile() { + return false + } + switch regenerate { + case 0: + return !fileGroup.HasLockFiles() + case 1: + return onlyNonNativeLockFiles(fileGroup.LockFiles) + case 2: + return true + } + + return false +} + +func onlyNonNativeLockFiles(lockFiles []string) bool { + debrickedLockFilePattern := regexp.MustCompile(`.*\.debricked\.lock`) + for _, lockFile := range lockFiles { + if !debrickedLockFilePattern.MatchString(lockFile) { + return false + } + } + + return true + +} diff --git a/internal/resolution/resolver_test.go b/internal/resolution/resolver_test.go index 749fc0c7..9b176e2a 100644 --- a/internal/resolution/resolver_test.go +++ b/internal/resolution/resolver_test.go @@ -39,8 +39,12 @@ func TestResolve(t *testing.T) { strategyTestdata.NewStrategyFactoryMock(), NewScheduler(workers), ) - - res, err := r.Resolve([]string{"../../go.mod"}, nil, true) + options := DebrickedOptions{ + Exclusions: nil, + Verbose: true, + Regenerate: 0, + } + res, err := r.Resolve([]string{"../../go.mod"}, options) assert.NotEmpty(t, res.Jobs()) assert.NoError(t, err) } @@ -52,8 +56,12 @@ func TestResolveInvokeError(t *testing.T) { strategyTestdata.NewStrategyFactoryErrorMock(), NewScheduler(workers), ) - - _, err := r.Resolve([]string{"../../go.mod"}, nil, true) + options := DebrickedOptions{ + Exclusions: nil, + Verbose: true, + Regenerate: 0, + } + _, err := r.Resolve([]string{"../../go.mod"}, options) assert.NotNil(t, err) } @@ -65,7 +73,12 @@ func TestResolveStrategyError(t *testing.T) { NewScheduler(workers), ) - res, err := r.Resolve([]string{"../../go.mod"}, nil, true) + options := DebrickedOptions{ + Exclusions: nil, + Verbose: true, + Regenerate: 0, + } + res, err := r.Resolve([]string{"../../go.mod"}, options) assert.Empty(t, res.Jobs()) assert.NoError(t, err) } @@ -79,7 +92,12 @@ func TestResolveScheduleError(t *testing.T) { SchedulerMock{Err: errAssertion}, ) - res, err := r.Resolve([]string{"../../go.mod"}, nil, true) + options := DebrickedOptions{ + Exclusions: nil, + Verbose: true, + Regenerate: 0, + } + res, err := r.Resolve([]string{"../../go.mod"}, options) assert.NotEmpty(t, res.Jobs()) assert.ErrorIs(t, err, errAssertion) } @@ -92,7 +110,12 @@ func TestResolveDirWithoutManifestFiles(t *testing.T) { SchedulerMock{}, ) - res, err := r.Resolve([]string{"."}, nil, true) + options := DebrickedOptions{ + Exclusions: nil, + Verbose: true, + Regenerate: 0, + } + res, err := r.Resolve([]string{"."}, options) assert.Empty(t, res.Jobs()) assert.NoError(t, err) } @@ -105,7 +128,12 @@ func TestResolveInvalidDir(t *testing.T) { SchedulerMock{}, ) - _, err := r.Resolve([]string{"invalid-dir"}, nil, true) + options := DebrickedOptions{ + Exclusions: nil, + Verbose: true, + Regenerate: 0, + } + _, err := r.Resolve([]string{"invalid-dir"}, options) assert.Error(t, err) } @@ -121,7 +149,12 @@ func TestResolveGetGroupsErr(t *testing.T) { SchedulerMock{}, ) - _, err := r.Resolve([]string{"."}, nil, true) + options := DebrickedOptions{ + Exclusions: nil, + Verbose: true, + Regenerate: 0, + } + _, err := r.Resolve([]string{"."}, options) assert.ErrorIs(t, testErr, err) } @@ -147,9 +180,14 @@ func TestResolveDirWithManifestFiles(t *testing.T) { SchedulerMock{}, ) - for _, dir := range cases { + for i, dir := range cases { + options := DebrickedOptions{ + Exclusions: nil, + Verbose: true, + Regenerate: i % 3, // To test the different regenerate values + } t.Run(fmt.Sprintf("Case: %s", dir), func(t *testing.T) { - res, err := r.Resolve([]string{dir}, nil, true) + res, err := r.Resolve([]string{dir}, options) assert.Len(t, res.Jobs(), 1) j := res.Jobs()[0] assert.False(t, j.Errors().HasError()) @@ -172,7 +210,12 @@ func TestResolveDirWithExclusions(t *testing.T) { SchedulerMock{}, ) - res, err := r.Resolve([]string{"."}, []string{"dir"}, true) + options := DebrickedOptions{ + Exclusions: []string{"dir"}, + Verbose: true, + Regenerate: 0, + } + res, err := r.Resolve([]string{"."}, options) assert.Len(t, res.Jobs(), 1) j := res.Jobs()[0] @@ -199,7 +242,12 @@ func TestResolveHasResolutionErrs(t *testing.T) { schedulerMock, ) - res, err := r.Resolve([]string{""}, []string{""}, true) + options := DebrickedOptions{ + Exclusions: []string{""}, + Verbose: true, + Regenerate: 0, + } + res, err := r.Resolve([]string{""}, options) assert.NoError(t, err) assert.Len(t, res.Jobs(), 1) diff --git a/internal/resolution/testdata/resolver_mock.go b/internal/resolution/testdata/resolver_mock.go index 392030b0..0fda1eb7 100644 --- a/internal/resolution/testdata/resolver_mock.go +++ b/internal/resolution/testdata/resolver_mock.go @@ -1,10 +1,11 @@ package testdata import ( - "github.com/debricked/cli/internal/resolution" - "github.com/debricked/cli/internal/resolution/job" "os" "path/filepath" + + "github.com/debricked/cli/internal/resolution" + "github.com/debricked/cli/internal/resolution/job" ) type ResolverMock struct { @@ -15,7 +16,7 @@ type ResolverMock struct { func (r *ResolverMock) SetNpmPreferred(_ bool) { } -func (r *ResolverMock) Resolve(_ []string, _ []string, _ bool) (resolution.IResolution, error) { +func (r *ResolverMock) Resolve(_ []string, _ resolution.IOptions) (resolution.IResolution, error) { for _, f := range r.files { createdFile, err := os.Create(f) if err != nil { diff --git a/internal/scan/scanner.go b/internal/scan/scanner.go index e349d569..f758c7c9 100644 --- a/internal/scan/scanner.go +++ b/internal/scan/scanner.go @@ -48,6 +48,7 @@ type DebrickedOptions struct { CallGraph bool Exclusions []string Verbose bool + Regenerate int RepositoryName string CommitName string BranchName string @@ -133,9 +134,15 @@ func (dScanner *DebrickedScanner) Scan(o IOptions) error { } func (dScanner *DebrickedScanner) scanResolve(options DebrickedOptions) error { + resolveOptions := resolution.DebrickedOptions{ + Path: options.Path, + Verbose: options.Verbose, + Regenerate: options.Regenerate, + Exclusions: options.Exclusions, + NpmPreferred: options.NpmPreferred, + } if options.Resolve { - dScanner.resolver.SetNpmPreferred(options.NpmPreferred) - _, resErr := dScanner.resolver.Resolve([]string{options.Path}, options.Exclusions, options.Verbose) + _, resErr := dScanner.resolver.Resolve([]string{options.Path}, resolveOptions) if resErr != nil { return resErr }