diff --git a/argocd-operator/api/v1beta1/argocd_types.go b/argocd-operator/api/v1beta1/argocd_types.go index ad8614a3978..d1eedbe909e 100644 --- a/argocd-operator/api/v1beta1/argocd_types.go +++ b/argocd-operator/api/v1beta1/argocd_types.go @@ -136,12 +136,68 @@ 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. + // 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"` } 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 +// (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, + // 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"` + + // 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..64764c26ab6 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() 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("2000Mi") + } +} + +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()}, + 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()}, + 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()}, + 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()}, + 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()}, + 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()}, + 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()}, + 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(), 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(), 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(), controllerGoRuntimeEnabled(true)}, + envVarValue: "false", + want: []corev1.EnvVar{}, + }, + { + name: "per-instance .spec.controller.goRuntime.memoryLimitPercent overrides the default 90% headroom", + opts: []argoCDOpt{controllerMemLimit(), 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, 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 * 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: "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: "both memory and CPU limits, but GoRuntime disabled: no env vars are set", + opts: []argoCDOpt{controllerMemLimit(), 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..e5d1eb42895 100644 --- a/bundle/manifests/argoproj.io_argocds.yaml +++ b/bundle/manifests/argoproj.io_argocds.yaml @@ -16301,6 +16301,37 @@ spec: items: type: string type: array + goRuntime: + 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: |- + 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, + 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 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/config/crd/bases/argoproj.io_argocds.yaml b/config/crd/bases/argoproj.io_argocds.yaml index 43b078a2d80..9a30fcbe795 100644 --- a/config/crd/bases/argoproj.io_argocds.yaml +++ b/config/crd/bases/argoproj.io_argocds.yaml @@ -16290,6 +16290,37 @@ spec: items: type: string type: array + goRuntime: + 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: |- + 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, + 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 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)) + }) + + }) +})