From 9d23aebab27d69f0541eb2f3ad37c392c176c940 Mon Sep 17 00:00:00 2001 From: Jonathan Eid Date: Mon, 20 Jul 2026 15:41:27 -0400 Subject: [PATCH 1/2] http proxy: apply mission node affinity + tolerations to the proxy pod The scalable nginx HTTP proxy Deployment's pod carried no affinity/tolerations, so it could only schedule on untainted capacity. On a busy cluster where the mission's node pool is tainted/dedicated (e.g. --require-node-labels=purpose:largetests --tolerate-node-taints=largetests), the proxy stayed Pending -> 'proxy not ready after 60 attempts' -> formation build failed. Mirror the core pods' placement (self.Affinity() + self.Tolerations()) so the proxy lands on the same pool it fronts. Co-Authored-By: Claude Opus 4.8 --- src/FSLibrary/StellarKubeSpecs.fs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/FSLibrary/StellarKubeSpecs.fs b/src/FSLibrary/StellarKubeSpecs.fs index 64c35b1f..2e16857a 100644 --- a/src/FSLibrary/StellarKubeSpecs.fs +++ b/src/FSLibrary/StellarKubeSpecs.fs @@ -974,7 +974,16 @@ type NetworkCfg with configMap = V1ConfigMapVolumeSource(name = self.HttpProxyConfigMapName) ) - let podSpec = V1PodSpec(containers = [| container |], volumes = [| volume |]) + // Mirror the core pods' node placement so the proxy schedules onto the + // same (often tainted/dedicated) node pool it fronts -- otherwise it is + // confined to untainted capacity and can stay Pending on a busy cluster. + let podSpec = + V1PodSpec( + containers = [| container |], + volumes = [| volume |], + ?affinity = self.Affinity(), + tolerations = self.Tolerations() + ) let podTemplate = V1PodTemplateSpec(metadata = V1ObjectMeta(labels = self.HttpProxyLabels), spec = podSpec) From 23213f16b4f3338ef15d4544350b13a0a67be440 Mon Sep 17 00:00:00 2001 From: Jonathan Eid Date: Mon, 20 Jul 2026 15:52:19 -0400 Subject: [PATCH 2/2] test: assert HTTP proxy pod inherits mission node affinity + tolerations Regression guard for the placement fix -- existing tests only exercise the core pod template, so they would not catch the proxy dropping affinity/tolerations again. Builds a config with require-node-labels + tolerate-node-taints and asserts ToHttpProxyDeployment().Spec.Template.Spec carries both. Co-Authored-By: Claude Opus 4.8 --- src/FSLibrary.Tests/Tests.fs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/FSLibrary.Tests/Tests.fs b/src/FSLibrary.Tests/Tests.fs index e4b6f5c9..7a3c4f73 100644 --- a/src/FSLibrary.Tests/Tests.fs +++ b/src/FSLibrary.Tests/Tests.fs @@ -278,6 +278,41 @@ type Tests(output: ITestOutputHelper) = Assert.Null(spec.Affinity) Assert.Equal(nCfgPlain.Nonce, nCfgPlain.PodLabels().[CfgVal.runNonceLabelKey]) + [] + member __.``HTTP proxy pod inherits mission node affinity and tolerations``() = + // Regression: the HTTP proxy Deployment must carry the mission's node + // placement (self.Affinity()/self.Tolerations()) like the core pods it + // fronts -- otherwise it cannot schedule onto a tainted/dedicated pool + // and stays Pending on a busy cluster. + let nCfg = + MakeNetworkCfg + { ctx with + requireNodeLabels = [ ("purpose", Some "largetests") ] + tolerateNodeTaints = [ ("largetests", None) ] + installNetworkDelay = Some false } + [ coreSet ] + passOpt + + let spec = nCfg.ToHttpProxyDeployment().Spec.Template.Spec + + // Node affinity requires the mission node label. + Assert.NotNull(spec.Affinity) + Assert.NotNull(spec.Affinity.NodeAffinity) + + let term = + Seq.exactlyOne spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms + + let expr = Seq.exactlyOne term.MatchExpressions + Assert.Equal("purpose", expr.Key) + Assert.Equal("In", expr.OperatorProperty) + Assert.Equal("largetests", Seq.exactlyOne expr.Values) + + // Tolerates the mission node taint. + Assert.True( + spec.Tolerations + |> Seq.exists (fun t -> t.Key = "largetests" && t.OperatorProperty = "Exists") + ) + [] member __.``Core init commands look reasonable``() = let nCfgWithoutSimulateApply =