diff --git a/.github/workflows/main-benchmark.yml b/.github/workflows/main-benchmark.yml new file mode 100644 index 0000000000..c9d8a4ff3d --- /dev/null +++ b/.github/workflows/main-benchmark.yml @@ -0,0 +1,64 @@ +name: Benchmark main + +## Only trigger tests if source is changing +on: + push: + branches: + - main + paths: + - '**.go' + - '**.mod' + - 'go.sum' + - .github/workflows/main-benchmark.yml + +permissions: + # deployments permission to deploy GitHub pages website + deployments: write + # contents permission to update benchmark contents in gh-pages branch + contents: write + +jobs: + go-bench: + runs-on: ubuntu-latest + env: + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Import environment variables from file + run: cat ".github/env" >> $GITHUB_ENV + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: ">=${{ env.golang-version }}" + cache: false + - name: Run benchmark + run: make benchmark/go | tee benchmark.txt + + # Remove log statements and leave just the benchmark results + - name: Cleanup benchmark file + run: sed -i -n '/goos:/,$p' benchmark.txt + + # Download previous benchmark result from cache (if exists) + - name: Download previous benchmark data + uses: actions/cache/restore@v4 + with: + path: ./cache + key: ${{ runner.os }}-benchmark + # Run `github-action-benchmark` action + - name: Store benchmark result + uses: benchmark-action/github-action-benchmark@v1 + with: + # What benchmark tool the output.txt came from + tool: 'go' + # Where the output from the benchmark tool is stored + output-file-path: benchmark.txt + # Where the previous data file is stored + external-data-json-path: ./cache/benchmark-data.json + save-data-file: true + + - name: Save benchmark data + uses: actions/cache/save@v4 + with: + path: ./cache + key: ${{ runner.os }}-benchmark \ No newline at end of file diff --git a/.github/workflows/pr-test-lint.yml b/.github/workflows/pr-test-lint.yml index a996c6b38c..a908c54622 100644 --- a/.github/workflows/pr-test-lint.yml +++ b/.github/workflows/pr-test-lint.yml @@ -90,6 +90,49 @@ jobs: with: name: test-results path: report.xml + + go-bench: + runs-on: ubuntu-latest + if: github.ref != 'refs/heads/main' + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Import environment variables from file + run: cat ".github/env" >> $GITHUB_ENV + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: ">=${{ env.golang-version }}" + cache: false + - name: Run benchmark + run: make benchmark/go | tee benchmark.txt + + # Remove log statements and leave just the benchmark results + - name: Cleanup benchmark file + run: sed -i -n '/goos:/,$p' benchmark.txt + + # Download previous benchmark result from cache (if exists) + - name: Download previous benchmark data + uses: actions/cache/restore@v4 + with: + path: ./cache + key: ${{ runner.os }}-benchmark + # Run `github-action-benchmark` action + - name: Store benchmark result + uses: benchmark-action/github-action-benchmark@v1 + with: + # What benchmark tool the output.txt came from + tool: 'go' + # Where the output from the benchmark tool is stored + output-file-path: benchmark.txt + # Where the previous data file is stored + external-data-json-path: ./cache/benchmark-data.json + github-token: ${{ secrets.GITHUB_TOKEN }} + comment-on-alert: true + summary-always: true + fail-on-alert: true + save-data-file: false + alert-threshold: '150%' event_file: name: "Store event file" diff --git a/Makefile b/Makefile index 93414024a2..919aed9c16 100644 --- a/Makefile +++ b/Makefile @@ -594,6 +594,9 @@ test/lint: test/lint/golangci-lint/run test: test/go test/lint +benchmark/go: + go test -bench=. -benchmem go.mondoo.com/cnquery/v10/explorer/scan/benchmark + test/go: cnquery/generate test/go/plain test/go/plain: diff --git a/explorer/scan/benchmark/benchmark_test.go b/explorer/scan/benchmark/benchmark_test.go new file mode 100644 index 0000000000..2932d17ea4 --- /dev/null +++ b/explorer/scan/benchmark/benchmark_test.go @@ -0,0 +1,119 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package benchmark + +import ( + "context" + "testing" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + + "github.com/stretchr/testify/require" + "go.mondoo.com/cnquery/v10" + "go.mondoo.com/cnquery/v10/explorer" + "go.mondoo.com/cnquery/v10/explorer/scan" + "go.mondoo.com/cnquery/v10/mqlc" + "go.mondoo.com/cnquery/v10/providers-sdk/v1/inventory" + "go.mondoo.com/cnquery/v10/providers-sdk/v1/testutils" +) + +func init() { + log.Logger = log.Logger.Level(zerolog.Disabled) + zerolog.SetGlobalLevel(zerolog.Disabled) +} + +func BenchmarkScan_SingleAsset(b *testing.B) { + ctx := context.Background() + runtime := testutils.Local() + conf := mqlc.NewConfig(runtime.Schema(), cnquery.DefaultFeatures) + job := &scan.Job{ + Inventory: &inventory.Inventory{ + Spec: &inventory.InventorySpec{ + Assets: []*inventory.Asset{ + { + Connections: []*inventory.Config{ + { + Type: "k8s", + Options: map[string]string{ + "path": "./testdata/1pod.yaml", + }, + Discover: &inventory.Discovery{ + Targets: []string{"pods"}, + }, + }, + }, + }, + }, + }, + }, + } + + bundle, err := explorer.BundleFromPaths("./testdata/mondoo-kubernetes-inventory.mql.yaml") + require.NoError(b, err) + + _, err = bundle.CompileExt(context.Background(), explorer.BundleCompileConf{ + CompilerConfig: conf, + RemoveFailing: true, + }) + require.NoError(b, err) + + job.Bundle = bundle + + scanner := scan.NewLocalScanner(scan.DisableProgressBar()) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + res, err := scanner.RunIncognito(ctx, job) + require.NoError(b, err) + require.NotNil(b, res) + } +} + +func BenchmarkScan_MultipleAssets(b *testing.B) { + ctx := context.Background() + runtime := testutils.Local() + conf := mqlc.NewConfig(runtime.Schema(), cnquery.DefaultFeatures) + job := &scan.Job{ + Inventory: &inventory.Inventory{ + Spec: &inventory.InventorySpec{ + Assets: []*inventory.Asset{ + { + Connections: []*inventory.Config{ + { + Type: "k8s", + Options: map[string]string{ + "path": "./testdata/2pods.yaml", + }, + Discover: &inventory.Discovery{ + Targets: []string{"pods"}, + }, + }, + }, + }, + }, + }, + }, + } + + bundle, err := explorer.BundleFromPaths("./testdata/mondoo-kubernetes-inventory.mql.yaml") + require.NoError(b, err) + + _, err = bundle.CompileExt(context.Background(), explorer.BundleCompileConf{ + CompilerConfig: conf, + RemoveFailing: true, + }) + require.NoError(b, err) + + job.Bundle = bundle + + scanner := scan.NewLocalScanner(scan.DisableProgressBar()) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + res, err := scanner.RunIncognito(ctx, job) + require.NoError(b, err) + require.NotNil(b, res) + } +} diff --git a/explorer/scan/benchmark/testdata/1pod.yaml b/explorer/scan/benchmark/testdata/1pod.yaml new file mode 100644 index 0000000000..32fa1a69c7 --- /dev/null +++ b/explorer/scan/benchmark/testdata/1pod.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + admission-result: pass + name: passing-pod-yaml + namespace: default +spec: + automountServiceAccountToken: false + containers: + - image: ubuntu:20.04 + imagePullPolicy: Always + command: ["/bin/sh", "-c"] + args: ["sleep 6000"] + name: ubuntu \ No newline at end of file diff --git a/explorer/scan/benchmark/testdata/2pods.yaml b/explorer/scan/benchmark/testdata/2pods.yaml new file mode 100644 index 0000000000..e17030ddb8 --- /dev/null +++ b/explorer/scan/benchmark/testdata/2pods.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + admission-result: pass + name: passing-pod-yaml + namespace: default +spec: + automountServiceAccountToken: false + containers: + - image: ubuntu:20.04 + imagePullPolicy: Always + command: ["/bin/sh", "-c"] + args: ["sleep 6000"] + name: ubuntu +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + admission-result: pass + name: passing-pod-yaml-2 + namespace: default +spec: + automountServiceAccountToken: false + containers: + - image: ubuntu:20.04 + imagePullPolicy: Always + command: ["/bin/sh", "-c"] + args: ["sleep 6000"] + name: ubuntu diff --git a/explorer/scan/benchmark/testdata/mondoo-kubernetes-inventory.mql.yaml b/explorer/scan/benchmark/testdata/mondoo-kubernetes-inventory.mql.yaml new file mode 100644 index 0000000000..320e4e17fe --- /dev/null +++ b/explorer/scan/benchmark/testdata/mondoo-kubernetes-inventory.mql.yaml @@ -0,0 +1,153 @@ +# Copyright (c) Mondoo, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +packs: + - uid: mondoo-kubernetes-inventory + name: Kubernetes Inventory Pack + version: 1.1.0 + license: BUSL-1.1 + authors: + - name: Mondoo, Inc + email: hello@mondoo.com + tags: + mondoo.com/platform: kubernetes,k8s # remove k8s when v9 is released + mondoo.com/category: best-practices + docs: + desc: | + The Kubernetes Inventory Pack by Mondoo pack retrieves data about a Kubernetes Cluster for asset inventory. + + To run this pack for a Kubernetes Cluster: + + ```bash + cnquery scan k8s -f mondoo-kubernetes-inventory.mql.yaml + ``` + + ## Join the community! + Our goal is to build query packs that are simple to deploy and provide accurate and useful data. + + If you have any suggestions for improving this query pack, or if you need support, [join the Mondoo community](https://github.com/orgs/mondoohq/discussions) in GitHub Discussions. + groups: + - title: Cluster inventory + filters: + - asset.platform == "kubernetes" || asset.platform == "k8s-cluster" + queries: + - uid: k8s-cluster-version + title: Kubernetes cluster version + mql: | + k8s.serverVersion + - uid: k8s-cluster-namespaces + title: Kubernetes cluster namespaces + mql: | + k8s.namespaces + - uid: k8s-cluster-nodes + title: Cluster modes + mql: | + k8s.nodes + - uid: k8s-cluster-clusterroles + title: Cluster RBAC ClusterRoles + mql: | + k8s.clusterroles + - uid: k8s-cluster-roles + title: RBAC Roles + mql: | + k8s.roles + - uid: k8s-cluster-clusterrolebindings + title: RBAC cluster-rolebindings + mql: | + k8s.clusterrolebindings + - uid: k8s-cluster-rolebindings + title: RBAC rolebindings + mql: | + k8s.rolebindings + - title: Pods inventory + filters: + - asset.platform == "k8s-pod" + queries: + - uid: k8s-pod + title: Pod information + mql: | + k8s.pod + - uid: k8s-pod-container + title: Container information + mql: | + k8s.pod.containers + - title: Deployments inventory + filters: + - asset.platform == "k8s-deployment" + queries: + - uid: k8s-deployment + title: Deployment information + mql: | + k8s.deployments + - uid: k8s-deployment-container + title: Container information + mql: | + k8s.deployment.containers { * } + - title: CronJobs inventory + filters: + - asset.platform == "k8s-cronjob" + queries: + - uid: k8s-cronjob + title: CronJob information + mql: | + k8s.cronjob { * } + - uid: k8s-cronjob-container + title: Container information + mql: | + k8s.cronjob.containers { * } + - title: Jobs inventory + filters: + - asset.platform == "k8s-job" + queries: + - uid: k8s-job + title: Job information + mql: | + k8s.job { * } + - uid: k8s-job-container + title: Container information + mql: | + k8s.job.containers { * } + - title: DaemonSets inventory + filters: + - asset.platform == "k8s-daemonset" + queries: + - uid: k8s-daemonset + title: DaemonSet information + mql: | + k8s.daemonset { * } + - uid: k8s-daemonset-container + title: Container information + mql: | + k8s.daemonset.containers { * } + - title: StatefulSets inventory + filters: + - asset.platform == "k8s-statefulset" + queries: + - uid: k8s-statefulset + title: StatefulSet information + mql: | + k8s.statefulset { * } + - uid: k8s-statefulset-container + title: Container information + mql: | + k8s.statefulset.containers { * } + - title: ReplicaSets inventory + filters: + - asset.platform == "k8s-replicaset" + queries: + - uid: k8s-replicaset + title: ReplicaSet information + mql: | + k8s.replicaset { * } + - uid: k8s-replicaset-container + title: Container information + mql: | + k8s.replicaset.containers { * } + - title: Ingresses inventory + filters: + - asset.platform == "k8s-ingress" + queries: + - uid: k8s-ingress + title: Ingress information + mql: | + k8s.ingress { * } \ No newline at end of file diff --git a/explorer/scan/local_scanner.go b/explorer/scan/local_scanner.go index d46d97f5d2..14f11c7cd3 100644 --- a/explorer/scan/local_scanner.go +++ b/explorer/scan/local_scanner.go @@ -44,10 +44,10 @@ type assetWithRuntime struct { } type LocalScanner struct { - ctx context.Context - fetcher *fetcher - upstream *upstream.UpstreamConfig - recording llx.Recording + fetcher *fetcher + upstream *upstream.UpstreamConfig + recording llx.Recording + disableProgressBar bool } type ScannerOption func(*LocalScanner) @@ -64,6 +64,12 @@ func WithRecording(r llx.Recording) func(s *LocalScanner) { } } +func DisableProgressBar() ScannerOption { + return func(s *LocalScanner) { + s.disableProgressBar = true + } +} + func NewLocalScanner(opts ...ScannerOption) *LocalScanner { ls := &LocalScanner{ fetcher: newFetcher(), @@ -382,7 +388,7 @@ func (s *LocalScanner) distributeJob(job *Job, ctx context.Context, upstream *up orderedKeys = append(orderedKeys, assets[i].asset.PlatformIds[0]) } var multiprogress progress.MultiProgress - if isatty.IsTerminal(os.Stdout.Fd()) && !strings.EqualFold(logger.GetLevel(), "debug") && !strings.EqualFold(logger.GetLevel(), "trace") { + if isatty.IsTerminal(os.Stdout.Fd()) && !s.disableProgressBar && !strings.EqualFold(logger.GetLevel(), "debug") && !strings.EqualFold(logger.GetLevel(), "trace") { var err error multiprogress, err = progress.NewMultiProgressBars(progressBarElements, orderedKeys) if err != nil {