From 5a0c28d87b2a822f536c30daf28357bea6e9d858 Mon Sep 17 00:00:00 2001 From: nmirasch Date: Mon, 26 Oct 2026 18:26:28 +0100 Subject: [PATCH 1/5] Go runtime auto-tuning GOMEMLIMIT/GOMAXPROCS for application-controller Co-Authored-By: Claude Sonnet 5 Signed-off-by: nmirasch --- argocd-operator/api/v1beta1/argocd_types.go | 50 ++++ .../api/v1beta1/zz_generated.deepcopy.go | 35 +++ argocd-operator/common/defaults.go | 6 + argocd-operator/common/keys.go | 10 + .../controllers/argocd/statefulset.go | 3 + .../controllers/argocd/statefulset_test.go | 133 +++++++++ argocd-operator/controllers/argocd/util.go | 65 +++++ .../controllers/argocd/util_test.go | 211 +++++++++++++++ bundle/manifests/argoproj.io_argocds.yaml | 49 ++++ config/crd/bases/argoproj.io_argocds.yaml | 26 ++ ...idate_controller_go_runtime_tuning_test.go | 255 ++++++++++++++++++ 11 files changed, 843 insertions(+) create mode 100644 test/openshift/e2e/ginkgo/parallel/1-144_validate_controller_go_runtime_tuning_test.go diff --git a/argocd-operator/api/v1beta1/argocd_types.go b/argocd-operator/api/v1beta1/argocd_types.go index ad8614a3978..7c59d5e40b9 100644 --- a/argocd-operator/api/v1beta1/argocd_types.go +++ b/argocd-operator/api/v1beta1/argocd_types.go @@ -136,12 +136,62 @@ type ArgoCDApplicationControllerSpec struct { // Metrics defines the metrics configuration for the Application Controller ServiceMonitor. Metrics *ArgoCDMetricsSpec `json:"metrics,omitempty"` + + // GoRuntime configures automatic Go runtime tuning for the Application Controller. + GoRuntime *ArgoCDGoRuntimeSpec `json:"goRuntime,omitempty"` } func (a *ArgoCDApplicationControllerSpec) IsEnabled() bool { return a.Enabled == nil || (a.Enabled != nil && *a.Enabled) } +// ArgoCDGoRuntimeSpec defines per-instance options for automatic Go runtime tuning (GOMEMLIMIT, GOMAXPROCS). +// Tuning is enabled cluster-wide by default; set ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING or +// ENABLE_GO_RUNTIME_TUNING to "false" on the operator to disable. This spec allows per-instance opt-out +// and customization of memory and CPU percentages. Currently used only in ArgoCDApplicationControllerSpec. +type ArgoCDGoRuntimeSpec struct { + // Enabled toggles automatic Go runtime tuning for this instance. Defaults to true. + // Cannot override operator-level disable (ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING=false). + //+operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Enable Go Runtime Auto-Tuning",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:fieldGroup:Controller","urn:alm:descriptor:com.tectonic.ui:booleanSwitch"} + Enabled *bool `json:"enabled,omitempty"` + + // MemoryLimitPercent is the percentage (1-100) of the memory limit used for GOMEMLIMIT. Defaults to 90. + // Represents headroom reserved for non-heap memory and GC pauses. + //+kubebuilder:validation:Minimum=1 + //+kubebuilder:validation:Maximum=100 + //+operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Memory Limit Percent",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:fieldGroup:Controller","urn:alm:descriptor:com.tectonic.ui:number"} + MemoryLimitPercent *int32 `json:"memoryLimitPercent,omitempty"` + + // CPULimitPercent is the percentage (1-100) of the CPU limit used for GOMAXPROCS. Defaults to 100. + // Reserves CPU headroom for the kernel. Result is floored to nearest core, minimum 1. + //+kubebuilder:validation:Minimum=1 + //+kubebuilder:validation:Maximum=100 + //+operator-sdk:csv:customresourcedefinitions:type=spec,displayName="CPU Limit Percent",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:fieldGroup:Controller","urn:alm:descriptor:com.tectonic.ui:number"} + CPULimitPercent *int32 `json:"cpuLimitPercent,omitempty"` +} + +// IsEnabled returns whether Go runtime tuning is enabled for this ArgoCDGoRuntimeSpec. +// A nil receiver (i.e. .Spec.Controller.GoRuntime is unset) or a nil Enabled field defaults to true. +func (g *ArgoCDGoRuntimeSpec) IsEnabled() bool { + return g == nil || g.Enabled == nil || *g.Enabled +} + +// GetMemoryLimitPercent returns the configured MemoryLimitPercent, or defaultPercent if unset. +func (g *ArgoCDGoRuntimeSpec) GetMemoryLimitPercent(defaultPercent int32) int32 { + if g == nil || g.MemoryLimitPercent == nil { + return defaultPercent + } + return *g.MemoryLimitPercent +} + +// GetCPULimitPercent returns the configured CPULimitPercent, or defaultPercent if unset. +func (g *ArgoCDGoRuntimeSpec) GetCPULimitPercent(defaultPercent int32) int32 { + if g == nil || g.CPULimitPercent == nil { + return defaultPercent + } + return *g.CPULimitPercent +} + // ArgoCDApplicationControllerShardSpec defines the options available for enabling sharding for the Application Controller component. type ArgoCDApplicationControllerShardSpec struct { // Enabled defines whether sharding should be enabled on the Application Controller component. diff --git a/argocd-operator/api/v1beta1/zz_generated.deepcopy.go b/argocd-operator/api/v1beta1/zz_generated.deepcopy.go index 75edb36203c..8f939ead440 100644 --- a/argocd-operator/api/v1beta1/zz_generated.deepcopy.go +++ b/argocd-operator/api/v1beta1/zz_generated.deepcopy.go @@ -359,6 +359,11 @@ func (in *ArgoCDApplicationControllerSpec) DeepCopyInto(out *ArgoCDApplicationCo *out = new(ArgoCDMetricsSpec) **out = **in } + if in.GoRuntime != nil { + in, out := &in.GoRuntime, &out.GoRuntime + *out = new(ArgoCDGoRuntimeSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArgoCDApplicationControllerSpec. @@ -585,6 +590,36 @@ func (in *ArgoCDDexSpec) DeepCopy() *ArgoCDDexSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ArgoCDGoRuntimeSpec) DeepCopyInto(out *ArgoCDGoRuntimeSpec) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.MemoryLimitPercent != nil { + in, out := &in.MemoryLimitPercent, &out.MemoryLimitPercent + *out = new(int32) + **out = **in + } + if in.CPULimitPercent != nil { + in, out := &in.CPULimitPercent, &out.CPULimitPercent + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArgoCDGoRuntimeSpec. +func (in *ArgoCDGoRuntimeSpec) DeepCopy() *ArgoCDGoRuntimeSpec { + if in == nil { + return nil + } + out := new(ArgoCDGoRuntimeSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ArgoCDGrafanaSpec) DeepCopyInto(out *ArgoCDGrafanaSpec) { *out = *in diff --git a/argocd-operator/common/defaults.go b/argocd-operator/common/defaults.go index 2143eaca5bb..a0be6d63d1a 100644 --- a/argocd-operator/common/defaults.go +++ b/argocd-operator/common/defaults.go @@ -27,6 +27,12 @@ const ( // ArgoCDApplicationControllerDefaultShardReplicas is the default number of replicas that the ArgoCD Application Controller Should Use ArgocdApplicationControllerDefaultReplicas = 1 + // ArgoCDDefaultControllerMemLimitPercent is the default percentage of the controller's memory limit used to derive GOMEMLIMIT when Go runtime tuning is enabled. + ArgoCDDefaultControllerMemLimitPercent = 90 + + // ArgoCDDefaultControllerCPULimitPercent is the default percentage of the controller's CPU limit used to derive GOMAXPROCS when Go runtime tuning is enabled. + ArgoCDDefaultControllerCPULimitPercent = 100 + // ArgoCDDefaultLogLevel is the default log level to be used by all ArgoCD components. ArgoCDDefaultLogLevel = "info" diff --git a/argocd-operator/common/keys.go b/argocd-operator/common/keys.go index 915b3a9a447..a048e61d0a2 100644 --- a/argocd-operator/common/keys.go +++ b/argocd-operator/common/keys.go @@ -294,6 +294,16 @@ const ( // the Namespace Management feature is enabled. EnableManagedNamespace = "ALLOW_NAMESPACE_MANAGEMENT_IN_NAMESPACE_SCOPED_INSTANCES" + // EnableApplicationControllerGoRuntimeTuning enables automatic Go runtime tuning (GOMEMLIMIT, GOMAXPROCS) + // for the application controller. Enabled by default; set to "false" to disable cluster-wide. + EnableApplicationControllerGoRuntimeTuning = "ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING" + + // EnableGoRuntimeTuning is a global, component-agnostic alias/fallback for EnableApplicationControllerGoRuntimeTuning. + // It is intended to gate Go runtime tuning across all GitOps components (not just the application controller) as the + // feature expands to other workloads (e.g. repo-server, server). If EnableApplicationControllerGoRuntimeTuning is unset + // on the operator, this variable is consulted as a cluster-wide default; if both are unset, tuning defaults to enabled. + EnableGoRuntimeTuning = "ENABLE_GO_RUNTIME_TUNING" + // ArgoCDImageUpdaterImageEnvName is the environment variable used to get the image // to used for the Image Updater container. ArgoCDImageUpdaterImageEnvName = "ARGOCD_IMAGE_UPDATER_IMAGE" diff --git a/argocd-operator/controllers/argocd/statefulset.go b/argocd-operator/controllers/argocd/statefulset.go index ce4cff005de..c88f73676b0 100644 --- a/argocd-operator/controllers/argocd/statefulset.go +++ b/argocd-operator/controllers/argocd/statefulset.go @@ -683,6 +683,9 @@ func (r *ReconcileArgoCD) reconcileApplicationControllerStatefulSet(cr *argoproj // Let user specify their own environment first controllerEnv = argoutil.EnvMerge(controllerEnv, proxyEnvVars(), false) controllerEnv = argoutil.EnvMerge(controllerEnv, argoutil.GetRedisAuthEnv(), false) + // Go runtime tuning (GOMEMLIMIT, GOMAXPROCS) is derived from resource limits, + // but .Spec.Controller.Env always takes precedence. + controllerEnv = argoutil.EnvMerge(controllerEnv, getArgoApplicationControllerGoRuntimeEnv(cr), false) if cr.Spec.Controller.InitContainers != nil { ss.Spec.Template.Spec.InitContainers = append(ss.Spec.Template.Spec.InitContainers, cr.Spec.Controller.InitContainers...) diff --git a/argocd-operator/controllers/argocd/statefulset_test.go b/argocd-operator/controllers/argocd/statefulset_test.go index 142d9bccba8..9b28474e51e 100644 --- a/argocd-operator/controllers/argocd/statefulset_test.go +++ b/argocd-operator/controllers/argocd/statefulset_test.go @@ -3,6 +3,7 @@ package argocd import ( "context" "fmt" + "os" "reflect" "testing" "time" @@ -941,6 +942,138 @@ func TestReconcileAppController_Initcontainer(t *testing.T) { assert.Equal(t, 0, len(ss.Spec.Template.Spec.InitContainers)) } +func TestReconcileAppController_GoRuntimeEnv(t *testing.T) { + a := makeTestArgoCD(func(a *argoproj.ArgoCD) { + a.Spec.Controller.Resources = &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resourcev1.MustParse("2000Mi"), + corev1.ResourceCPU: resourcev1.MustParse("2000m"), + }, + } + }) + + resObjs := []client.Object{a} + subresObjs := []client.Object{a} + runtimeObjs := []runtime.Object{} + sch := makeTestReconcilerScheme(argoproj.AddToScheme, promoter.AddToScheme) + cl := makeTestReconcilerClient(sch, resObjs, subresObjs, runtimeObjs) + r := makeTestReconciler(cl, sch, testclient.NewSimpleClientset()) + + getControllerEnv := func() []corev1.EnvVar { + ss := &appsv1.StatefulSet{} + assert.NoError(t, r.Get( + context.TODO(), + types.NamespacedName{ + Name: applicationControllerResourceName(a), + Namespace: a.Namespace, + }, + ss)) + return ss.Spec.Template.Spec.Containers[0].Env + } + + // Go runtime tuning is enabled by default: GOMEMLIMIT and GOMAXPROCS should both be derived from the + // configured memory/CPU limits. + assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false)) + assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMEMLIMIT", Value: "1887436800"}) // 2000Mi * 90 / 100, in bytes + assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMAXPROCS", Value: "2"}) // 2000m * 100 / 100000 = 2 cores + + // A user-specified value in .Spec.Controller.Env should always win over the derived value, for both vars. + a.Spec.Controller.Env = []corev1.EnvVar{ + {Name: "GOMEMLIMIT", Value: "999999999"}, + {Name: "GOMAXPROCS", Value: "1"}, + } + assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false)) + assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMEMLIMIT", Value: "999999999"}) + assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMAXPROCS", Value: "1"}) + + // Disabling the feature at the operator level should remove both derived vars. + a.Spec.Controller.Env = nil + os.Setenv(common.EnableApplicationControllerGoRuntimeTuning, "false") + + assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false)) + for _, e := range getControllerEnv() { + assert.NotEqual(t, "GOMEMLIMIT", e.Name, "GOMEMLIMIT should not be set when the feature is disabled at the operator level") + assert.NotEqual(t, "GOMAXPROCS", e.Name, "GOMAXPROCS should not be set when the feature is disabled at the operator level") + } + + // A per-instance .Spec.Controller.GoRuntime.Enabled=true cannot override the operator-level kill switch. + trueVal := true + a.Spec.Controller.GoRuntime = &argoproj.ArgoCDGoRuntimeSpec{Enabled: &trueVal} + assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false)) + for _, e := range getControllerEnv() { + assert.NotEqual(t, "GOMEMLIMIT", e.Name, "GOMEMLIMIT should not be set when the operator-level kill switch is on, regardless of the per-instance field") + assert.NotEqual(t, "GOMAXPROCS", e.Name, "GOMAXPROCS should not be set when the operator-level kill switch is on, regardless of the per-instance field") + } + + os.Unsetenv(common.EnableApplicationControllerGoRuntimeTuning) + + // With the operator-level kill switch removed again, the per-instance Enabled=true field is a no-op (same as default-on). + assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false)) + assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMEMLIMIT", Value: "1887436800"}) + assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMAXPROCS", Value: "2"}) + + // A per-instance .Spec.Controller.GoRuntime.Enabled=false opt-out should remove both derived vars. + falseVal := false + a.Spec.Controller.GoRuntime = &argoproj.ArgoCDGoRuntimeSpec{Enabled: &falseVal} + assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false)) + for _, e := range getControllerEnv() { + assert.NotEqual(t, "GOMEMLIMIT", e.Name, "GOMEMLIMIT should not be set when the per-instance GoRuntime.Enabled field is false") + assert.NotEqual(t, "GOMAXPROCS", e.Name, "GOMAXPROCS should not be set when the per-instance GoRuntime.Enabled field is false") + } + + // A per-instance .Spec.Controller.GoRuntime.MemoryLimitPercent should override the default 90% headroom, + // and should not affect GOMAXPROCS (memory and CPU tuning are independent). + var fiftyPercent int32 = 50 + a.Spec.Controller.GoRuntime = &argoproj.ArgoCDGoRuntimeSpec{MemoryLimitPercent: &fiftyPercent} + assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false)) + assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMEMLIMIT", Value: "1048576000"}) // 2000Mi * 50 / 100, in bytes + assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMAXPROCS", Value: "2"}) // unaffected, still default 100% of 2000m + + // A per-instance .Spec.Controller.GoRuntime.CPULimitPercent should override the default 100% CPU headroom, + // and should not affect GOMEMLIMIT (memory and CPU tuning are independent). 2000m * 20 / 100000 = 0.4, floors to 1. + var twentyPercent int32 = 20 + a.Spec.Controller.GoRuntime = &argoproj.ArgoCDGoRuntimeSpec{CPULimitPercent: &twentyPercent} + assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false)) + assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMAXPROCS", Value: "1"}) + assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMEMLIMIT", Value: "1887436800"}) // unaffected, back to default 90% of 2000Mi + + a.Spec.Controller.GoRuntime = nil +} + +// TestReconcileAppController_GoRuntimeEnv_GOMAXPROCSFloor verifies that GOMAXPROCS is floored at 1 (never 0) +// for sub-1-core CPU limits, through the full reconcileApplicationControllerStatefulSet path (not just the +// getArgoApplicationControllerGoRuntimeEnv unit level). +func TestReconcileAppController_GoRuntimeEnv_GOMAXPROCSFloor(t *testing.T) { + a := makeTestArgoCD(func(a *argoproj.ArgoCD) { + a.Spec.Controller.Resources = &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resourcev1.MustParse("250m"), // 0.25 cores + }, + } + }) + + resObjs := []client.Object{a} + subresObjs := []client.Object{a} + runtimeObjs := []runtime.Object{} + sch := makeTestReconcilerScheme(argoproj.AddToScheme, promoter.AddToScheme) + cl := makeTestReconcilerClient(sch, resObjs, subresObjs, runtimeObjs) + r := makeTestReconciler(cl, sch, testclient.NewSimpleClientset()) + + assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false)) + + ss := &appsv1.StatefulSet{} + assert.NoError(t, r.Get( + context.TODO(), + types.NamespacedName{ + Name: applicationControllerResourceName(a), + Namespace: a.Namespace, + }, + ss)) + + // 250m * 100 / 100000 = 0.25, which floors to 1, never 0 (Go runtime panics on GOMAXPROCS=0). + assert.Contains(t, ss.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: "GOMAXPROCS", Value: "1"}) +} + func TestReconcileArgoCD_sidecarcontainer(t *testing.T) { a := makeTestArgoCD(func(a *argoproj.ArgoCD) { a.Spec.Controller.SidecarContainers = []corev1.Container{ diff --git a/argocd-operator/controllers/argocd/util.go b/argocd-operator/controllers/argocd/util.go index 41f8bd50888..9b6507e647b 100644 --- a/argocd-operator/controllers/argocd/util.go +++ b/argocd-operator/controllers/argocd/util.go @@ -172,6 +172,71 @@ func getArgoApplicationControllerResources(cr *argoproj.ArgoCD) corev1.ResourceR return resources } +// isApplicationControllerGoRuntimeTuningEnabledOnOperator reports whether operator-level Go runtime +// tuning is enabled. ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING takes precedence over +// ENABLE_GO_RUNTIME_TUNING; both default to true. +func isApplicationControllerGoRuntimeTuningEnabledOnOperator() bool { + if v, ok := os.LookupEnv(common.EnableApplicationControllerGoRuntimeTuning); ok { + return v != "false" + } + if v, ok := os.LookupEnv(common.EnableGoRuntimeTuning); ok { + return v != "false" + } + return true +} + +// getArgoApplicationControllerGoRuntimeEnv returns GOMEMLIMIT and GOMAXPROCS for the application +// controller, derived as a percentage of its memory and CPU limits respectively. +func getArgoApplicationControllerGoRuntimeEnv(cr *argoproj.ArgoCD) []corev1.EnvVar { + env := make([]corev1.EnvVar, 0) + + // Default-on: only skip Go runtime tuning if it has been explicitly disabled on the operator, via either + // the component-specific gate or the global alias (component-specific takes precedence if both are set). + if !isApplicationControllerGoRuntimeTuningEnabledOnOperator() { + return env + } + + // Per-instance opt-out. Note that GoRuntime.IsEnabled() defaults to true when GoRuntime (or GoRuntime.Enabled) + // is unset, so this only short-circuits when the CR author has explicitly set enabled: false. + if !cr.Spec.Controller.GoRuntime.IsEnabled() { + return env + } + + resources := getArgoApplicationControllerResources(cr) + + // Compute GOMEMLIMIT from memory limit if configured + memLimit := resources.Limits.Memory() + if memLimit != nil && !memLimit.IsZero() { + memLimitPercent := cr.Spec.Controller.GoRuntime.GetMemoryLimitPercent(common.ArgoCDDefaultControllerMemLimitPercent) + goMemLimitBytes := memLimit.Value() * int64(memLimitPercent) / 100 + if goMemLimitBytes > 0 { + env = append(env, corev1.EnvVar{ + Name: "GOMEMLIMIT", + Value: fmt.Sprintf("%d", goMemLimitBytes), + }) + } + } + + // Compute GOMAXPROCS from CPU limit if configured + cpuLimit := resources.Limits.Cpu() + if cpuLimit != nil && !cpuLimit.IsZero() { + cpuLimitPercent := cr.Spec.Controller.GoRuntime.GetCPULimitPercent(common.ArgoCDDefaultControllerCPULimitPercent) + // GOMAXPROCS = floor(millicores * percent / 100000), i.e. a truncated percentage of the + // CPU limit in whole cores. Truncation (like GOMEMLIMIT above) never allows more + // concurrency than the percentage permits. Floored to 1 because GOMAXPROCS=0 is invalid. + gomaxprocsValue := cpuLimit.MilliValue() * int64(cpuLimitPercent) / 100000 + if gomaxprocsValue < 1 { + gomaxprocsValue = 1 + } + env = append(env, corev1.EnvVar{ + Name: "GOMAXPROCS", + Value: fmt.Sprintf("%d", gomaxprocsValue), + }) + } + + return env +} + // getArgoApplicationControllerCommand will return the command for the ArgoCD Application Controller component. func getArgoApplicationControllerCommand(cr *argoproj.ArgoCD, useTLSForRedis bool) []string { allowed := argoutil.IsNamespaceClusterConfigNamespace(cr.Namespace) diff --git a/argocd-operator/controllers/argocd/util_test.go b/argocd-operator/controllers/argocd/util_test.go index 2e1f75ca48f..0ffdc2af32b 100644 --- a/argocd-operator/controllers/argocd/util_test.go +++ b/argocd-operator/controllers/argocd/util_test.go @@ -3,6 +3,7 @@ package argocd import ( "context" b64 "encoding/base64" + "os" "reflect" "strings" "testing" @@ -17,6 +18,7 @@ import ( "github.com/redhat-developer/gitops-operator/argocd-operator/controllers/argoutil" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" testclient "k8s.io/client-go/kubernetes/fake" @@ -665,6 +667,215 @@ func TestGetArgoApplicationContainerEnv(t *testing.T) { } } +func controllerMemLimit(memLimit string) argoCDOpt { + return func(a *argoproj.ArgoCD) { + if a.Spec.Controller.Resources == nil { + a.Spec.Controller.Resources = &corev1.ResourceRequirements{} + } + if a.Spec.Controller.Resources.Limits == nil { + a.Spec.Controller.Resources.Limits = corev1.ResourceList{} + } + a.Spec.Controller.Resources.Limits[corev1.ResourceMemory] = resource.MustParse(memLimit) + } +} + +func controllerGoRuntimeEnabled(enabled bool) argoCDOpt { + return func(a *argoproj.ArgoCD) { + if a.Spec.Controller.GoRuntime == nil { + a.Spec.Controller.GoRuntime = &argoproj.ArgoCDGoRuntimeSpec{} + } + a.Spec.Controller.GoRuntime.Enabled = &enabled + } +} + +func controllerGoRuntimeMemLimitPercent(percent int32) argoCDOpt { + return func(a *argoproj.ArgoCD) { + if a.Spec.Controller.GoRuntime == nil { + a.Spec.Controller.GoRuntime = &argoproj.ArgoCDGoRuntimeSpec{} + } + a.Spec.Controller.GoRuntime.MemoryLimitPercent = &percent + } +} + +func controllerCPULimit(cpuLimit string) argoCDOpt { + return func(a *argoproj.ArgoCD) { + if a.Spec.Controller.Resources == nil { + a.Spec.Controller.Resources = &corev1.ResourceRequirements{} + } + if a.Spec.Controller.Resources.Limits == nil { + a.Spec.Controller.Resources.Limits = corev1.ResourceList{} + } + a.Spec.Controller.Resources.Limits[corev1.ResourceCPU] = resource.MustParse(cpuLimit) + } +} + +func controllerGoRuntimeCPULimitPercent(percent int32) argoCDOpt { + return func(a *argoproj.ArgoCD) { + if a.Spec.Controller.GoRuntime == nil { + a.Spec.Controller.GoRuntime = &argoproj.ArgoCDGoRuntimeSpec{} + } + a.Spec.Controller.GoRuntime.CPULimitPercent = &percent + } +} + +func TestGetArgoApplicationControllerGoRuntimeEnv(t *testing.T) { + tests := []struct { + name string + opts []argoCDOpt + envVarValue string // value of common.EnableApplicationControllerGoRuntimeTuning to set for the test, "" to unset + globalEnvVarValue string // value of common.EnableGoRuntimeTuning (global alias) to set for the test, "" to unset + want []corev1.EnvVar + }{ + { + name: "no memory limit configured: no GOMEMLIMIT is set, even though feature is enabled by default", + opts: nil, + want: []corev1.EnvVar{}, + }, + { + name: "memory limit configured: GOMEMLIMIT is derived as a percentage of the memory limit", + opts: []argoCDOpt{controllerMemLimit("2000Mi")}, + want: []corev1.EnvVar{ + {Name: "GOMEMLIMIT", Value: "1887436800"}, // 2000Mi * 90 / 100, in bytes + }, + }, + { + name: "feature explicitly disabled via operator env var: no GOMEMLIMIT is set, even with a memory limit configured", + opts: []argoCDOpt{controllerMemLimit("2000Mi")}, + envVarValue: "false", + want: []corev1.EnvVar{}, + }, + { + name: "feature explicitly enabled via operator env var (non-'false' value): behaves the same as default-on", + opts: []argoCDOpt{controllerMemLimit("2000Mi")}, + envVarValue: "true", + want: []corev1.EnvVar{ + {Name: "GOMEMLIMIT", Value: "1887436800"}, + }, + }, + { + name: "feature explicitly disabled via global alias env var (ENABLE_GO_RUNTIME_TUNING): no GOMEMLIMIT is set", + opts: []argoCDOpt{controllerMemLimit("2000Mi")}, + globalEnvVarValue: "false", + want: []corev1.EnvVar{}, + }, + { + name: "global alias env var enabled (ENABLE_GO_RUNTIME_TUNING=true), component-specific unset: behaves the same as default-on", + opts: []argoCDOpt{controllerMemLimit("2000Mi")}, + globalEnvVarValue: "true", + want: []corev1.EnvVar{ + {Name: "GOMEMLIMIT", Value: "1887436800"}, + }, + }, + { + name: "component-specific env var takes precedence over the global alias when both are set", + opts: []argoCDOpt{controllerMemLimit("2000Mi")}, + envVarValue: "true", + globalEnvVarValue: "false", + want: []corev1.EnvVar{ + {Name: "GOMEMLIMIT", Value: "1887436800"}, + }, + }, + { + name: "component-specific env var (false) takes precedence over the global alias (true) when both are set", + opts: []argoCDOpt{controllerMemLimit("2000Mi")}, + envVarValue: "false", + globalEnvVarValue: "true", + want: []corev1.EnvVar{}, + }, + { + name: "per-instance opt-out via .spec.controller.goRuntime.enabled=false: no GOMEMLIMIT is set, even though the operator-level default is on", + opts: []argoCDOpt{controllerMemLimit("2000Mi"), controllerGoRuntimeEnabled(false)}, + want: []corev1.EnvVar{}, + }, + { + name: "per-instance .spec.controller.goRuntime.enabled=true is a no-op when the operator-level default is already on", + opts: []argoCDOpt{controllerMemLimit("2000Mi"), controllerGoRuntimeEnabled(true)}, + want: []corev1.EnvVar{ + {Name: "GOMEMLIMIT", Value: "1887436800"}, + }, + }, + { + name: "per-instance .spec.controller.goRuntime.enabled=true cannot override the operator-level kill switch", + opts: []argoCDOpt{controllerMemLimit("2000Mi"), controllerGoRuntimeEnabled(true)}, + envVarValue: "false", + want: []corev1.EnvVar{}, + }, + { + name: "per-instance .spec.controller.goRuntime.memoryLimitPercent overrides the default 90% headroom", + opts: []argoCDOpt{controllerMemLimit("2000Mi"), controllerGoRuntimeMemLimitPercent(50)}, + want: []corev1.EnvVar{ + {Name: "GOMEMLIMIT", Value: "1048576000"}, // 2000Mi * 50 / 100, in bytes + }, + }, + { + name: "no CPU limit configured: no GOMAXPROCS is set, even though feature is enabled by default", + opts: nil, + want: []corev1.EnvVar{}, + }, + { + name: "CPU limit configured: GOMAXPROCS is derived from the CPU limit (full cores)", + opts: []argoCDOpt{controllerCPULimit("2000m")}, + want: []corev1.EnvVar{ + {Name: "GOMAXPROCS", Value: "2"}, // 2000m = 2 cores, 100% of 2 = 2 + }, + }, + { + name: "sub-core CPU limit: GOMAXPROCS floors at 1, never 0", + opts: []argoCDOpt{controllerCPULimit("250m")}, + want: []corev1.EnvVar{ + {Name: "GOMAXPROCS", Value: "1"}, // 250m = 0.25 cores, ceil(0.25) = 1 + }, + }, + { + name: "fractional CPU limit with default percent: GOMAXPROCS correctly computed", + opts: []argoCDOpt{controllerCPULimit("1500m")}, + want: []corev1.EnvVar{ + {Name: "GOMAXPROCS", Value: "1"}, // 1500m = 1.5 cores, 100% of 1.5 = 1.5, ceil = 2, but actual calc: 1500 * 100 / 100000 = 1.5, floor at 1 due to rounding + }, + }, + { + name: "per-instance .spec.controller.goRuntime.cpuLimitPercent reduces GOMAXPROCS headroom", + opts: []argoCDOpt{controllerCPULimit("2000m"), controllerGoRuntimeCPULimitPercent(50)}, + want: []corev1.EnvVar{ + {Name: "GOMAXPROCS", Value: "1"}, // 2000m * 50 / 100000 = 1.0, ceil = 1 + }, + }, + { + name: "both memory and CPU limits configured: both GOMEMLIMIT and GOMAXPROCS are set", + opts: []argoCDOpt{controllerMemLimit("2000Mi"), controllerCPULimit("2000m")}, + want: []corev1.EnvVar{ + {Name: "GOMEMLIMIT", Value: "1887436800"}, // 2000Mi * 90 / 100 + {Name: "GOMAXPROCS", Value: "2"}, // 2000m * 100 / 100000 = 2.0 + }, + }, + { + name: "both memory and CPU limits, but GoRuntime disabled: no env vars are set", + opts: []argoCDOpt{controllerMemLimit("2000Mi"), controllerCPULimit("2000m"), controllerGoRuntimeEnabled(false)}, + want: []corev1.EnvVar{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.envVarValue != "" { + os.Setenv(common.EnableApplicationControllerGoRuntimeTuning, tt.envVarValue) + defer os.Unsetenv(common.EnableApplicationControllerGoRuntimeTuning) + } + if tt.globalEnvVarValue != "" { + os.Setenv(common.EnableGoRuntimeTuning, tt.globalEnvVarValue) + defer os.Unsetenv(common.EnableGoRuntimeTuning) + } + + cr := makeTestArgoCD(tt.opts...) + env := getArgoApplicationControllerGoRuntimeEnv(cr) + + if !reflect.DeepEqual(env, tt.want) { + t.Fatalf("got %#v, want %#v", env, tt.want) + } + }) + } +} + func TestDeleteRBACsForNamespace(t *testing.T) { a := makeTestArgoCD() testClient := testclient.NewSimpleClientset() diff --git a/bundle/manifests/argoproj.io_argocds.yaml b/bundle/manifests/argoproj.io_argocds.yaml index 73e6f038ae4..b262904ca3b 100644 --- a/bundle/manifests/argoproj.io_argocds.yaml +++ b/bundle/manifests/argoproj.io_argocds.yaml @@ -16301,6 +16301,55 @@ spec: items: type: string type: array + goRuntime: + description: |- + GoRuntime configures automatic Go runtime tuning (GOMEMLIMIT, GOMAXPROCS) for the Application + Controller container, derived from its configured Resources.Limits. This is an advanced + per-instance override of the cluster-wide default configured on the operator; see + ArgoCDGoRuntimeSpec for details. (optional) + properties: + cpuLimitPercent: + description: |- + CPULimitPercent is the percentage (1-100) of the Application Controller container's configured + CPU limit (.Spec.Controller.Resources.Limits.cpu) that is used to derive the Go runtime max CPU constraint + (GOMAXPROCS). Defaults to 100 if unset, meaning GOMAXPROCS is set to the full core count of the CPU limit. + + GOMAXPROCS controls the maximum number of OS threads that can execute Go code concurrently. A lower + percentage can be used to reserve CPU headroom for the kernel and other system processes, though this + may reduce application throughput. The result is truncated (floored) to the nearest whole core: for + example, with a 2000m (2-core) CPU limit and CPULimitPercent=80, GOMAXPROCS is set to 1 + (floor(2 * 0.8) = floor(1.6) = 1). It is recommended to keep this at 100 unless you observe excessive + context switching or need explicit CPU headroom management. + + Note: GOMAXPROCS is always floored at a minimum of 1, never 0, to prevent Go runtime panics - even on + sub-1-core limits (e.g. 250m) where the percentage-of-limit calculation alone would otherwise yield 0. + format: int32 + maximum: 100 + minimum: 1 + type: integer + enabled: + description: |- + Enabled toggles automatic Go runtime tuning (GOMEMLIMIT, GOMAXPROCS) for this ArgoCD instance's Application + Controller container. Defaults to true (enabled) if unset. + + Note: this field can only narrow the cluster-wide default - if Go runtime tuning has been disabled + for the whole operator (via the ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING environment variable + on the operator), setting Enabled to true here will not override that cluster-wide setting. + type: boolean + memoryLimitPercent: + description: |- + MemoryLimitPercent is the percentage (1-100) of the Application Controller container's configured + memory limit (.Spec.Controller.Resources.Limits.memory) that is used to derive the Go runtime soft + memory limit (GOMEMLIMIT), when Go runtime tuning is enabled. Defaults to 90 if unset. + + The percentage represents the headroom reserved for non-heap memory and GC pauses. For example, with a + 2Gi memory limit and the default 90%, GOMEMLIMIT is set to 1.8Gi, leaving 0.2Gi for kernel buffers, + non-heap allocations, and GC transient spikes. + format: int32 + maximum: 100 + minimum: 1 + type: integer + type: object initContainers: description: InitContainers defines the list of initialization containers for the Application Controller component. diff --git a/config/crd/bases/argoproj.io_argocds.yaml b/config/crd/bases/argoproj.io_argocds.yaml index 43b078a2d80..af491c71dda 100644 --- a/config/crd/bases/argoproj.io_argocds.yaml +++ b/config/crd/bases/argoproj.io_argocds.yaml @@ -16290,6 +16290,32 @@ spec: items: type: string type: array + goRuntime: + description: GoRuntime configures automatic Go runtime tuning + for the Application Controller. + properties: + cpuLimitPercent: + description: |- + CPULimitPercent is the percentage (1-100) of the CPU limit used for GOMAXPROCS. Defaults to 100. + Reserves CPU headroom for the kernel. Result is floored to nearest core, minimum 1. + format: int32 + maximum: 100 + minimum: 1 + type: integer + enabled: + description: |- + Enabled toggles automatic Go runtime tuning for this instance. Defaults to true. + Cannot override operator-level disable (ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING=false). + type: boolean + memoryLimitPercent: + description: |- + MemoryLimitPercent is the percentage (1-100) of the memory limit used for GOMEMLIMIT. Defaults to 90. + Represents headroom reserved for non-heap memory and GC pauses. + format: int32 + maximum: 100 + minimum: 1 + type: integer + type: object initContainers: description: InitContainers defines the list of initialization containers for the Application Controller component. diff --git a/test/openshift/e2e/ginkgo/parallel/1-144_validate_controller_go_runtime_tuning_test.go b/test/openshift/e2e/ginkgo/parallel/1-144_validate_controller_go_runtime_tuning_test.go new file mode 100644 index 00000000000..f2ab548a3a0 --- /dev/null +++ b/test/openshift/e2e/ginkgo/parallel/1-144_validate_controller_go_runtime_tuning_test.go @@ -0,0 +1,255 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package parallel + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + argov1beta1api "github.com/redhat-developer/gitops-operator/argocd-operator/api/v1beta1" + "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture" + argocdFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/argocd" + k8sFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/k8s" + statefulsetFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/statefulset" + fixtureUtils "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/utils" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("GitOps Operator Parallel E2E Tests", func() { + + Context("1-144_validate_controller_go_runtime_tuning", func() { + + var ( + k8sClient client.Client + ctx context.Context + ) + + BeforeEach(func() { + fixture.EnsureParallelCleanSlate() + k8sClient, _ = fixtureUtils.GetE2ETestKubeClient() + ctx = context.Background() + }) + + It("verifies that GOMEMLIMIT is automatically derived from the application controller's memory limit, and that a user-specified value in .spec.controller.env always takes precedence", func() { + + By("creating a namespace-scoped Argo CD instance with a memory limit configured on the controller") + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "argocd", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Controller: argov1beta1api.ArgoCDApplicationControllerSpec{ + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("250m"), + corev1.ResourceMemory: resource.MustParse("500Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1"), + // 2000Mi limit -> 90% GOMEMLIMIT default = 1887436800 bytes + corev1.ResourceMemory: resource.MustParse("2000Mi"), + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + ss := &appsv1.StatefulSet{ObjectMeta: metav1.ObjectMeta{Name: "argocd-application-controller", Namespace: ns.Name}} + Eventually(ss).Should(k8sFixture.ExistByName()) + + By("verifying that GOMEMLIMIT was automatically set on the application controller StatefulSet, based on the configured memory limit") + Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMEMLIMIT", "1887436800", 0)) + + By("setting a custom GOMEMLIMIT value via .spec.controller.env") + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.Controller.Env = []corev1.EnvVar{{Name: "GOMEMLIMIT", Value: "999999999"}} + }) + + By("verifying that the user-specified GOMEMLIMIT value always takes precedence over the automatically derived value") + Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMEMLIMIT", "999999999", 0)) + }) + + It("verifies that .spec.controller.goRuntime allows per-instance opt-out and memory percentage overrides", func() { + + By("creating a namespace-scoped Argo CD instance with a memory limit configured on the controller") + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "argocd", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Controller: argov1beta1api.ArgoCDApplicationControllerSpec{ + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + Limits: corev1.ResourceList{ + // 2000Mi limit -> 90% default GOMEMLIMIT = 1887436800 bytes + corev1.ResourceMemory: resource.MustParse("2000Mi"), + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + ss := &appsv1.StatefulSet{ObjectMeta: metav1.ObjectMeta{Name: "argocd-application-controller", Namespace: ns.Name}} + Eventually(ss).Should(k8sFixture.ExistByName()) + + By("verifying that GOMEMLIMIT is set by default, derived from the memory limit") + Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMEMLIMIT", "1887436800", 0)) + + By("setting .spec.controller.goRuntime.memoryLimitPercent to a custom value") + falseVal := false + var fiftyPercent int32 = 50 + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.Controller.GoRuntime = &argov1beta1api.ArgoCDGoRuntimeSpec{ + MemoryLimitPercent: &fiftyPercent, + } + }) + + By("verifying that GOMEMLIMIT is recomputed using the custom memory percentage") + Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMEMLIMIT", "1048576000", 0)) // 2000Mi * 50 / 100 + + By("setting .spec.controller.goRuntime.enabled to false to opt out of Go runtime tuning for this instance") + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.Controller.GoRuntime = &argov1beta1api.ArgoCDGoRuntimeSpec{ + Enabled: &falseVal, + } + }) + + By("verifying that GOMEMLIMIT is no longer present on the application-controller container") + Eventually(func() bool { + Expect(k8sClient.Get(ctx, client.ObjectKey{ + Name: ss.Name, + Namespace: ss.Namespace, + }, ss)).To(Succeed()) + + for _, e := range ss.Spec.Template.Spec.Containers[0].Env { + if e.Name == "GOMEMLIMIT" { + return false + } + } + return true + }, "60s", "5s").Should(BeTrue()) + }) + + It("verifies that .spec.controller.goRuntime.cpuLimitPercent allows GOMAXPROCS tuning", func() { + + By("creating a namespace-scoped Argo CD instance with CPU and memory limits configured on the controller") + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "argocd", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Controller: argov1beta1api.ArgoCDApplicationControllerSpec{ + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2000m"), // 2 cores + corev1.ResourceMemory: resource.MustParse("2000Mi"), + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + ss := &appsv1.StatefulSet{ObjectMeta: metav1.ObjectMeta{Name: "argocd-application-controller", Namespace: ns.Name}} + Eventually(ss).Should(k8sFixture.ExistByName()) + + By("verifying that GOMAXPROCS is set by default, derived from the CPU limit") + Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMAXPROCS", "2", 0)) + + By("verifying that GOMEMLIMIT is also set (both tuning knobs work together)") + Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMEMLIMIT", "1887436800", 0)) + + By("setting .spec.controller.goRuntime.cpuLimitPercent to reduce CPU scheduling headroom") + var twentyPercent int32 = 20 + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.Controller.GoRuntime = &argov1beta1api.ArgoCDGoRuntimeSpec{ + CPULimitPercent: &twentyPercent, + } + }) + + By("verifying that GOMAXPROCS is recomputed using the custom CPU percentage") + // 2000m * 20 / 100000 = 0.4, floor at 1 + Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMAXPROCS", "1", 0)) + + By("verifying that GOMEMLIMIT is unaffected by CPULimitPercent changes") + Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMEMLIMIT", "1887436800", 0)) + }) + + It("verifies that GOMAXPROCS is floored at 1 for sub-1-core CPU limits", func() { + + By("creating a namespace-scoped Argo CD instance with a sub-1-core CPU limit") + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "argocd", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Controller: argov1beta1api.ArgoCDApplicationControllerSpec{ + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("250m"), // 0.25 cores + corev1.ResourceMemory: resource.MustParse("2000Mi"), + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + ss := &appsv1.StatefulSet{ObjectMeta: metav1.ObjectMeta{Name: "argocd-application-controller", Namespace: ns.Name}} + Eventually(ss).Should(k8sFixture.ExistByName()) + + By("verifying that GOMAXPROCS is set to 1 (floored minimum), not 0") + Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMAXPROCS", "1", 0)) + }) + + }) +}) From 098795166524afefe9aad4dbb920766a6c7e55a5 Mon Sep 17 00:00:00 2001 From: nmirasch Date: Tue, 27 Oct 2026 11:22:34 +0100 Subject: [PATCH 2/5] Added comment code rabbit comments suggestions Signed-off-by: nmirasch --- argocd-operator/controllers/argocd/util_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/argocd-operator/controllers/argocd/util_test.go b/argocd-operator/controllers/argocd/util_test.go index 0ffdc2af32b..4c600631e32 100644 --- a/argocd-operator/controllers/argocd/util_test.go +++ b/argocd-operator/controllers/argocd/util_test.go @@ -823,21 +823,21 @@ func TestGetArgoApplicationControllerGoRuntimeEnv(t *testing.T) { name: "sub-core CPU limit: GOMAXPROCS floors at 1, never 0", opts: []argoCDOpt{controllerCPULimit("250m")}, want: []corev1.EnvVar{ - {Name: "GOMAXPROCS", Value: "1"}, // 250m = 0.25 cores, ceil(0.25) = 1 + {Name: "GOMAXPROCS", Value: "1"}, // 250m = 0.25 cores, floor(0.25) = 0, raised to the minimum of 1 }, }, { name: "fractional CPU limit with default percent: GOMAXPROCS correctly computed", opts: []argoCDOpt{controllerCPULimit("1500m")}, want: []corev1.EnvVar{ - {Name: "GOMAXPROCS", Value: "1"}, // 1500m = 1.5 cores, 100% of 1.5 = 1.5, ceil = 2, but actual calc: 1500 * 100 / 100000 = 1.5, floor at 1 due to rounding + {Name: "GOMAXPROCS", Value: "1"}, // 1500m * 100 / 100000 = 1.5, truncated to 1 }, }, { name: "per-instance .spec.controller.goRuntime.cpuLimitPercent reduces GOMAXPROCS headroom", opts: []argoCDOpt{controllerCPULimit("2000m"), controllerGoRuntimeCPULimitPercent(50)}, want: []corev1.EnvVar{ - {Name: "GOMAXPROCS", Value: "1"}, // 2000m * 50 / 100000 = 1.0, ceil = 1 + {Name: "GOMAXPROCS", Value: "1"}, // 2000m * 50 / 100000 = 1 }, }, { From 8719029f0613b66557967a2ae6213a85f3040eed Mon Sep 17 00:00:00 2001 From: nmirasch Date: Tue, 27 Oct 2026 14:03:14 +0100 Subject: [PATCH 3/5] Fix lint error Signed-off-by: nmirasch --- .../controllers/argocd/util_test.go | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/argocd-operator/controllers/argocd/util_test.go b/argocd-operator/controllers/argocd/util_test.go index 4c600631e32..64764c26ab6 100644 --- a/argocd-operator/controllers/argocd/util_test.go +++ b/argocd-operator/controllers/argocd/util_test.go @@ -667,7 +667,7 @@ func TestGetArgoApplicationContainerEnv(t *testing.T) { } } -func controllerMemLimit(memLimit string) argoCDOpt { +func controllerMemLimit() argoCDOpt { return func(a *argoproj.ArgoCD) { if a.Spec.Controller.Resources == nil { a.Spec.Controller.Resources = &corev1.ResourceRequirements{} @@ -675,7 +675,7 @@ func controllerMemLimit(memLimit string) argoCDOpt { if a.Spec.Controller.Resources.Limits == nil { a.Spec.Controller.Resources.Limits = corev1.ResourceList{} } - a.Spec.Controller.Resources.Limits[corev1.ResourceMemory] = resource.MustParse(memLimit) + a.Spec.Controller.Resources.Limits[corev1.ResourceMemory] = resource.MustParse("2000Mi") } } @@ -733,20 +733,20 @@ func TestGetArgoApplicationControllerGoRuntimeEnv(t *testing.T) { }, { name: "memory limit configured: GOMEMLIMIT is derived as a percentage of the memory limit", - opts: []argoCDOpt{controllerMemLimit("2000Mi")}, + opts: []argoCDOpt{controllerMemLimit()}, want: []corev1.EnvVar{ {Name: "GOMEMLIMIT", Value: "1887436800"}, // 2000Mi * 90 / 100, in bytes }, }, { name: "feature explicitly disabled via operator env var: no GOMEMLIMIT is set, even with a memory limit configured", - opts: []argoCDOpt{controllerMemLimit("2000Mi")}, + opts: []argoCDOpt{controllerMemLimit()}, envVarValue: "false", want: []corev1.EnvVar{}, }, { name: "feature explicitly enabled via operator env var (non-'false' value): behaves the same as default-on", - opts: []argoCDOpt{controllerMemLimit("2000Mi")}, + opts: []argoCDOpt{controllerMemLimit()}, envVarValue: "true", want: []corev1.EnvVar{ {Name: "GOMEMLIMIT", Value: "1887436800"}, @@ -754,13 +754,13 @@ func TestGetArgoApplicationControllerGoRuntimeEnv(t *testing.T) { }, { name: "feature explicitly disabled via global alias env var (ENABLE_GO_RUNTIME_TUNING): no GOMEMLIMIT is set", - opts: []argoCDOpt{controllerMemLimit("2000Mi")}, + opts: []argoCDOpt{controllerMemLimit()}, globalEnvVarValue: "false", want: []corev1.EnvVar{}, }, { name: "global alias env var enabled (ENABLE_GO_RUNTIME_TUNING=true), component-specific unset: behaves the same as default-on", - opts: []argoCDOpt{controllerMemLimit("2000Mi")}, + opts: []argoCDOpt{controllerMemLimit()}, globalEnvVarValue: "true", want: []corev1.EnvVar{ {Name: "GOMEMLIMIT", Value: "1887436800"}, @@ -768,7 +768,7 @@ func TestGetArgoApplicationControllerGoRuntimeEnv(t *testing.T) { }, { name: "component-specific env var takes precedence over the global alias when both are set", - opts: []argoCDOpt{controllerMemLimit("2000Mi")}, + opts: []argoCDOpt{controllerMemLimit()}, envVarValue: "true", globalEnvVarValue: "false", want: []corev1.EnvVar{ @@ -777,32 +777,32 @@ func TestGetArgoApplicationControllerGoRuntimeEnv(t *testing.T) { }, { name: "component-specific env var (false) takes precedence over the global alias (true) when both are set", - opts: []argoCDOpt{controllerMemLimit("2000Mi")}, + opts: []argoCDOpt{controllerMemLimit()}, envVarValue: "false", globalEnvVarValue: "true", want: []corev1.EnvVar{}, }, { name: "per-instance opt-out via .spec.controller.goRuntime.enabled=false: no GOMEMLIMIT is set, even though the operator-level default is on", - opts: []argoCDOpt{controllerMemLimit("2000Mi"), controllerGoRuntimeEnabled(false)}, + opts: []argoCDOpt{controllerMemLimit(), controllerGoRuntimeEnabled(false)}, want: []corev1.EnvVar{}, }, { name: "per-instance .spec.controller.goRuntime.enabled=true is a no-op when the operator-level default is already on", - opts: []argoCDOpt{controllerMemLimit("2000Mi"), controllerGoRuntimeEnabled(true)}, + opts: []argoCDOpt{controllerMemLimit(), controllerGoRuntimeEnabled(true)}, want: []corev1.EnvVar{ {Name: "GOMEMLIMIT", Value: "1887436800"}, }, }, { name: "per-instance .spec.controller.goRuntime.enabled=true cannot override the operator-level kill switch", - opts: []argoCDOpt{controllerMemLimit("2000Mi"), controllerGoRuntimeEnabled(true)}, + opts: []argoCDOpt{controllerMemLimit(), controllerGoRuntimeEnabled(true)}, envVarValue: "false", want: []corev1.EnvVar{}, }, { name: "per-instance .spec.controller.goRuntime.memoryLimitPercent overrides the default 90% headroom", - opts: []argoCDOpt{controllerMemLimit("2000Mi"), controllerGoRuntimeMemLimitPercent(50)}, + opts: []argoCDOpt{controllerMemLimit(), controllerGoRuntimeMemLimitPercent(50)}, want: []corev1.EnvVar{ {Name: "GOMEMLIMIT", Value: "1048576000"}, // 2000Mi * 50 / 100, in bytes }, @@ -842,7 +842,7 @@ func TestGetArgoApplicationControllerGoRuntimeEnv(t *testing.T) { }, { name: "both memory and CPU limits configured: both GOMEMLIMIT and GOMAXPROCS are set", - opts: []argoCDOpt{controllerMemLimit("2000Mi"), controllerCPULimit("2000m")}, + opts: []argoCDOpt{controllerMemLimit(), controllerCPULimit("2000m")}, want: []corev1.EnvVar{ {Name: "GOMEMLIMIT", Value: "1887436800"}, // 2000Mi * 90 / 100 {Name: "GOMAXPROCS", Value: "2"}, // 2000m * 100 / 100000 = 2.0 @@ -850,7 +850,7 @@ func TestGetArgoApplicationControllerGoRuntimeEnv(t *testing.T) { }, { name: "both memory and CPU limits, but GoRuntime disabled: no env vars are set", - opts: []argoCDOpt{controllerMemLimit("2000Mi"), controllerCPULimit("2000m"), controllerGoRuntimeEnabled(false)}, + opts: []argoCDOpt{controllerMemLimit(), controllerCPULimit("2000m"), controllerGoRuntimeEnabled(false)}, want: []corev1.EnvVar{}, }, } From ae679b97f6f72095d5d47efda474fe3dcdbc4273 Mon Sep 17 00:00:00 2001 From: nmirasch Date: Tue, 27 Oct 2026 16:20:47 +0100 Subject: [PATCH 4/5] document ENABLE_GO_RUNTIME_TUNING fallback in GoRuntime comments and regenerate CRDs Signed-off-by: nmirasch --- argocd-operator/api/v1beta1/argocd_types.go | 12 ++++-- bundle/manifests/argoproj.io_argocds.yaml | 42 ++++++--------------- config/crd/bases/argoproj.io_argocds.yaml | 11 ++++-- 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/argocd-operator/api/v1beta1/argocd_types.go b/argocd-operator/api/v1beta1/argocd_types.go index 7c59d5e40b9..d1eedbe909e 100644 --- a/argocd-operator/api/v1beta1/argocd_types.go +++ b/argocd-operator/api/v1beta1/argocd_types.go @@ -138,6 +138,10 @@ type ArgoCDApplicationControllerSpec struct { Metrics *ArgoCDMetricsSpec `json:"metrics,omitempty"` // GoRuntime configures automatic Go runtime tuning for the Application Controller. + // Tuning is enabled cluster-wide by default; set ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING + // (component-specific, takes precedence) or ENABLE_GO_RUNTIME_TUNING (global fallback) to "false" + // on the operator to disable it. When both are set, the component-specific setting wins, so a + // global "false" does not disable tuning if the component-specific setting is "true". GoRuntime *ArgoCDGoRuntimeSpec `json:"goRuntime,omitempty"` } @@ -146,12 +150,14 @@ func (a *ArgoCDApplicationControllerSpec) IsEnabled() bool { } // ArgoCDGoRuntimeSpec defines per-instance options for automatic Go runtime tuning (GOMEMLIMIT, GOMAXPROCS). -// Tuning is enabled cluster-wide by default; set ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING or -// ENABLE_GO_RUNTIME_TUNING to "false" on the operator to disable. This spec allows per-instance opt-out +// Tuning is enabled cluster-wide by default; set ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING +// (component-specific, takes precedence) or ENABLE_GO_RUNTIME_TUNING (global fallback) to "false" +// on the operator to disable. This spec allows per-instance opt-out // and customization of memory and CPU percentages. Currently used only in ArgoCDApplicationControllerSpec. type ArgoCDGoRuntimeSpec struct { // Enabled toggles automatic Go runtime tuning for this instance. Defaults to true. - // Cannot override operator-level disable (ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING=false). + // Cannot override operator-level disable (ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING=false, + // or ENABLE_GO_RUNTIME_TUNING=false when the component-specific setting is unset). //+operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Enable Go Runtime Auto-Tuning",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:fieldGroup:Controller","urn:alm:descriptor:com.tectonic.ui:booleanSwitch"} Enabled *bool `json:"enabled,omitempty"` diff --git a/bundle/manifests/argoproj.io_argocds.yaml b/bundle/manifests/argoproj.io_argocds.yaml index b262904ca3b..e5d1eb42895 100644 --- a/bundle/manifests/argoproj.io_argocds.yaml +++ b/bundle/manifests/argoproj.io_argocds.yaml @@ -16303,48 +16303,30 @@ spec: type: array goRuntime: description: |- - GoRuntime configures automatic Go runtime tuning (GOMEMLIMIT, GOMAXPROCS) for the Application - Controller container, derived from its configured Resources.Limits. This is an advanced - per-instance override of the cluster-wide default configured on the operator; see - ArgoCDGoRuntimeSpec for details. (optional) + GoRuntime configures automatic Go runtime tuning for the Application Controller. + Tuning is enabled cluster-wide by default; set ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING + (component-specific, takes precedence) or ENABLE_GO_RUNTIME_TUNING (global fallback) to "false" + on the operator to disable it. When both are set, the component-specific setting wins, so a + global "false" does not disable tuning if the component-specific setting is "true". properties: cpuLimitPercent: description: |- - CPULimitPercent is the percentage (1-100) of the Application Controller container's configured - CPU limit (.Spec.Controller.Resources.Limits.cpu) that is used to derive the Go runtime max CPU constraint - (GOMAXPROCS). Defaults to 100 if unset, meaning GOMAXPROCS is set to the full core count of the CPU limit. - - GOMAXPROCS controls the maximum number of OS threads that can execute Go code concurrently. A lower - percentage can be used to reserve CPU headroom for the kernel and other system processes, though this - may reduce application throughput. The result is truncated (floored) to the nearest whole core: for - example, with a 2000m (2-core) CPU limit and CPULimitPercent=80, GOMAXPROCS is set to 1 - (floor(2 * 0.8) = floor(1.6) = 1). It is recommended to keep this at 100 unless you observe excessive - context switching or need explicit CPU headroom management. - - Note: GOMAXPROCS is always floored at a minimum of 1, never 0, to prevent Go runtime panics - even on - sub-1-core limits (e.g. 250m) where the percentage-of-limit calculation alone would otherwise yield 0. + CPULimitPercent is the percentage (1-100) of the CPU limit used for GOMAXPROCS. Defaults to 100. + Reserves CPU headroom for the kernel. Result is floored to nearest core, minimum 1. format: int32 maximum: 100 minimum: 1 type: integer enabled: description: |- - Enabled toggles automatic Go runtime tuning (GOMEMLIMIT, GOMAXPROCS) for this ArgoCD instance's Application - Controller container. Defaults to true (enabled) if unset. - - Note: this field can only narrow the cluster-wide default - if Go runtime tuning has been disabled - for the whole operator (via the ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING environment variable - on the operator), setting Enabled to true here will not override that cluster-wide setting. + Enabled toggles automatic Go runtime tuning for this instance. Defaults to true. + Cannot override operator-level disable (ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING=false, + or ENABLE_GO_RUNTIME_TUNING=false when the component-specific setting is unset). type: boolean memoryLimitPercent: description: |- - MemoryLimitPercent is the percentage (1-100) of the Application Controller container's configured - memory limit (.Spec.Controller.Resources.Limits.memory) that is used to derive the Go runtime soft - memory limit (GOMEMLIMIT), when Go runtime tuning is enabled. Defaults to 90 if unset. - - The percentage represents the headroom reserved for non-heap memory and GC pauses. For example, with a - 2Gi memory limit and the default 90%, GOMEMLIMIT is set to 1.8Gi, leaving 0.2Gi for kernel buffers, - non-heap allocations, and GC transient spikes. + MemoryLimitPercent is the percentage (1-100) of the memory limit used for GOMEMLIMIT. Defaults to 90. + Represents headroom reserved for non-heap memory and GC pauses. format: int32 maximum: 100 minimum: 1 diff --git a/config/crd/bases/argoproj.io_argocds.yaml b/config/crd/bases/argoproj.io_argocds.yaml index af491c71dda..9a30fcbe795 100644 --- a/config/crd/bases/argoproj.io_argocds.yaml +++ b/config/crd/bases/argoproj.io_argocds.yaml @@ -16291,8 +16291,12 @@ spec: type: string type: array goRuntime: - description: GoRuntime configures automatic Go runtime tuning - for the Application Controller. + description: |- + GoRuntime configures automatic Go runtime tuning for the Application Controller. + Tuning is enabled cluster-wide by default; set ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING + (component-specific, takes precedence) or ENABLE_GO_RUNTIME_TUNING (global fallback) to "false" + on the operator to disable it. When both are set, the component-specific setting wins, so a + global "false" does not disable tuning if the component-specific setting is "true". properties: cpuLimitPercent: description: |- @@ -16305,7 +16309,8 @@ spec: enabled: description: |- Enabled toggles automatic Go runtime tuning for this instance. Defaults to true. - Cannot override operator-level disable (ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING=false). + Cannot override operator-level disable (ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING=false, + or ENABLE_GO_RUNTIME_TUNING=false when the component-specific setting is unset). type: boolean memoryLimitPercent: description: |- From dae00b4a63bdbad79cede6a606538774105f7f0b Mon Sep 17 00:00:00 2001 From: nmirasch Date: Wed, 28 Oct 2026 19:26:22 +0100 Subject: [PATCH 5/5] Default GOMEMLIMIT to 80% and drop cpuLimitPercent field Signed-off-by: nmirasch --- argocd-operator/api/v1beta1/argocd_types.go | 22 +++--------- .../api/v1beta1/zz_generated.deepcopy.go | 5 --- argocd-operator/common/defaults.go | 5 +-- .../controllers/argocd/statefulset_test.go | 20 ++++------- argocd-operator/controllers/argocd/util.go | 11 +++--- .../controllers/argocd/util_test.go | 36 ++++++------------- bundle/manifests/argoproj.io_argocds.yaml | 10 +----- ...gitops-operator.clusterserviceversion.yaml | 2 +- config/crd/bases/argoproj.io_argocds.yaml | 10 +----- ...idate_controller_go_runtime_tuning_test.go | 29 ++++----------- 10 files changed, 36 insertions(+), 114 deletions(-) diff --git a/argocd-operator/api/v1beta1/argocd_types.go b/argocd-operator/api/v1beta1/argocd_types.go index d1eedbe909e..e64bb827a82 100644 --- a/argocd-operator/api/v1beta1/argocd_types.go +++ b/argocd-operator/api/v1beta1/argocd_types.go @@ -152,8 +152,9 @@ func (a *ArgoCDApplicationControllerSpec) IsEnabled() bool { // ArgoCDGoRuntimeSpec defines per-instance options for automatic Go runtime tuning (GOMEMLIMIT, GOMAXPROCS). // Tuning is enabled cluster-wide by default; set ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING // (component-specific, takes precedence) or ENABLE_GO_RUNTIME_TUNING (global fallback) to "false" -// on the operator to disable. This spec allows per-instance opt-out -// and customization of memory and CPU percentages. Currently used only in ArgoCDApplicationControllerSpec. +// on the operator to disable. This spec allows per-instance opt-out and customization of the +// GOMEMLIMIT percentage. GOMAXPROCS is always derived at 100% of the CPU limit and is not +// configurable. Currently used only in ArgoCDApplicationControllerSpec. type ArgoCDGoRuntimeSpec struct { // Enabled toggles automatic Go runtime tuning for this instance. Defaults to true. // Cannot override operator-level disable (ENABLE_APPLICATION_CONTROLLER_GO_RUNTIME_TUNING=false, @@ -161,19 +162,12 @@ type ArgoCDGoRuntimeSpec struct { //+operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Enable Go Runtime Auto-Tuning",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:fieldGroup:Controller","urn:alm:descriptor:com.tectonic.ui:booleanSwitch"} Enabled *bool `json:"enabled,omitempty"` - // MemoryLimitPercent is the percentage (1-100) of the memory limit used for GOMEMLIMIT. Defaults to 90. + // MemoryLimitPercent is the percentage (1-100) of the memory limit used for GOMEMLIMIT. Defaults to 80. // Represents headroom reserved for non-heap memory and GC pauses. //+kubebuilder:validation:Minimum=1 //+kubebuilder:validation:Maximum=100 //+operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Memory Limit Percent",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:fieldGroup:Controller","urn:alm:descriptor:com.tectonic.ui:number"} MemoryLimitPercent *int32 `json:"memoryLimitPercent,omitempty"` - - // CPULimitPercent is the percentage (1-100) of the CPU limit used for GOMAXPROCS. Defaults to 100. - // Reserves CPU headroom for the kernel. Result is floored to nearest core, minimum 1. - //+kubebuilder:validation:Minimum=1 - //+kubebuilder:validation:Maximum=100 - //+operator-sdk:csv:customresourcedefinitions:type=spec,displayName="CPU Limit Percent",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:fieldGroup:Controller","urn:alm:descriptor:com.tectonic.ui:number"} - CPULimitPercent *int32 `json:"cpuLimitPercent,omitempty"` } // IsEnabled returns whether Go runtime tuning is enabled for this ArgoCDGoRuntimeSpec. @@ -190,14 +184,6 @@ func (g *ArgoCDGoRuntimeSpec) GetMemoryLimitPercent(defaultPercent int32) int32 return *g.MemoryLimitPercent } -// GetCPULimitPercent returns the configured CPULimitPercent, or defaultPercent if unset. -func (g *ArgoCDGoRuntimeSpec) GetCPULimitPercent(defaultPercent int32) int32 { - if g == nil || g.CPULimitPercent == nil { - return defaultPercent - } - return *g.CPULimitPercent -} - // ArgoCDApplicationControllerShardSpec defines the options available for enabling sharding for the Application Controller component. type ArgoCDApplicationControllerShardSpec struct { // Enabled defines whether sharding should be enabled on the Application Controller component. diff --git a/argocd-operator/api/v1beta1/zz_generated.deepcopy.go b/argocd-operator/api/v1beta1/zz_generated.deepcopy.go index 8f939ead440..87b53439036 100644 --- a/argocd-operator/api/v1beta1/zz_generated.deepcopy.go +++ b/argocd-operator/api/v1beta1/zz_generated.deepcopy.go @@ -603,11 +603,6 @@ func (in *ArgoCDGoRuntimeSpec) DeepCopyInto(out *ArgoCDGoRuntimeSpec) { *out = new(int32) **out = **in } - if in.CPULimitPercent != nil { - in, out := &in.CPULimitPercent, &out.CPULimitPercent - *out = new(int32) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArgoCDGoRuntimeSpec. diff --git a/argocd-operator/common/defaults.go b/argocd-operator/common/defaults.go index a0be6d63d1a..ccc3bfbcd71 100644 --- a/argocd-operator/common/defaults.go +++ b/argocd-operator/common/defaults.go @@ -28,10 +28,7 @@ const ( ArgocdApplicationControllerDefaultReplicas = 1 // ArgoCDDefaultControllerMemLimitPercent is the default percentage of the controller's memory limit used to derive GOMEMLIMIT when Go runtime tuning is enabled. - ArgoCDDefaultControllerMemLimitPercent = 90 - - // ArgoCDDefaultControllerCPULimitPercent is the default percentage of the controller's CPU limit used to derive GOMAXPROCS when Go runtime tuning is enabled. - ArgoCDDefaultControllerCPULimitPercent = 100 + ArgoCDDefaultControllerMemLimitPercent = 80 // ArgoCDDefaultLogLevel is the default log level to be used by all ArgoCD components. ArgoCDDefaultLogLevel = "info" diff --git a/argocd-operator/controllers/argocd/statefulset_test.go b/argocd-operator/controllers/argocd/statefulset_test.go index 9b28474e51e..36effab297b 100644 --- a/argocd-operator/controllers/argocd/statefulset_test.go +++ b/argocd-operator/controllers/argocd/statefulset_test.go @@ -974,8 +974,8 @@ func TestReconcileAppController_GoRuntimeEnv(t *testing.T) { // Go runtime tuning is enabled by default: GOMEMLIMIT and GOMAXPROCS should both be derived from the // configured memory/CPU limits. assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false)) - assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMEMLIMIT", Value: "1887436800"}) // 2000Mi * 90 / 100, in bytes - assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMAXPROCS", Value: "2"}) // 2000m * 100 / 100000 = 2 cores + assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMEMLIMIT", Value: "1677721600"}) // 2000Mi * 80 / 100, in bytes + assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMAXPROCS", Value: "2"}) // 2000m / 1000 = 2 cores // A user-specified value in .Spec.Controller.Env should always win over the derived value, for both vars. a.Spec.Controller.Env = []corev1.EnvVar{ @@ -1009,7 +1009,7 @@ func TestReconcileAppController_GoRuntimeEnv(t *testing.T) { // With the operator-level kill switch removed again, the per-instance Enabled=true field is a no-op (same as default-on). assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false)) - assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMEMLIMIT", Value: "1887436800"}) + assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMEMLIMIT", Value: "1677721600"}) assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMAXPROCS", Value: "2"}) // A per-instance .Spec.Controller.GoRuntime.Enabled=false opt-out should remove both derived vars. @@ -1021,21 +1021,13 @@ func TestReconcileAppController_GoRuntimeEnv(t *testing.T) { assert.NotEqual(t, "GOMAXPROCS", e.Name, "GOMAXPROCS should not be set when the per-instance GoRuntime.Enabled field is false") } - // A per-instance .Spec.Controller.GoRuntime.MemoryLimitPercent should override the default 90% headroom, + // A per-instance .Spec.Controller.GoRuntime.MemoryLimitPercent should override the default 80% headroom, // and should not affect GOMAXPROCS (memory and CPU tuning are independent). var fiftyPercent int32 = 50 a.Spec.Controller.GoRuntime = &argoproj.ArgoCDGoRuntimeSpec{MemoryLimitPercent: &fiftyPercent} assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false)) assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMEMLIMIT", Value: "1048576000"}) // 2000Mi * 50 / 100, in bytes - assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMAXPROCS", Value: "2"}) // unaffected, still default 100% of 2000m - - // A per-instance .Spec.Controller.GoRuntime.CPULimitPercent should override the default 100% CPU headroom, - // and should not affect GOMEMLIMIT (memory and CPU tuning are independent). 2000m * 20 / 100000 = 0.4, floors to 1. - var twentyPercent int32 = 20 - a.Spec.Controller.GoRuntime = &argoproj.ArgoCDGoRuntimeSpec{CPULimitPercent: &twentyPercent} - assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false)) - assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMAXPROCS", Value: "1"}) - assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMEMLIMIT", Value: "1887436800"}) // unaffected, back to default 90% of 2000Mi + assert.Contains(t, getControllerEnv(), corev1.EnvVar{Name: "GOMAXPROCS", Value: "2"}) // unaffected, GOMAXPROCS is always 100% of the CPU limit a.Spec.Controller.GoRuntime = nil } @@ -1070,7 +1062,7 @@ func TestReconcileAppController_GoRuntimeEnv_GOMAXPROCSFloor(t *testing.T) { }, ss)) - // 250m * 100 / 100000 = 0.25, which floors to 1, never 0 (Go runtime panics on GOMAXPROCS=0). + // 250m / 1000 = 0.25, which floors to 1, never 0 (Go runtime panics on GOMAXPROCS=0). assert.Contains(t, ss.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: "GOMAXPROCS", Value: "1"}) } diff --git a/argocd-operator/controllers/argocd/util.go b/argocd-operator/controllers/argocd/util.go index 9b6507e647b..091b165c333 100644 --- a/argocd-operator/controllers/argocd/util.go +++ b/argocd-operator/controllers/argocd/util.go @@ -217,14 +217,13 @@ func getArgoApplicationControllerGoRuntimeEnv(cr *argoproj.ArgoCD) []corev1.EnvV } } - // Compute GOMAXPROCS from CPU limit if configured + // Compute GOMAXPROCS from CPU limit if configured. GOMAXPROCS is intentionally not + // configurable per-instance; only the GOMEMLIMIT percentage has a knob. GOMAXPROCS is + // always 100% of the CPU limit, i.e. floor(millicores / 1000) whole cores, + // floored to 1 because GOMAXPROCS=0 is invalid. cpuLimit := resources.Limits.Cpu() if cpuLimit != nil && !cpuLimit.IsZero() { - cpuLimitPercent := cr.Spec.Controller.GoRuntime.GetCPULimitPercent(common.ArgoCDDefaultControllerCPULimitPercent) - // GOMAXPROCS = floor(millicores * percent / 100000), i.e. a truncated percentage of the - // CPU limit in whole cores. Truncation (like GOMEMLIMIT above) never allows more - // concurrency than the percentage permits. Floored to 1 because GOMAXPROCS=0 is invalid. - gomaxprocsValue := cpuLimit.MilliValue() * int64(cpuLimitPercent) / 100000 + gomaxprocsValue := cpuLimit.MilliValue() / 1000 if gomaxprocsValue < 1 { gomaxprocsValue = 1 } diff --git a/argocd-operator/controllers/argocd/util_test.go b/argocd-operator/controllers/argocd/util_test.go index 64764c26ab6..a97b3ec255c 100644 --- a/argocd-operator/controllers/argocd/util_test.go +++ b/argocd-operator/controllers/argocd/util_test.go @@ -709,15 +709,6 @@ func controllerCPULimit(cpuLimit string) argoCDOpt { } } -func controllerGoRuntimeCPULimitPercent(percent int32) argoCDOpt { - return func(a *argoproj.ArgoCD) { - if a.Spec.Controller.GoRuntime == nil { - a.Spec.Controller.GoRuntime = &argoproj.ArgoCDGoRuntimeSpec{} - } - a.Spec.Controller.GoRuntime.CPULimitPercent = &percent - } -} - func TestGetArgoApplicationControllerGoRuntimeEnv(t *testing.T) { tests := []struct { name string @@ -735,7 +726,7 @@ func TestGetArgoApplicationControllerGoRuntimeEnv(t *testing.T) { name: "memory limit configured: GOMEMLIMIT is derived as a percentage of the memory limit", opts: []argoCDOpt{controllerMemLimit()}, want: []corev1.EnvVar{ - {Name: "GOMEMLIMIT", Value: "1887436800"}, // 2000Mi * 90 / 100, in bytes + {Name: "GOMEMLIMIT", Value: "1677721600"}, // 2000Mi * 80 / 100, in bytes }, }, { @@ -749,7 +740,7 @@ func TestGetArgoApplicationControllerGoRuntimeEnv(t *testing.T) { opts: []argoCDOpt{controllerMemLimit()}, envVarValue: "true", want: []corev1.EnvVar{ - {Name: "GOMEMLIMIT", Value: "1887436800"}, + {Name: "GOMEMLIMIT", Value: "1677721600"}, }, }, { @@ -763,7 +754,7 @@ func TestGetArgoApplicationControllerGoRuntimeEnv(t *testing.T) { opts: []argoCDOpt{controllerMemLimit()}, globalEnvVarValue: "true", want: []corev1.EnvVar{ - {Name: "GOMEMLIMIT", Value: "1887436800"}, + {Name: "GOMEMLIMIT", Value: "1677721600"}, }, }, { @@ -772,7 +763,7 @@ func TestGetArgoApplicationControllerGoRuntimeEnv(t *testing.T) { envVarValue: "true", globalEnvVarValue: "false", want: []corev1.EnvVar{ - {Name: "GOMEMLIMIT", Value: "1887436800"}, + {Name: "GOMEMLIMIT", Value: "1677721600"}, }, }, { @@ -791,7 +782,7 @@ func TestGetArgoApplicationControllerGoRuntimeEnv(t *testing.T) { name: "per-instance .spec.controller.goRuntime.enabled=true is a no-op when the operator-level default is already on", opts: []argoCDOpt{controllerMemLimit(), controllerGoRuntimeEnabled(true)}, want: []corev1.EnvVar{ - {Name: "GOMEMLIMIT", Value: "1887436800"}, + {Name: "GOMEMLIMIT", Value: "1677721600"}, }, }, { @@ -801,7 +792,7 @@ func TestGetArgoApplicationControllerGoRuntimeEnv(t *testing.T) { want: []corev1.EnvVar{}, }, { - name: "per-instance .spec.controller.goRuntime.memoryLimitPercent overrides the default 90% headroom", + name: "per-instance .spec.controller.goRuntime.memoryLimitPercent overrides the default 80% headroom", opts: []argoCDOpt{controllerMemLimit(), controllerGoRuntimeMemLimitPercent(50)}, want: []corev1.EnvVar{ {Name: "GOMEMLIMIT", Value: "1048576000"}, // 2000Mi * 50 / 100, in bytes @@ -827,25 +818,18 @@ func TestGetArgoApplicationControllerGoRuntimeEnv(t *testing.T) { }, }, { - name: "fractional CPU limit with default percent: GOMAXPROCS correctly computed", + name: "fractional CPU limit: GOMAXPROCS correctly computed", opts: []argoCDOpt{controllerCPULimit("1500m")}, want: []corev1.EnvVar{ - {Name: "GOMAXPROCS", Value: "1"}, // 1500m * 100 / 100000 = 1.5, truncated to 1 - }, - }, - { - name: "per-instance .spec.controller.goRuntime.cpuLimitPercent reduces GOMAXPROCS headroom", - opts: []argoCDOpt{controllerCPULimit("2000m"), controllerGoRuntimeCPULimitPercent(50)}, - want: []corev1.EnvVar{ - {Name: "GOMAXPROCS", Value: "1"}, // 2000m * 50 / 100000 = 1 + {Name: "GOMAXPROCS", Value: "1"}, // 1500m / 1000 = 1.5, truncated to 1 }, }, { name: "both memory and CPU limits configured: both GOMEMLIMIT and GOMAXPROCS are set", opts: []argoCDOpt{controllerMemLimit(), controllerCPULimit("2000m")}, want: []corev1.EnvVar{ - {Name: "GOMEMLIMIT", Value: "1887436800"}, // 2000Mi * 90 / 100 - {Name: "GOMAXPROCS", Value: "2"}, // 2000m * 100 / 100000 = 2.0 + {Name: "GOMEMLIMIT", Value: "1677721600"}, // 2000Mi * 80 / 100 + {Name: "GOMAXPROCS", Value: "2"}, // 2000m / 1000 = 2.0 }, }, { diff --git a/bundle/manifests/argoproj.io_argocds.yaml b/bundle/manifests/argoproj.io_argocds.yaml index e5d1eb42895..283cbb1e928 100644 --- a/bundle/manifests/argoproj.io_argocds.yaml +++ b/bundle/manifests/argoproj.io_argocds.yaml @@ -16309,14 +16309,6 @@ spec: on the operator to disable it. When both are set, the component-specific setting wins, so a global "false" does not disable tuning if the component-specific setting is "true". properties: - cpuLimitPercent: - description: |- - CPULimitPercent is the percentage (1-100) of the CPU limit used for GOMAXPROCS. Defaults to 100. - Reserves CPU headroom for the kernel. Result is floored to nearest core, minimum 1. - format: int32 - maximum: 100 - minimum: 1 - type: integer enabled: description: |- Enabled toggles automatic Go runtime tuning for this instance. Defaults to true. @@ -16325,7 +16317,7 @@ spec: type: boolean memoryLimitPercent: description: |- - MemoryLimitPercent is the percentage (1-100) of the memory limit used for GOMEMLIMIT. Defaults to 90. + MemoryLimitPercent is the percentage (1-100) of the memory limit used for GOMEMLIMIT. Defaults to 80. Represents headroom reserved for non-heap memory and GC pauses. format: int32 maximum: 100 diff --git a/bundle/manifests/gitops-operator.clusterserviceversion.yaml b/bundle/manifests/gitops-operator.clusterserviceversion.yaml index 71d7f67572e..9941b46038f 100644 --- a/bundle/manifests/gitops-operator.clusterserviceversion.yaml +++ b/bundle/manifests/gitops-operator.clusterserviceversion.yaml @@ -190,7 +190,7 @@ metadata: capabilities: Deep Insights console.openshift.io/plugins: '["gitops-plugin"]' containerImage: quay.io/redhat-developer/gitops-operator - createdAt: "2026-10-22T01:59:19Z" + createdAt: "2026-10-28T18:21:56Z" description: Enables teams to adopt GitOps principles for managing cluster configurations and application delivery across hybrid multi-cluster Kubernetes environments. features.operators.openshift.io/disconnected: "true" diff --git a/config/crd/bases/argoproj.io_argocds.yaml b/config/crd/bases/argoproj.io_argocds.yaml index 9a30fcbe795..9942ee0ec6a 100644 --- a/config/crd/bases/argoproj.io_argocds.yaml +++ b/config/crd/bases/argoproj.io_argocds.yaml @@ -16298,14 +16298,6 @@ spec: on the operator to disable it. When both are set, the component-specific setting wins, so a global "false" does not disable tuning if the component-specific setting is "true". properties: - cpuLimitPercent: - description: |- - CPULimitPercent is the percentage (1-100) of the CPU limit used for GOMAXPROCS. Defaults to 100. - Reserves CPU headroom for the kernel. Result is floored to nearest core, minimum 1. - format: int32 - maximum: 100 - minimum: 1 - type: integer enabled: description: |- Enabled toggles automatic Go runtime tuning for this instance. Defaults to true. @@ -16314,7 +16306,7 @@ spec: type: boolean memoryLimitPercent: description: |- - MemoryLimitPercent is the percentage (1-100) of the memory limit used for GOMEMLIMIT. Defaults to 90. + MemoryLimitPercent is the percentage (1-100) of the memory limit used for GOMEMLIMIT. Defaults to 80. Represents headroom reserved for non-heap memory and GC pauses. format: int32 maximum: 100 diff --git a/test/openshift/e2e/ginkgo/parallel/1-144_validate_controller_go_runtime_tuning_test.go b/test/openshift/e2e/ginkgo/parallel/1-144_validate_controller_go_runtime_tuning_test.go index f2ab548a3a0..43dfe7f0e6e 100644 --- a/test/openshift/e2e/ginkgo/parallel/1-144_validate_controller_go_runtime_tuning_test.go +++ b/test/openshift/e2e/ginkgo/parallel/1-144_validate_controller_go_runtime_tuning_test.go @@ -67,7 +67,7 @@ var _ = Describe("GitOps Operator Parallel E2E Tests", func() { }, Limits: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("1"), - // 2000Mi limit -> 90% GOMEMLIMIT default = 1887436800 bytes + // 2000Mi limit -> 80% GOMEMLIMIT default = 1677721600 bytes corev1.ResourceMemory: resource.MustParse("2000Mi"), }, }, @@ -83,7 +83,7 @@ var _ = Describe("GitOps Operator Parallel E2E Tests", func() { Eventually(ss).Should(k8sFixture.ExistByName()) By("verifying that GOMEMLIMIT was automatically set on the application controller StatefulSet, based on the configured memory limit") - Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMEMLIMIT", "1887436800", 0)) + Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMEMLIMIT", "1677721600", 0)) By("setting a custom GOMEMLIMIT value via .spec.controller.env") argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { @@ -110,7 +110,7 @@ var _ = Describe("GitOps Operator Parallel E2E Tests", func() { corev1.ResourceMemory: resource.MustParse("128Mi"), }, Limits: corev1.ResourceList{ - // 2000Mi limit -> 90% default GOMEMLIMIT = 1887436800 bytes + // 2000Mi limit -> 80% default GOMEMLIMIT = 1677721600 bytes corev1.ResourceMemory: resource.MustParse("2000Mi"), }, }, @@ -126,7 +126,7 @@ var _ = Describe("GitOps Operator Parallel E2E Tests", func() { Eventually(ss).Should(k8sFixture.ExistByName()) By("verifying that GOMEMLIMIT is set by default, derived from the memory limit") - Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMEMLIMIT", "1887436800", 0)) + Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMEMLIMIT", "1677721600", 0)) By("setting .spec.controller.goRuntime.memoryLimitPercent to a custom value") falseVal := false @@ -163,7 +163,7 @@ var _ = Describe("GitOps Operator Parallel E2E Tests", func() { }, "60s", "5s").Should(BeTrue()) }) - It("verifies that .spec.controller.goRuntime.cpuLimitPercent allows GOMAXPROCS tuning", func() { + It("verifies that GOMAXPROCS is automatically derived from the CPU limit alongside GOMEMLIMIT", func() { By("creating a namespace-scoped Argo CD instance with CPU and memory limits configured on the controller") ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() @@ -197,23 +197,8 @@ var _ = Describe("GitOps Operator Parallel E2E Tests", func() { By("verifying that GOMAXPROCS is set by default, derived from the CPU limit") Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMAXPROCS", "2", 0)) - By("verifying that GOMEMLIMIT is also set (both tuning knobs work together)") - Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMEMLIMIT", "1887436800", 0)) - - By("setting .spec.controller.goRuntime.cpuLimitPercent to reduce CPU scheduling headroom") - var twentyPercent int32 = 20 - argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { - ac.Spec.Controller.GoRuntime = &argov1beta1api.ArgoCDGoRuntimeSpec{ - CPULimitPercent: &twentyPercent, - } - }) - - By("verifying that GOMAXPROCS is recomputed using the custom CPU percentage") - // 2000m * 20 / 100000 = 0.4, floor at 1 - Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMAXPROCS", "1", 0)) - - By("verifying that GOMEMLIMIT is unaffected by CPULimitPercent changes") - Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMEMLIMIT", "1887436800", 0)) + By("verifying that GOMEMLIMIT is also set (both derivations work together)") + Eventually(ss, "60s", "5s").Should(statefulsetFixture.HaveContainerWithEnvVar("GOMEMLIMIT", "1677721600", 0)) }) It("verifies that GOMAXPROCS is floored at 1 for sub-1-core CPU limits", func() {