Skip to content
Open
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
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,14 @@ install-tools:
go install $(GO_MOD_FLAGS) github.com/golang/mock/mockgen
go install $(GO_MOD_FLAGS) golang.org/x/lint/golint
go install $(GO_MOD_FLAGS) github.com/golangci/golangci-lint/cmd/golangci-lint
go install $(GO_MOD_FLAGS) sigs.k8s.io/controller-runtime/tools/setup-envtest
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

benchstat?


.PHONY: coverage
coverage:
hack/codecov.sh

.PHONY: setup-envtest
# Downloads envtest binaries and prints the required export KUBEBUILDER_ASSETS=... command.
# Run the printed export command (or add it to your shell rc) before running benchmarks.
setup-envtest:
hack/setup-envtest.sh
Comment on lines +393 to +397
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add missing .PHONY declarations for clean and test.

At Line [393], static analysis reports missing required phony targets, which can cause avoidable checkmake warnings.

🔧 Suggested fix
-.PHONY: all
+.PHONY: all clean test
🧰 Tools
🪛 checkmake (0.2.2)

[warning] 393-393: Missing required phony target "clean"

(minphony)


[warning] 393-393: Missing required phony target "test"

(minphony)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 393 - 397, Add the missing .PHONY declarations for the
Makefile targets "clean" and "test" to avoid checkmake warnings; similar to the
existing .PHONY: setup-envtest line, update the .PHONY list (or add a new .PHONY
line) to include clean and test so the Makefile treats those targets as phony
rather than files.

1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ require (
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
sigs.k8s.io/cluster-api-provider-azure v1.21.1-0.20250929163617-2c4eaa611a39
sigs.k8s.io/controller-runtime v0.22.3
sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20240927101401-4381fa0aeee4
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Odd that this didn't produce a go.sum delta?

sigs.k8s.io/controller-tools v0.19.0
sigs.k8s.io/yaml v1.6.0
)
Expand Down
127 changes: 127 additions & 0 deletions hack/benchmark-comparison.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/bin/bash
# Compare benchmark performance before and after working changes.
#
# Uses git worktrees for safe comparison without touching your working tree.
# Compares the current working tree against a base commit (default: HEAD~1).
#
# Usage:
# ./hack/benchmark-comparison.sh [base-commit]
#
# Examples:
# # Compare current working tree vs parent commit
# ./hack/benchmark-comparison.sh
#
# # Compare current working tree vs main branch
# ./hack/benchmark-comparison.sh main
#
# # Compare with custom benchmark settings
# BENCHTIME=10x COUNT=8 ./hack/benchmark-comparison.sh
#
# # Benchmark specific packages
# BENCH_PKGS="./pkg/resource/" ./hack/benchmark-comparison.sh

set -eo pipefail

# Configurable benchmark parameters
BENCHTIME=${BENCHTIME:-5x}
COUNT=${COUNT:-6}
BENCH_PATTERN=${BENCH_PATTERN:-'^Benchmark'}
BENCH_PKGS=${BENCH_PKGS:-./test/benchmark/}
BASE_COMMIT=${1:-HEAD~1}

# Ensure we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo "Error: Not in a git repository"
exit 1
fi

# Ensure KUBEBUILDER_ASSETS is set
if [ -z "$KUBEBUILDER_ASSETS" ]; then
echo "Error: KUBEBUILDER_ASSETS environment variable not set"
echo ""
echo "Run: make setup-envtest"
echo "Then: export KUBEBUILDER_ASSETS=\"\$(hack/setup-envtest.sh | tail -1 | cut -d'=' -f2 | tr -d '\"')\""
exit 1
fi
Comment on lines +38 to +45
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What would be really cool is if the benchmark could optionally be run against an actual cluster. I've been trying to track down where/how KUBEBUILDER_ASSETS is actually consumed to spoof the cluster via envtest, but have so far been unsuccessful.


# Verify base commit exists
if ! git rev-parse "$BASE_COMMIT" > /dev/null 2>&1; then
echo "Error: Base commit '$BASE_COMMIT' not found"
exit 1
fi

TEMP_DIR=$(mktemp -d)
WORKTREE_DIR=$(mktemp -d -t hive-benchmark-worktree.XXXXXX)

cleanup() {
if [ -d "$WORKTREE_DIR" ]; then
echo ""
echo "Cleaning up worktree..."
git worktree remove "$WORKTREE_DIR" --force 2>/dev/null || true
rm -rf "$WORKTREE_DIR" 2>/dev/null || true
fi
}
trap cleanup EXIT

echo "==========================================================="
echo "Benchmark Comparison"
echo "==========================================================="
echo "Base commit: $BASE_COMMIT ($(git rev-parse --short "$BASE_COMMIT"))"
echo "Working tree: $(git rev-parse --short HEAD)$(git diff --quiet && git diff --cached --quiet || echo ' (dirty)')"
echo "Benchmark time: ${BENCHTIME}"
echo "Run count: ${COUNT}"
echo "Pattern: ${BENCH_PATTERN}"
echo "Packages: ${BENCH_PKGS}"
echo "Results dir: ${TEMP_DIR}"
echo "==========================================================="
echo ""

# Run benchmarks in the current working tree
echo "-> Running benchmarks in current working tree..."
go test -bench="${BENCH_PATTERN}" -benchmem -benchtime="${BENCHTIME}" -count="${COUNT}" ${BENCH_PKGS} 2>&1 | tee "${TEMP_DIR}/new.txt"

# Create worktree at base commit
echo ""
echo "-> Creating worktree at ${BASE_COMMIT}..."
git worktree add --detach "$WORKTREE_DIR" "$BASE_COMMIT" --quiet

# Run benchmarks at base commit (in worktree)
echo "-> Running benchmarks at base commit..."
(cd "$WORKTREE_DIR" && \
export KUBEBUILDER_ASSETS="$KUBEBUILDER_ASSETS" && \
go test -bench="${BENCH_PATTERN}" -benchmem -benchtime="${BENCHTIME}" -count="${COUNT}" ${BENCH_PKGS}) 2>&1 | tee "${TEMP_DIR}/old.txt"

# Cleanup worktree early
echo ""
git worktree remove "$WORKTREE_DIR" --force
rm -rf "$WORKTREE_DIR"

# Compare results
echo ""
echo "==========================================================="
echo "BENCHMARK COMPARISON"
echo "==========================================================="
echo ""

if ! command -v benchstat &> /dev/null; then
echo "Warning: benchstat not installed. Install with:"
echo " go install golang.org/x/perf/cmd/benchstat@latest"
echo ""
echo "Raw results saved to:"
echo " Old: ${TEMP_DIR}/old.txt"
echo " New: ${TEMP_DIR}/new.txt"
else
benchstat "${TEMP_DIR}/old.txt" "${TEMP_DIR}/new.txt"
fi

echo ""
echo "==========================================================="
echo "Results saved to: ${TEMP_DIR}"
echo " Base ($BASE_COMMIT): ${TEMP_DIR}/old.txt"
echo " Working tree: ${TEMP_DIR}/new.txt"
echo ""
if command -v benchstat &> /dev/null; then
echo "To view again:"
echo " benchstat ${TEMP_DIR}/old.txt ${TEMP_DIR}/new.txt"
fi
echo "==========================================================="
21 changes: 21 additions & 0 deletions hack/setup-envtest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash

set -o errexit
set -o nounset
set -o pipefail

if ! command -v setup-envtest &> /dev/null; then
echo "run 'make install-tools' to install setup-envtest"
exit 1
fi

K8S_VERSION=${K8S_VERSION:-$(go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $3}')}

echo "using k8s version ${K8S_VERSION}"

echo "Fetching binaries (on first run this may take some time)"
ENVTEST_PATH=$(setup-envtest use "$K8S_VERSION" -p path 2>&1)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and read the target file
find . -name "setup-envtest.sh" -type f

Repository: openshift/hive

Length of output: 81


🏁 Script executed:

# Read the file to see context around line 17
cat -n hack/setup-envtest.sh | head -40

Repository: openshift/hive

Length of output: 782


🏁 Script executed:

# Check how ENVTEST_PATH is used in the file
grep -n "ENVTEST_PATH" hack/setup-envtest.sh

Repository: openshift/hive

Length of output: 178


🏁 Script executed:

# Check for KUBEBUILDER_ASSETS references
grep -n "KUBEBUILDER_ASSETS" hack/setup-envtest.sh

Repository: openshift/hive

Length of output: 113


Remove 2>&1 from ENVTEST_PATH capture to avoid stderr pollution.

At Line 17, 2>&1 captures both stderr and stdout into the variable. Any progress or error output from setup-envtest will be mixed into ENVTEST_PATH, yielding an invalid path when exported as KUBEBUILDER_ASSETS on Line 21.

💡 Proposed fix
-ENVTEST_PATH=$(setup-envtest use "$K8S_VERSION" -p path 2>&1)
+ENVTEST_PATH="$(setup-envtest use "$K8S_VERSION" -p path)"
+if [[ ! -d "$ENVTEST_PATH" ]]; then
+    echo "setup-envtest returned a non-directory path: $ENVTEST_PATH" >&2
+    exit 1
+fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ENVTEST_PATH=$(setup-envtest use "$K8S_VERSION" -p path 2>&1)
ENVTEST_PATH="$(setup-envtest use "$K8S_VERSION" -p path)"
if [[ ! -d "$ENVTEST_PATH" ]]; then
echo "setup-envtest returned a non-directory path: $ENVTEST_PATH" >&2
exit 1
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/setup-envtest.sh` at line 17, The ENVTEST_PATH assignment is capturing
stderr into the variable (ENVTEST_PATH=$(setup-envtest use "$K8S_VERSION" -p
path 2>&1)), which pollutes the path used later for KUBEBUILDER_ASSETS; remove
the redirection so only stdout is captured (ENVTEST_PATH=$(setup-envtest use
"$K8S_VERSION" -p path)). Locate the ENVTEST_PATH assignment and the subsequent
export of KUBEBUILDER_ASSETS and ensure no stderr is merged into ENVTEST_PATH;
if you need error output, redirect errors separately or check the command exit
status and log errors to stderr rather than into ENVTEST_PATH.


echo
echo "Run this now or add it to your shell rc:"
echo " export KUBEBUILDER_ASSETS=\"$ENVTEST_PATH\""
3 changes: 3 additions & 0 deletions pkg/dependencymagnet/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ import (
// TODO: remove this patch with kube bump to 1.34, which will carry the fix (https://github.com/kubernetes/kubernetes/pull/132378)
// Work around for https://github.com/kubernetes/kubernetes/issues/132377
_ "k8s.io/code-generator/cmd/validation-gen"

// Used for envtest
_ "sigs.k8s.io/controller-runtime/tools/setup-envtest"
)
14 changes: 14 additions & 0 deletions pkg/remoteclient/benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package remoteclient_test

import (
"os"
"testing"

"github.com/openshift/hive/test/benchutil"
)

func TestMain(m *testing.M) {
code := m.Run()
benchutil.StopEnvTest()
os.Exit(code)
}
17 changes: 15 additions & 2 deletions pkg/remoteclient/kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,24 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
)

func NewBuilderFromKubeconfig(c client.Client, secret *corev1.Secret, controllerName hivev1.ControllerName) Builder {
func NewBuilderFromKubeconfig(c client.Client, secret *corev1.Secret, controllerName hivev1.ControllerName, opts ...BuilderOption) Builder {
var bo builderOptions
for _, o := range opts {
o(&bo)
}
return &kubeconfigBuilder{
c: c,
secret: secret,
fieldManager: "hive3-" + string(controllerName),
opts: bo,
}
}

type kubeconfigBuilder struct {
c client.Client
secret *corev1.Secret
fieldManager string
opts builderOptions
}

// Build is also responsible for verifying reachability of client
Expand Down Expand Up @@ -89,5 +95,12 @@ func (b *kubeconfigBuilder) UseSecondaryAPIURL() Builder {
}

func (b *kubeconfigBuilder) RESTConfig() (*rest.Config, error) {
return utils.RestConfigFromSecret(b.secret, false)
cfg, err := utils.RestConfigFromSecret(b.secret, false)
if err != nil {
return nil, err
}
if b.opts.transportWrapper != nil {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This seems like it should be DRYable if, instead of func(*builderOptions), our BuilderOptions (possibly renamed to RESTConfigOptions) was func(*whatever_type_cfg_is). Unless that type is different in the different calls?

cfg.Wrap(b.opts.transportWrapper)
}
return cfg, nil
}
29 changes: 28 additions & 1 deletion pkg/remoteclient/remoteclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ type Builder interface {
// The controllerName is needed for metrics.
// If the ClusterDeployment carries the fake cluster annotation, a fake client will be returned populated with
// runtime.Objects we need to query for in all our controllers.
func NewBuilder(c client.Client, cd *hivev1.ClusterDeployment, controllerName hivev1.ControllerName) Builder {
func NewBuilder(c client.Client, cd *hivev1.ClusterDeployment, controllerName hivev1.ControllerName, opts ...BuilderOption) Builder {
var bo builderOptions
for _, o := range opts {
o(&bo)
}
if utils.IsFakeCluster(cd) {
clusterVersion := ""
if cd.Status.InstallVersion != nil {
Expand All @@ -70,6 +74,7 @@ func NewBuilder(c client.Client, cd *hivev1.ClusterDeployment, controllerName hi
cd: cd,
controllerName: controllerName,
urlToUse: activeURL,
opts: bo,
}
}

Expand Down Expand Up @@ -185,11 +190,29 @@ func SetUnreachableCondition(cd *hivev1.ClusterDeployment, connectionError error
return
}

// BuilderOption configures optional behavior on a Builder.
type BuilderOption func(*builderOptions)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

NTR: This new BuilderOption (and associated machinery) is the only new code in production. I tried to make this as non-invasive as possible: all the callsites that construct new Builders are unchanged and this code no-ops in prod


type builderOptions struct {
transportWrapper func(http.RoundTripper) http.RoundTripper
}

// WithTransportWrapper adds a transport wrapper that will be applied to the
// REST config returned by RESTConfig(). The wrapper is applied after the
// controller metrics transport, so it sits on the outermost layer and
// observes all HTTP round trips.
func WithTransportWrapper(wrapper func(http.RoundTripper) http.RoundTripper) BuilderOption {
return func(o *builderOptions) {
o.transportWrapper = wrapper
}
}

type builder struct {
c client.Client
cd *hivev1.ClusterDeployment
controllerName hivev1.ControllerName
urlToUse int
opts builderOptions
}

const (
Expand Down Expand Up @@ -267,6 +290,10 @@ func (b *builder) RESTConfig() (*rest.Config, error) {

utils.AddControllerMetricsTransportWrapper(cfg, b.controllerName, true)

if b.opts.transportWrapper != nil {
cfg.Wrap(b.opts.transportWrapper)
}

if override := b.cd.Spec.ControlPlaneConfig.APIURLOverride; override != "" {
if b.urlToUse == primaryURL ||
(b.urlToUse == activeURL && IsPrimaryURLActive(b.cd)) {
Expand Down
53 changes: 53 additions & 0 deletions pkg/remoteclient/remoteclient_benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package remoteclient_test

import (
"testing"

"github.com/openshift/hive/test/benchutil"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func benchBuilderOp(b *testing.B, fn func(*testing.B, *benchutil.BenchRemoteClient)) {
benchutil.ControllerHarness[*benchutil.BenchRemoteClient]{
Setup: benchutil.SetupRemoteClient,
Reconcile: func(b *testing.B, brc *benchutil.BenchRemoteClient, _ []client.Object, _ int) {
fn(b, brc)
},
}.Run(b)
}

// BenchmarkBuilderBuild measures Build: secret read + parse + discovery + client creation.
func BenchmarkBuilderBuild(b *testing.B) {
benchBuilderOp(b, func(b *testing.B, brc *benchutil.BenchRemoteClient) {
if _, err := brc.NewBuilder().Build(); err != nil {
b.Fatalf("Build failed: %v", err)
}
})
}

// BenchmarkBuilderRESTConfig measures secret read + parse (no remote calls).
func BenchmarkBuilderRESTConfig(b *testing.B) {
benchBuilderOp(b, func(b *testing.B, brc *benchutil.BenchRemoteClient) {
if _, err := brc.NewBuilder().RESTConfig(); err != nil {
b.Fatalf("RESTConfig failed: %v", err)
}
})
}

// BenchmarkBuilderBuildDynamic measures BuildDynamic (no discovery).
func BenchmarkBuilderBuildDynamic(b *testing.B) {
benchBuilderOp(b, func(b *testing.B, brc *benchutil.BenchRemoteClient) {
if _, err := brc.NewBuilder().BuildDynamic(); err != nil {
b.Fatalf("BuildDynamic failed: %v", err)
}
})
}

// BenchmarkBuilderBuildKubeClient measures BuildKubeClient (no discovery).
func BenchmarkBuilderBuildKubeClient(b *testing.B) {
benchBuilderOp(b, func(b *testing.B, brc *benchutil.BenchRemoteClient) {
if _, err := brc.NewBuilder().BuildKubeClient(); err != nil {
b.Fatalf("BuildKubeClient failed: %v", err)
}
})
}
14 changes: 14 additions & 0 deletions pkg/resource/benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package resource_test

import (
"os"
"testing"

"github.com/openshift/hive/test/benchutil"
)

func TestMain(m *testing.M) {
code := m.Run()
benchutil.StopEnvTest()
os.Exit(code)
}
Loading