Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow predefined queries to be specified in aspect configuration #122

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 22 additions & 2 deletions cmd/aspect/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,15 @@ func NewRootCmd(
cmd.PersistentFlags().StringVar(&cfgFile, flags.ConfigFlagName, "", "config file (default is $HOME/.aspect.yaml)")
cmd.PersistentFlags().BoolVar(&interactive, flags.InteractiveFlagName, defaultInteractive, "Interactive mode (e.g. prompts for user input)")

// ### Viper
// If user specifies the config file to use then we want to only use that config.
// If user does not specify a config file to use then we want to load ".aspect" from the
// $HOME directory and from the root of the repo (if it exists).
// Adding a second config path using "AddConfigPath" does not work because we dont
// change the config name using "AddConfigPath". This results in loading the same config
// file twice. A workaround for this is to have a second viper instance load the repo
// config and merge them together. Source: https://github.com/spf13/viper/issues/181
repoViper := viper.New()

if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
Expand All @@ -71,15 +79,27 @@ func NewRootCmd(
home, err := homedir.Dir()
cobra.CheckErr(err)

// Search config in home directory with name ".aspect" (without extension).
// Search for config in home directory with name ".aspect" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".aspect")

// Search for config in root of current repo with name ".aspect" (without extension).
repoViper.AddConfigPath(".")
repoViper.SetConfigName(".aspect")
repoViper.AutomaticEnv()
}

viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {
faint.Fprintln(streams.Stderr, "Using config file:", viper.ConfigFileUsed())
}

if err := repoViper.ReadInConfig(); err == nil {
faint.Fprintln(streams.Stderr, "Using config file:", repoViper.ConfigFileUsed())
}

viper.MergeConfigMap(repoViper.AllSettings())

// ### Child commands
// IMPORTANT: when adding a new command, also update the _DOCS list in /docs/BUILD.bazel
cmd.AddCommand(build.NewDefaultBuildCmd(pluginSystem))
Expand Down
20 changes: 20 additions & 0 deletions examples/predefined-queries/.aspect.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
query:
presets:
afoo:
description: List deps
query: deps(?target)
verb: aquery
cfoo:
description: List deps
query: deps(?target)
verb: cquery
foo:
description: List deps
query: deps(?target)
verb: query
why:
description:
Override the default why verb. Determine why targetA depends on
targetB
query: somepath(?targetA, ?targetB)
verb: query
2 changes: 2 additions & 0 deletions examples/predefined-queries/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.aspect/
bazel-*
6 changes: 6 additions & 0 deletions examples/predefined-queries/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# predefined-queries

This example exercises the ability to add predefined queries to a
checked in aspect config file. Run a query using `aspect query`
in this workspace and the queries from the config file should be
used as predefined queries.
1 change: 1 addition & 0 deletions examples/predefined-queries/WORKSPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
workspace(name = "predefined_queries")
5 changes: 5 additions & 0 deletions examples/predefined-queries/foo/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
genrule(
name = "foo",
outs = ["foo.txt"],
cmd = "echo 'Hello World' > '$@'",
)
11 changes: 11 additions & 0 deletions integration_tests/query/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
sh_test(
name = "long_preset_query_test",
srcs = ["long_preset_query_test.sh"],
data = ["//cmd/aspect"],
)

sh_test(
name = "passthrough_query_test",
srcs = ["passthrough_query_test.sh"],
data = ["//cmd/aspect"],
)
36 changes: 36 additions & 0 deletions integration_tests/query/long_preset_query_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash

set -o pipefail -o errexit -o nounset
HOME="$TEST_TMPDIR"
touch "$HOME"/.aspect.yaml
ASPECT="$TEST_SRCDIR/build_aspect_cli/cmd/aspect/aspect_/aspect"
export HOME
touch WORKSPACE

mkdir foo
cat > foo/BUILD <<'EOF'
genrule(
name = "foo",
outs = ["foo.txt"],
cmd = "touch $@",
)
EOF

cat > .aspect.yaml <<'EOF'
query:
presets:
foo:
description: "List deps"
query: "deps(?target)"
verb: "query"
EOF

# Only capture stdout, just like `bazel version` prints to stdout
query=$($ASPECT query foo //foo 2>/dev/null) || "$ASPECT" query foo //foo

# Should list the //foo:foo genrule that we have created
[[ "$query" =~ "//foo:foo" ]] || {
echo >&2 "Expected 'aspect query foo //foo' stdout to contain '//foo:foo', but was"
echo "$query"
exit 1
}
28 changes: 28 additions & 0 deletions integration_tests/query/passthrough_query_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash

set -o pipefail -o errexit -o nounset
HOME="$TEST_TMPDIR"
touch "$HOME"/.aspect.yaml
ASPECT="$TEST_SRCDIR/build_aspect_cli/cmd/aspect/aspect_/aspect"
export HOME
touch WORKSPACE

mkdir foo

cat > foo/BUILD <<'EOF'
genrule(
name = "foo",
outs = ["foo.txt"],
cmd = "touch $@",
)
EOF

# Only capture stdout, just like `bazel version` prints to stdout
query=$($ASPECT query 'deps(//foo)' 2>/dev/null) || "$ASPECT" query 'deps(//foo)'

# Should list the //foo:foo genrule that we have created
[[ "$query" =~ "//foo:foo" ]] || {
echo >&2 "Expected 'aspect query deps(//foo)' stdout to contain '//foo:foo', but was"
echo "$query"
exit 1
}
1 change: 1 addition & 0 deletions pkg/aspect/aquery/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ go_library(
"//pkg/bazel",
"//pkg/ioutils",
"@com_github_spf13_cobra//:cobra",
"@com_github_spf13_viper//:viper",
],
)

Expand Down
7 changes: 6 additions & 1 deletion pkg/aspect/aquery/aquery.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package aquery

import (
"github.com/spf13/cobra"
"github.com/spf13/viper"

"aspect.build/cli/pkg/aspect/query/shared"
"aspect.build/cli/pkg/bazel"
Expand All @@ -21,14 +22,17 @@ type AQuery struct {
IsInteractive bool

Presets []*shared.PresetQuery
Prefs viper.Viper

Prompt func(label string) shared.PromptRunner
Confirmation func(question string) shared.ConfirmationRunner
Select func(presetNames []string) shared.SelectRunner
}

func New(streams ioutils.Streams, bzl bazel.Bazel, isInteractive bool) *AQuery {
presets := shared.PrecannedQueries("aquery")
v := *viper.GetViper()

presets := shared.PrecannedQueries("aquery", v)

return &AQuery{
Streams: streams,
Expand All @@ -38,6 +42,7 @@ func New(streams ioutils.Streams, bzl bazel.Bazel, isInteractive bool) *AQuery {
Prompt: shared.Prompt,
Select: shared.Select,
Confirmation: shared.Confirmation,
Prefs: v,
}
}

Expand Down
1 change: 1 addition & 0 deletions pkg/aspect/cquery/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ go_library(
"//pkg/bazel",
"//pkg/ioutils",
"@com_github_spf13_cobra//:cobra",
"@com_github_spf13_viper//:viper",
],
)

Expand Down
7 changes: 6 additions & 1 deletion pkg/aspect/cquery/cquery.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package cquery

import (
"github.com/spf13/cobra"
"github.com/spf13/viper"

"aspect.build/cli/pkg/aspect/query/shared"
"aspect.build/cli/pkg/bazel"
Expand All @@ -21,14 +22,17 @@ type CQuery struct {
IsInteractive bool

Presets []*shared.PresetQuery
Prefs viper.Viper

Prompt func(label string) shared.PromptRunner
Confirmation func(question string) shared.ConfirmationRunner
Select func(presetNames []string) shared.SelectRunner
}

func New(streams ioutils.Streams, bzl bazel.Bazel, isInteractive bool) *CQuery {
presets := shared.PrecannedQueries("cquery")
v := *viper.GetViper()

presets := shared.PrecannedQueries("cquery", v)

return &CQuery{
Streams: streams,
Expand All @@ -38,6 +42,7 @@ func New(streams ioutils.Streams, bzl bazel.Bazel, isInteractive bool) *CQuery {
Prompt: shared.Prompt,
Select: shared.Select,
Confirmation: shared.Confirmation,
Prefs: v,
}
}

Expand Down
8 changes: 5 additions & 3 deletions pkg/aspect/query/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ type Query struct {
}

func New(streams ioutils.Streams, bzl bazel.Bazel, isInteractive bool) *Query {
v := *viper.GetViper()

// the list of available preset queries will potentially be updated during the "Run" function.
// if the user requests that query also show aquery and cquery predefined queries then these
// will be added to the list of presets
presets := shared.PrecannedQueries("query")
presets := shared.PrecannedQueries("query", v)

return &Query{
Streams: streams,
Expand All @@ -52,7 +54,7 @@ func New(streams ioutils.Streams, bzl bazel.Bazel, isInteractive bool) *Query {
Prompt: shared.Prompt,
Select: shared.Select,
Confirmation: shared.Confirmation,
Prefs: *viper.GetViper(),
Prefs: v,
}
}

Expand Down Expand Up @@ -82,7 +84,7 @@ func (q *Query) Run(cmd *cobra.Command, args []string) error {
}

if q.Prefs.GetBool(allowAllQueries) {
q.Presets = shared.PrecannedQueries("")
q.Presets = shared.PrecannedQueries("", q.Prefs)
}

presets, presetNames, err := shared.ProcessQueries(q.Presets)
Expand Down
27 changes: 27 additions & 0 deletions pkg/aspect/query/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,4 +299,31 @@ func TestQuery(t *testing.T) {
err = q.Run(cmd, []string{})
g.Expect(err).To(BeNil())
})

t.Run("user defined queries can overwrite default predefined queries", func(t *testing.T) {
g := NewGomegaWithT(t)

viper := *viper.New()
cfg, err := os.CreateTemp(os.Getenv("TEST_TMPDIR"), "cfg***.ini")

g.Expect(err).To(BeNil())

viper.SetConfigFile(cfg.Name())
viper.Set("query.presets.why.description", "Override the default why verb. Determine why targetA depends on targetB")
viper.Set("query.presets.why.query", "somepath(?targetA, ?targetB)")
viper.Set("query.presets.why.verb", "query")

result := shared.PrecannedQueries("query", viper)
g.Expect(len(result)).To(Equal(2))

g.Expect(result[0].Description).To(Equal("Get the deps of a target"))
g.Expect(result[0].Query).To(Equal("deps(?target)"))
g.Expect(result[0].Verb).To(Equal("query"))
g.Expect(result[0].Name).To(Equal("deps"))

g.Expect(result[1].Description).To(Equal("Override the default why verb. Determine why targetA depends on targetB"))
g.Expect(result[1].Query).To(Equal("somepath(?targetA, ?targetB)"))
g.Expect(result[1].Verb).To(Equal("query"))
g.Expect(result[1].Name).To(Equal("why"))
})
}
1 change: 1 addition & 0 deletions pkg/aspect/query/shared/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ go_library(
"//pkg/ioutils",
"@com_github_manifoldco_promptui//:promptui",
"@com_github_spf13_cobra//:cobra",
"@com_github_spf13_viper//:viper",
],
)
42 changes: 39 additions & 3 deletions pkg/aspect/query/shared/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
"github.com/spf13/viper"

"aspect.build/cli/pkg/aspecterrors"
"aspect.build/cli/pkg/bazel"
Expand Down Expand Up @@ -64,9 +65,7 @@ func Select(presetNames []string) SelectRunner {
}
}

func PrecannedQueries(verb string) []*PresetQuery {
// TODO: Queries should be loadable from the plugin config
// https://github.com/aspect-build/aspect-cli/issues/98
func PrecannedQueries(verb string, viper viper.Viper) []*PresetQuery {
presets := []*PresetQuery{
{
Name: "why",
Expand Down Expand Up @@ -94,6 +93,28 @@ func PrecannedQueries(verb string) []*PresetQuery {
},
}

presetsKey := "query.presets"

userDefinedQueries := viper.GetStringMap(presetsKey)

for name := range userDefinedQueries {
userDefinedQuery := viper.GetStringMapString(fmt.Sprintf("%s.%s", presetsKey, name))

presetQuery := &PresetQuery{
Name: name,
Description: userDefinedQuery["description"],
Query: userDefinedQuery["query"],
Verb: userDefinedQuery["verb"],
}

presetExists, existingPresetIndex := isPresetQueryInSlice(presetQuery, presets)
if presetExists {
presets = removePresetQuery(presets, existingPresetIndex)
}

presets = append(presets, presetQuery)
}

switch verb {
case "query":
return filterPrecannedQueries("query", presets)
Expand Down Expand Up @@ -221,3 +242,18 @@ func SelectQuery(
func GetPrettyError(cmd *cobra.Command, err error) error {
return fmt.Errorf("failed to run 'aspect %s': %w", cmd.Use, err)
}

// if preset query is present return true and the index of where it is found
// if preset query is not preset return false and -1 for index
func isPresetQueryInSlice(presetQuery *PresetQuery, presetQueries []*PresetQuery) (bool, int) {
for i, existingPresetQuery := range presetQueries {
if existingPresetQuery.Name == presetQuery.Name && existingPresetQuery.Verb == presetQuery.Verb {
return true, i
}
}
return false, -1
}

func removePresetQuery(presetQueries []*PresetQuery, i int) []*PresetQuery {
return append(presetQueries[:i], presetQueries[i+1:]...)
}