Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
85d659a
initial structure of command
vinit-chauhan Oct 9, 2025
17e4b39
fix: error skipped in edit dashboard command
vinit-chauhan Oct 9, 2025
01a681c
fix: error skipped in edit dashboard command
vinit-chauhan Oct 9, 2025
010f1a4
code refactors
vinit-chauhan Oct 10, 2025
358f9d6
Merge branch 'elastic:main' into cmd-filter
vinit-chauhan Oct 10, 2025
c3fef19
update response format.
vinit-chauhan Oct 14, 2025
3d84151
move filter code to internal
vinit-chauhan Oct 14, 2025
9a1c2aa
ignore .vscode dir
vinit-chauhan Oct 14, 2025
1336ff2
no args will return list of all packages
vinit-chauhan Oct 14, 2025
b588164
Merge branch 'main' of github.com:elastic/elastic-package into cmd-fi…
vinit-chauhan Oct 14, 2025
53bd4f8
initial implementation for foreach.
vinit-chauhan Oct 15, 2025
2d827cc
run multiple commands
vinit-chauhan Oct 15, 2025
bc7a167
add option to run commands in parallel
vinit-chauhan Oct 15, 2025
ada16f7
stash: filter-registry
vinit-chauhan Oct 16, 2025
9123dae
working with subcommand flags
vinit-chauhan Oct 16, 2025
423f035
Merge branch 'cmd-filter' into implement-filter-registry
vinit-chauhan Oct 16, 2025
2e3721c
created structure with working input filter
vinit-chauhan Oct 16, 2025
8180ff4
minor refactor; add code owner flag
vinit-chauhan Oct 16, 2025
f2abc11
minor refactor
vinit-chauhan Oct 16, 2025
f72ea26
add category flag
vinit-chauhan Oct 16, 2025
5c4d63e
refactor code
vinit-chauhan Oct 16, 2025
d94e9b2
added spec version flag
vinit-chauhan Oct 20, 2025
3cc0824
fix issue with unused filters applied
vinit-chauhan Oct 21, 2025
78cce2b
Merge branch 'main' into implement-filter-registry
vinit-chauhan Oct 21, 2025
2656a22
Merge branch 'elastic:main' into implement-filter-registry
vinit-chauhan Oct 21, 2025
c36efd7
run command in parallel
vinit-chauhan Oct 21, 2025
0649c73
remove --exec to leverage -- to pass sub commands without string
vinit-chauhan Oct 22, 2025
5e3b8b0
add subcommand allowlist
vinit-chauhan Oct 22, 2025
df2c85b
create map of dir_name and manifest to allow different package_name i…
vinit-chauhan Oct 22, 2025
b495ced
add flag suffix to go files
vinit-chauhan Oct 22, 2025
812530c
add integration type flag
vinit-chauhan Oct 22, 2025
e3e3c9a
add package name flag
vinit-chauhan Oct 22, 2025
54fce2b
minor refactors
vinit-chauhan Oct 23, 2025
13f348f
use glob instead of regex.
vinit-chauhan Oct 24, 2025
b10bb7f
removing duplicate code because I overlooked it while merging branches.
vinit-chauhan Oct 24, 2025
ab72763
move allowlist out of the validateSubCommand func
vinit-chauhan Oct 24, 2025
005c2c3
uniform command flag name.
vinit-chauhan Oct 24, 2025
8189930
create a type to hold dirname and manifest
vinit-chauhan Oct 27, 2025
caaff9a
lint and build docs
vinit-chauhan Oct 27, 2025
f997640
solve some review comments
vinit-chauhan Nov 5, 2025
a9699e4
- add way to filter packages in arbitrary directory.
vinit-chauhan Nov 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ elastic-package
# IDEA
.idea

# VSCode
.vscode

# Build directory
/build

Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,24 @@ Use this command to export ingest pipelines with referenced pipelines from the E

Use this command to download selected ingest pipelines and its referenced processor pipelines from Elasticsearch. Select data stream or the package root directories to download the pipelines. Pipelines are downloaded as is and will need adjustment to meet your package needs.

### `elastic-package filter [flags]`

_Context: package_

This command gives you a list of all packages based on the given query
Comment on lines +369 to +373
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #2327 this functionality was proposed as a subcommand that is only available under foreach. wdyt about this approach?

This could be implemented if as proposed in https://github.com/elastic/elastic-package/pull/3027/files#r2469930805, we create another root command for foreach with its own allowed subcommands, that could be a different subset to the ones available in the root command.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea behind a separate filter command is that it allows us users pipe its output to run arbitrary scripts for bulk operations which we don't support.
Moreover, both filter and foreach utilizes same code. If we add a new flag in filter it will directly be available to foreach without any code change in foreach. So it's just a way to allow users to fetch a list of integrations for given condition.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it allows us users pipe its output to run arbitrary scripts for bulk operations which we don't support.

Yes, completely agree with this. But making filter a subcommand of foreach would allow to have all this kind of functionality intended for multiple packages under an only subcommand.


### `elastic-package foreach [flags] -- <SUBCOMMAND>`

_Context: package_

Execute a command for each package matching the given filter criteria.

This command combines filtering capabilities with command execution, allowing you to run
any elastic-package subcommand across multiple packages in a single operation.

The command uses the same filter flags as the 'filter' command to select packages,
then executes the specified subcommand for each matched package.

### `elastic-package format`

_Context: package_
Expand Down
118 changes: 118 additions & 0 deletions cmd/filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package cmd

import (
"encoding/json"
"fmt"
"io"
"os"
"slices"

"github.com/spf13/cobra"

"github.com/elastic/elastic-package/internal/cobraext"
"github.com/elastic/elastic-package/internal/filter"
"github.com/elastic/elastic-package/internal/packages"
)

const filterLongDescription = `This command gives you a list of all packages based on the given query`

func setupFilterCommand() *cobraext.Command {
cmd := &cobra.Command{
Use: "filter [flags]",
Short: "filter integrations based on given flags",
Long: filterLongDescription,
Args: cobra.NoArgs,
RunE: filterCommandAction,
}

// add filter flags to the command (input, code owner, kibana version, categories)
filter.SetFilterFlags(cmd)

// add the output package name and absolute path flags to the command
cmd.Flags().BoolP(cobraext.FilterOutputPackageNameFlagName, "", false, cobraext.FilterOutputPackageNameFlagDescription)
cmd.Flags().BoolP(cobraext.FilterOutputAbsolutePathFlagName, "", false, cobraext.FilterOutputAbsolutePathFlagDescription)

return cobraext.NewCommand(cmd, cobraext.ContextPackage)
}

func filterCommandAction(cmd *cobra.Command, args []string) error {
filtered, err := filterPackage(cmd)
if err != nil {
return fmt.Errorf("filtering packages failed: %w", err)
}

printPackageName, err := cmd.Flags().GetBool(cobraext.FilterOutputPackageNameFlagName)
if err != nil {
return fmt.Errorf("getting output package name flag failed: %w", err)
}

outputAbsolutePath, err := cmd.Flags().GetBool("output-absolute-path")
if err != nil {
return fmt.Errorf("getting output absolute path flag failed: %w", err)
}

if err = printPkgList(filtered, printPackageName, outputAbsolutePath, os.Stdout); err != nil {
return fmt.Errorf("printing JSON failed: %w", err)
}

return nil
}

func filterPackage(cmd *cobra.Command) ([]packages.PackageDirNameAndManifest, error) {
depth, err := cmd.Flags().GetInt(cobraext.FilterDepthFlagName)
if err != nil {
return nil, fmt.Errorf("getting depth flag failed: %w", err)
}

excludeDirs, err := cmd.Flags().GetString(cobraext.FilterExcludeDirFlagName)
if err != nil {
return nil, fmt.Errorf("getting exclude-dir flag failed: %w", err)
}

filters := filter.NewFilterRegistry(depth, excludeDirs)

if err := filters.Parse(cmd); err != nil {
return nil, fmt.Errorf("parsing filter options failed: %w", err)
}

if err := filters.Validate(); err != nil {
return nil, fmt.Errorf("validating filter options failed: %w", err)
}

filtered, errors := filters.Execute()
if errors != nil {
return nil, fmt.Errorf("filtering packages failed: %s", errors.Error())
}

return filtered, nil
}

func printPkgList(pkgs []packages.PackageDirNameAndManifest, printPackageName bool, outputAbsolutePath bool, w io.Writer) error {
enc := json.NewEncoder(w)
enc.SetEscapeHTML(false)
if len(pkgs) == 0 {
return nil
}

names := make([]string, 0, len(pkgs))
if printPackageName {
for _, pkg := range pkgs {
names = append(names, pkg.Manifest.Name)
}
} else if outputAbsolutePath {
for _, pkg := range pkgs {
names = append(names, pkg.Path)
}
} else {
for _, pkg := range pkgs {
names = append(names, pkg.DirName)
}
}

slices.Sort(names)
return enc.Encode(names)
}
153 changes: 153 additions & 0 deletions cmd/foreach.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package cmd

import (
"fmt"
"io"
"os"
"os/exec"
"slices"
"strings"
"sync"

"github.com/spf13/cobra"

"github.com/elastic/elastic-package/internal/cobraext"
"github.com/elastic/elastic-package/internal/filter"
"github.com/elastic/elastic-package/internal/logger"
"github.com/elastic/elastic-package/internal/multierror"
)

const foreachLongDescription = `Execute a command for each package matching the given filter criteria.

This command combines filtering capabilities with command execution, allowing you to run
any elastic-package subcommand across multiple packages in a single operation.

The command uses the same filter flags as the 'filter' command to select packages,
then executes the specified subcommand for each matched package.`

// getAllowedSubCommands returns the list of allowed subcommands for the foreach command.
func getAllowedSubCommands() []string {
return []string{
"build",
"check",
"clean",
"format",
"install",
"lint",
"test",
"uninstall",
}
}

func setupForeachCommand() *cobraext.Command {
cmd := &cobra.Command{
Use: "foreach [flags] -- <SUBCOMMAND>",
Short: "Execute a command for filtered packages",
Long: foreachLongDescription,
Example: ` # Run system tests for packages with specific inputs
elastic-package foreach --input tcp,udp --parallel 10 -- test system -g`,
RunE: foreachCommandAction,
Args: cobra.MinimumNArgs(1),
}

// Add filter flags
filter.SetFilterFlags(cmd)

// Add pool size flag
cmd.Flags().IntP(cobraext.ForeachPoolSizeFlagName, cobraext.ForeachPoolSizeFlagShorthand, 1, cobraext.ForeachPoolSizeFlagDescription)

return cobraext.NewCommand(cmd, cobraext.ContextPackage)
}

func foreachCommandAction(cmd *cobra.Command, args []string) error {
poolSize, err := cmd.Flags().GetInt(cobraext.ForeachPoolSizeFlagName)
if err != nil {
return fmt.Errorf("getting pool size failed: %w", err)
}

if err := validateSubCommand(args[0]); err != nil {
return fmt.Errorf("validating sub command failed: %w", err)
}

// reuse filterPackage from cmd/filter.go
filtered, err := filterPackage(cmd)
if err != nil {
return fmt.Errorf("filtering packages failed: %w", err)
}

wg := sync.WaitGroup{}
mu := sync.Mutex{}
errs := multierror.Error{}
successes := 0

packagePathChan := make(chan string, poolSize)

for range poolSize {
wg.Add(1)
go func(packagePathChan <-chan string) {
defer wg.Done()
for packagePath := range packagePathChan {
err := executeCommand(args, packagePath)

mu.Lock()
if err != nil {
errs = append(errs, fmt.Errorf("executing command for package %s failed: %w", packagePath, err))
} else {
successes++
}
mu.Unlock()
}
}(packagePathChan)
}

for _, pkg := range filtered {
packagePathChan <- pkg.Path
}
close(packagePathChan)

wg.Wait()

logger.Infof("Successfully executed command for %d packages\n", successes)
logger.Infof("Errors occurred while executing command for %d packages\n", len(errs))

if errs.Error() != "" {
return fmt.Errorf("errors occurred while executing command for packages: \n%s", errs.Error())
}

return nil
}

func executeCommand(args []string, path string) error {
// Look up the elastic-package binary in PATH
execPath, err := exec.LookPath("elastic-package")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as this is a command run on top of other commands of this binary, could we target directly the commands by its functions, instead of looking for the path binary?
what if the binary is not updated or needs to run specific version? just having some thoughts here on how would be a better aproach without depending of having the binary installed as its own dependency.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be possible to do something like this:

cmd := cmd.RootCmd()
cmd.SetArgs(args)
return cmd.Execute()

We could also create our own command that is like cmd.RootCmd(), but where we only add the commands with AddCommand that we allow.

if err != nil {
return fmt.Errorf("elastic-package binary not found in PATH: %w", err)
}

cmd := &exec.Cmd{
Path: execPath,
Args: append([]string{execPath}, args...),
Dir: path,
Stdout: io.Discard,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why discarding stdout?

Stderr: os.Stderr,
Env: os.Environ(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when running the command, could we select the variables strictly needed instead of just bulking all the system ones?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This came across my mind at first but I did not go through with it.
The reason I chose to pass all the env variables, is that elastic-package already has access to all the env variables. Additionally, it saves us from future bugs where we add some capability with specific env var and forget to add it to a list on other part of codebase.

Let me know if we want to have a list if env variables to limit it.

}

if err := cmd.Run(); err != nil {
return fmt.Errorf("executing command for package %s failed: %w", path, err)
}

return nil
}

func validateSubCommand(subCommand string) error {
if !slices.Contains(getAllowedSubCommands(), subCommand) {
return fmt.Errorf("invalid subcommand: %s. Allowed subcommands are: %s", subCommand, strings.Join(getAllowedSubCommands(), ", "))
}

return nil
}
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ var commands = []*cobraext.Command{
setupDumpCommand(),
setupEditCommand(),
setupExportCommand(),
setupFilterCommand(),
setupFormatCommand(),
setupForeachCommand(),
setupInstallCommand(),
setupLinksCommand(),
setupLintCommand(),
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/elastic/package-spec/v3 v3.5.0
github.com/fatih/color v1.18.0
github.com/go-viper/mapstructure/v2 v2.4.0
github.com/gobwas/glob v0.2.3
github.com/google/go-cmp v0.7.0
github.com/google/go-github/v32 v32.1.0
github.com/google/go-querystring v1.1.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
Expand Down
47 changes: 45 additions & 2 deletions internal/cobraext/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,51 @@ const (
FailOnMissingFlagName = "fail-on-missing"
FailOnMissingFlagDescription = "fail if tests are missing"

FailFastFlagName = "fail-fast"
FailFastFlagDescription = "fail immediately if any file requires updates (do not overwrite)"
FailFastFlagName = "fail-fast"
FailFastFlagDescription = "fail immediately if any file requires updates (do not overwrite)"

FilterCategoriesFlagName = "categories"
FilterCategoriesFlagDescription = "integration categories to filter by (comma-separated values)"

FilterCodeOwnerFlagName = "code-owners"
FilterCodeOwnerFlagDescription = "code owners to filter by (comma-separated values)"

FilterPackageTypeFlagName = "package-types"
FilterPackageTypeFlagDescription = "package types to filter by (comma-separated values)"

FilterInputFlagName = "inputs"
FilterInputFlagDescription = "name of the inputs to filter by (comma-separated values)"

FilterKibanaVersionFlagName = "kibana-version"
FilterKibanaVersionFlagDescription = "kibana version to filter by (semver)"

FilterOutputAbsolutePathFlagName = "output-absolute-path"
FilterOutputAbsolutePathFlagDescription = "output the absolute path of the package"

FilterOutputPackageNameFlagName = "output-package-name"
FilterOutputPackageNameFlagDescription = "print the package name instead of the directory name in the output"

FilterPackageDirNameFlagName = "package-dirs"
FilterPackageDirNameFlagDescription = "package directories to filter by (comma-separated values)"

FilterPackagesFlagName = "packages"
FilterPackagesFlagDescription = "package names to filter by (comma-separated values)"

FilterSpecVersionFlagName = "spec-version"
FilterSpecVersionFlagDescription = "Package spec version to filter by (semver)"

FilterDepthFlagName = "depth"
FilterDepthFlagDescription = "maximum depth to search for packages (default: 2)"
FilterDepthFlagDefault = 2
FilterDepthFlagShorthand = "d"

FilterExcludeDirFlagName = "exclude-dirs"
FilterExcludeDirFlagDescription = "comma-separated list of directories to exclude from search"

ForeachPoolSizeFlagName = "parallel"
ForeachPoolSizeFlagShorthand = "p"
ForeachPoolSizeFlagDescription = "Number of subcommands to execute in parallel (defaults to serial execution)"

GenerateTestResultFlagName = "generate"
GenerateTestResultFlagDescription = "generate test result file"

Expand Down
Loading