From b71321d8cd75d049393146baa3a82e64a2292153 Mon Sep 17 00:00:00 2001 From: Shweta Deshpande Date: Mon, 3 Nov 2025 17:06:12 -0800 Subject: [PATCH 01/14] Fix issue with speculative runs not scaling agents --- .../controller/agentpool_controller_autoscaling.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/controller/agentpool_controller_autoscaling.go b/internal/controller/agentpool_controller_autoscaling.go index ec4252bc..a9918c4f 100644 --- a/internal/controller/agentpool_controller_autoscaling.go +++ b/internal/controller/agentpool_controller_autoscaling.go @@ -74,7 +74,7 @@ func pendingWorkspaceRuns(ctx context.Context, ap *agentPoolInstance) (int32, er PageNumber: initPageNumber, }, } - + runCount := 0 for { runsList, err := ap.tfClient.Client.Runs.ListForOrganization(ctx, ap.instance.Spec.Organization, listOpts) if err != nil { @@ -87,6 +87,10 @@ func pendingWorkspaceRuns(ctx context.Context, ap *agentPoolInstance) (int32, er awaitingUserInteractionRuns[string(run.Status)]++ continue } + if _, exists := runs[run.Workspace.ID]; exists && run.PlanOnly { + runCount++ + continue + } runs[run.Workspace.ID] = struct{}{} } if runsList.NextPage == 0 { @@ -97,8 +101,10 @@ func pendingWorkspaceRuns(ctx context.Context, ap *agentPoolInstance) (int32, er // TODO: // Add metric(s) for runs awaiting user interaction - - return int32(len(runs)), nil + ap.log.Info("Runs", "msg", fmt.Sprintf("Runs: %+v", runs)) + ap.log.Info("Run count", "msg", fmt.Sprintf("RunCount: %+v", runCount)) + runCount = len(runs) + runCount + return int32(runCount), nil } // computeRequiredAgents is a legacy algorithm that is used to compute the number of agents needed. From b4813af6da468ac347a7dc7deb64a19fb77a69b1 Mon Sep 17 00:00:00 2001 From: Shweta Deshpande Date: Wed, 12 Nov 2025 11:51:21 -0800 Subject: [PATCH 02/14] Add mocks and unit test --- go.mod | 2 + go.sum | 13 +++ .../agentpool_controller_autoscaling.go | 32 ++++--- .../agentpool_controller_autoscaling_test.go | 96 ++++++++++++++++++- .../agentpool_controller_autoscaling_mocks.go | 50 ++++++++++ 5 files changed, 178 insertions(+), 15 deletions(-) create mode 100644 mocks/agentpool_controller_autoscaling_mocks.go diff --git a/go.mod b/go.mod index eee656e4..fe2e7fa1 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.4 require ( github.com/go-logr/logr v1.4.3 github.com/go-logr/zapr v1.3.0 + github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.7.0 github.com/hashicorp/go-slug v0.16.7 github.com/hashicorp/go-tfe v1.93.0 @@ -12,6 +13,7 @@ require ( github.com/onsi/gomega v1.36.3 github.com/prometheus/client_golang v1.22.0 github.com/stretchr/testify v1.11.1 + go.uber.org/mock v0.4.0 go.uber.org/zap v1.27.0 k8s.io/api v0.34.1 k8s.io/apimachinery v0.34.1 diff --git a/go.sum b/go.sum index f4503b82..b910ab54 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= @@ -136,10 +138,13 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= @@ -153,10 +158,12 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= @@ -164,13 +171,18 @@ golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -183,6 +195,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/controller/agentpool_controller_autoscaling.go b/internal/controller/agentpool_controller_autoscaling.go index a9918c4f..db2a004e 100644 --- a/internal/controller/agentpool_controller_autoscaling.go +++ b/internal/controller/agentpool_controller_autoscaling.go @@ -18,6 +18,10 @@ import ( appv1alpha2 "github.com/hashicorp/hcp-terraform-operator/api/v1alpha2" ) +type AgentPoolControllerAutoscaling interface { + pendingWorkspaceRuns(ctx context.Context, ap *agentPoolInstance) (int32, error) +} + // userInteractionRunStatuses contains run statuses that require user interaction. var userInteractionRunStatuses = map[tfc.RunStatus]struct{}{ tfc.RunCostEstimated: {}, @@ -61,9 +65,10 @@ func matchWildcardName(wildcard string, str string) bool { } } -// pendingWorkspaceRuns returns the number of workspaces with pending runs for a given agent pool. +// pendingWorkspaceRuns returns the number of agents needed to execute current pending runs for a given agent pool. +// If there are no plan-only runs in the list of current pending runs for a workspace this functoion returns the number of workspaces. // This function is compatible with HCP Terraform and TFE version v202409-1 and later. -func pendingWorkspaceRuns(ctx context.Context, ap *agentPoolInstance) (int32, error) { +func (ap *agentPoolInstance) pendingWorkspaceRuns(ctx context.Context) (int32, error) { runs := map[string]struct{}{} awaitingUserInteractionRuns := map[string]int{} // Track runs awaiting user interaction by status for future metrics listOpts := &tfc.RunListForOrganizationOptions{ @@ -74,8 +79,9 @@ func pendingWorkspaceRuns(ctx context.Context, ap *agentPoolInstance) (int32, er PageNumber: initPageNumber, }, } - runCount := 0 + planOnlyRunCount := 0 for { + ap.log.Info("Fetching runs for organization", "org", ap.instance.Spec.Organization, "page", listOpts.PageNumber) runsList, err := ap.tfClient.Client.Runs.ListForOrganization(ctx, ap.instance.Spec.Organization, listOpts) if err != nil { return 0, err @@ -87,8 +93,9 @@ func pendingWorkspaceRuns(ctx context.Context, ap *agentPoolInstance) (int32, er awaitingUserInteractionRuns[string(run.Status)]++ continue } - if _, exists := runs[run.Workspace.ID]; exists && run.PlanOnly { - runCount++ + // Count plan-only runs separately so agents can scale up and execute runs parallely + if run.PlanOnly { + planOnlyRunCount++ continue } runs[run.Workspace.ID] = struct{}{} @@ -101,15 +108,14 @@ func pendingWorkspaceRuns(ctx context.Context, ap *agentPoolInstance) (int32, er // TODO: // Add metric(s) for runs awaiting user interaction - ap.log.Info("Runs", "msg", fmt.Sprintf("Runs: %+v", runs)) - ap.log.Info("Run count", "msg", fmt.Sprintf("RunCount: %+v", runCount)) - runCount = len(runs) + runCount - return int32(runCount), nil + agentsCount := len(runs) + planOnlyRunCount + ap.log.Info("Workspaces and plan-only runs count", "msg", fmt.Sprintf("Workspaces: %+v Plan-only runs: %d Total agents: %d", runs, planOnlyRunCount, agentsCount)) + return int32(agentsCount), nil } // computeRequiredAgents is a legacy algorithm that is used to compute the number of agents needed. // It is used when the TFE version is less than v202409-1. -func computeRequiredAgents(ctx context.Context, ap *agentPoolInstance) (int32, error) { +func (ap *agentPoolInstance) computeRequiredAgents(ctx context.Context) (int32, error) { required := 0 // NOTE: // - Two maps are used here to simplify target workspace searching by ID, name, and wildcard. @@ -248,7 +254,7 @@ func (r *AgentPoolReconciler) reconcileAgentAutoscaling(ctx context.Context, ap requiredAgents, err := func() (int32, error) { if ap.tfClient.Client.IsCloud() { - return pendingWorkspaceRuns(ctx, ap) + return ap.pendingWorkspaceRuns(ctx) } tfeVersion := ap.tfClient.Client.RemoteTFEVersion() useRunsEndpoint, err := validateTFEVersion(tfeVersion) @@ -262,10 +268,10 @@ func (r *AgentPoolReconciler) reconcileAgentAutoscaling(ctx context.Context, ap // It now allows retrieving a list of runs for the organization. if useRunsEndpoint { ap.log.Info("Reconcile Agent Autoscaling", "msg", fmt.Sprintf("Proceeding with the new algorithm based on the detected TFE version %s", tfeVersion)) - return pendingWorkspaceRuns(ctx, ap) + return ap.pendingWorkspaceRuns(ctx) } ap.log.Info("Reconcile Agent Autoscaling", "msg", fmt.Sprintf("Proceeding with the legacy algorithm based to the detected TFE version %s", tfeVersion)) - return computeRequiredAgents(ctx, ap) + return ap.computeRequiredAgents(ctx) }() if err != nil { ap.log.Error(err, "Reconcile Agent Autoscaling", "msg", "Failed to get agents needed") diff --git a/internal/controller/agentpool_controller_autoscaling_test.go b/internal/controller/agentpool_controller_autoscaling_test.go index 2bbf5ee7..0139448a 100644 --- a/internal/controller/agentpool_controller_autoscaling_test.go +++ b/internal/controller/agentpool_controller_autoscaling_test.go @@ -4,17 +4,25 @@ package controller import ( + "context" + "errors" "fmt" + "testing" "time" + "github.com/go-logr/logr" tfc "github.com/hashicorp/go-tfe" + "github.com/hashicorp/go-tfe/mocks" appv1alpha2 "github.com/hashicorp/hcp-terraform-operator/api/v1alpha2" "github.com/hashicorp/hcp-terraform-operator/internal/pointer" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + gomock "go.uber.org/mock/gomock" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" + k8sapierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/stretchr/testify/assert" ) var _ = Describe("Agent Pool controller", Ordered, func() { @@ -77,7 +85,7 @@ var _ = Describe("Agent Pool controller", Ordered, func() { Expect(k8sClient.Delete(ctx, instance)).To(Succeed()) Eventually(func() bool { err := k8sClient.Get(ctx, namespacedName, instance) - return errors.IsNotFound(err) + return k8sapierrors.IsNotFound(err) }).Should(BeTrue()) }) @@ -194,3 +202,87 @@ var _ = Describe("Agent Pool controller", Ordered, func() { }) }) }) + +func TestPendingWorkspaceRuns(t *testing.T) { + tests := []struct { + name string + mockRuns []*tfc.Run + mockErr error + expectedCount int32 + expectError bool + }{ + { + name: "returns error from client", + mockErr: errors.New("api error"), + expectedCount: 0, + expectError: true, + }, + { + name: "counts plan-only runs", + mockRuns: []*tfc.Run{ + {ID: "run1", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run2", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}}, + }, + expectedCount: 2, + expectError: false, + }, + { + name: "skips user interaction runs", + mockRuns: []*tfc.Run{ + {ID: "run1", PlanOnly: false, Status: tfc.RunPlanned, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run2", PlanOnly: false, Status: tfc.RunPolicyOverride, Workspace: &tfc.Workspace{ID: "ws2"}}, + }, + expectedCount: 0, + expectError: false, + }, + { + name: "counts normal pending runs", + mockRuns: []*tfc.Run{ + {ID: "run1", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run2", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}}, + }, + expectedCount: 2, + expectError: false, + }, + { + name: "mix of plan-only and normal runs", + mockRuns: []*tfc.Run{ + {ID: "run1", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run2", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}}, + }, + expectedCount: 2, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockRuns := mocks.NewMockRuns(ctrl) + mockRuns.EXPECT(). + ListForOrganization(gomock.Any(), "test-org", gomock.Any()). + Return(&tfc.RunList{Items: tt.mockRuns, Pagination: &tfc.Pagination{NextPage: 0}}, tt.mockErr) + + ap := &agentPoolInstance{ + tfClient: HCPTerraformClient{Client: &tfc.Client{Runs: mockRuns}}, + instance: appv1alpha2.AgentPool{ + Spec: appv1alpha2.AgentPoolSpec{ + Name: "test-pool", + Organization: "test-org", + }, + }, + log: logr.Logger{}, + } + + count, err := ap.pendingWorkspaceRuns(context.Background()) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedCount, count) + } + }) + } +} diff --git a/mocks/agentpool_controller_autoscaling_mocks.go b/mocks/agentpool_controller_autoscaling_mocks.go new file mode 100644 index 00000000..899afdf2 --- /dev/null +++ b/mocks/agentpool_controller_autoscaling_mocks.go @@ -0,0 +1,50 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./internal/controller/agentpool_controller_autoscaling.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockAgentPoolControllerAutoscaling is a mock of AgentPoolControllerAutoscaling interface. +type MockAgentPoolControllerAutoscaling struct { + ctrl *gomock.Controller + recorder *MockAgentPoolControllerAutoscalingMockRecorder +} + +// MockAgentPoolControllerAutoscalingMockRecorder is the mock recorder for MockAgentPoolControllerAutoscaling. +type MockAgentPoolControllerAutoscalingMockRecorder struct { + mock *MockAgentPoolControllerAutoscaling +} + +// NewMockAgentPoolControllerAutoscaling creates a new mock instance. +func NewMockAgentPoolControllerAutoscaling(ctrl *gomock.Controller) *MockAgentPoolControllerAutoscaling { + mock := &MockAgentPoolControllerAutoscaling{ctrl: ctrl} + mock.recorder = &MockAgentPoolControllerAutoscalingMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAgentPoolControllerAutoscaling) EXPECT() *MockAgentPoolControllerAutoscalingMockRecorder { + return m.recorder +} + +// pendingWorkspaceRuns mocks base method. +func (m *MockAgentPoolControllerAutoscaling) pendingWorkspaceRuns(ctx context.Context) (int32, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "pendingWorkspaceRuns", ctx) + ret0, _ := ret[0].(int32) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// pendingWorkspaceRuns indicates an expected call of pendingWorkspaceRuns. +func (mr *MockAgentPoolControllerAutoscalingMockRecorder) pendingWorkspaceRuns(ctx, ap interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "pendingWorkspaceRuns", reflect.TypeOf((*MockAgentPoolControllerAutoscaling)(nil).pendingWorkspaceRuns), ctx, ap) +} From 3d54ae3708987cd30a04b26c0793412afcecca03 Mon Sep 17 00:00:00 2001 From: Shweta Deshpande Date: Mon, 17 Nov 2025 11:57:44 -0800 Subject: [PATCH 03/14] Remove unnecessary mock and fix logging --- .../agentpool_controller_autoscaling.go | 9 ++-- .../agentpool_controller_autoscaling_test.go | 2 +- .../agentpool_controller_autoscaling_mocks.go | 50 ------------------- 3 files changed, 5 insertions(+), 56 deletions(-) delete mode 100644 mocks/agentpool_controller_autoscaling_mocks.go diff --git a/internal/controller/agentpool_controller_autoscaling.go b/internal/controller/agentpool_controller_autoscaling.go index db2a004e..97eba023 100644 --- a/internal/controller/agentpool_controller_autoscaling.go +++ b/internal/controller/agentpool_controller_autoscaling.go @@ -81,7 +81,6 @@ func (ap *agentPoolInstance) pendingWorkspaceRuns(ctx context.Context) (int32, e } planOnlyRunCount := 0 for { - ap.log.Info("Fetching runs for organization", "org", ap.instance.Spec.Organization, "page", listOpts.PageNumber) runsList, err := ap.tfClient.Client.Runs.ListForOrganization(ctx, ap.instance.Spec.Organization, listOpts) if err != nil { return 0, err @@ -108,9 +107,9 @@ func (ap *agentPoolInstance) pendingWorkspaceRuns(ctx context.Context) (int32, e // TODO: // Add metric(s) for runs awaiting user interaction - agentsCount := len(runs) + planOnlyRunCount - ap.log.Info("Workspaces and plan-only runs count", "msg", fmt.Sprintf("Workspaces: %+v Plan-only runs: %d Total agents: %d", runs, planOnlyRunCount, agentsCount)) - return int32(agentsCount), nil + totalPendingRuns := len(runs) + planOnlyRunCount + ap.log.Info("Runs", "msg", fmt.Sprintf("Workspaces: %+v Plan-only runs: %d Total pending runs: %d", runs, planOnlyRunCount, totalPendingRuns)) + return int32(totalPendingRuns), nil } // computeRequiredAgents is a legacy algorithm that is used to compute the number of agents needed. @@ -278,7 +277,7 @@ func (r *AgentPoolReconciler) reconcileAgentAutoscaling(ctx context.Context, ap r.Recorder.Eventf(&ap.instance, corev1.EventTypeWarning, "AutoscaleAgentPoolDeployment", "Autoscaling failed: %v", err.Error()) return err } - ap.log.Info("Reconcile Agent Autoscaling", "msg", fmt.Sprintf("%d workspaces have pending runs", requiredAgents)) + ap.log.Info("Reconcile Agent Autoscaling", "msg", fmt.Sprintf("%d agents are required", requiredAgents)) currentReplicas, err := r.getAgentDeploymentReplicas(ctx, ap) if err != nil { diff --git a/internal/controller/agentpool_controller_autoscaling_test.go b/internal/controller/agentpool_controller_autoscaling_test.go index 0139448a..9249069c 100644 --- a/internal/controller/agentpool_controller_autoscaling_test.go +++ b/internal/controller/agentpool_controller_autoscaling_test.go @@ -263,7 +263,7 @@ func TestPendingWorkspaceRuns(t *testing.T) { mockRuns := mocks.NewMockRuns(ctrl) mockRuns.EXPECT(). ListForOrganization(gomock.Any(), "test-org", gomock.Any()). - Return(&tfc.RunList{Items: tt.mockRuns, Pagination: &tfc.Pagination{NextPage: 0}}, tt.mockErr) + Return(&tfc.OrganizationRunList{Items: tt.mockRuns, PaginationNextPrev: &tfc.PaginationNextPrev{NextPage: 0}}, tt.mockErr) ap := &agentPoolInstance{ tfClient: HCPTerraformClient{Client: &tfc.Client{Runs: mockRuns}}, diff --git a/mocks/agentpool_controller_autoscaling_mocks.go b/mocks/agentpool_controller_autoscaling_mocks.go deleted file mode 100644 index 899afdf2..00000000 --- a/mocks/agentpool_controller_autoscaling_mocks.go +++ /dev/null @@ -1,50 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: ./internal/controller/agentpool_controller_autoscaling.go - -// Package mocks is a generated GoMock package. -package mocks - -import ( - context "context" - reflect "reflect" - - gomock "github.com/golang/mock/gomock" -) - -// MockAgentPoolControllerAutoscaling is a mock of AgentPoolControllerAutoscaling interface. -type MockAgentPoolControllerAutoscaling struct { - ctrl *gomock.Controller - recorder *MockAgentPoolControllerAutoscalingMockRecorder -} - -// MockAgentPoolControllerAutoscalingMockRecorder is the mock recorder for MockAgentPoolControllerAutoscaling. -type MockAgentPoolControllerAutoscalingMockRecorder struct { - mock *MockAgentPoolControllerAutoscaling -} - -// NewMockAgentPoolControllerAutoscaling creates a new mock instance. -func NewMockAgentPoolControllerAutoscaling(ctrl *gomock.Controller) *MockAgentPoolControllerAutoscaling { - mock := &MockAgentPoolControllerAutoscaling{ctrl: ctrl} - mock.recorder = &MockAgentPoolControllerAutoscalingMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockAgentPoolControllerAutoscaling) EXPECT() *MockAgentPoolControllerAutoscalingMockRecorder { - return m.recorder -} - -// pendingWorkspaceRuns mocks base method. -func (m *MockAgentPoolControllerAutoscaling) pendingWorkspaceRuns(ctx context.Context) (int32, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "pendingWorkspaceRuns", ctx) - ret0, _ := ret[0].(int32) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// pendingWorkspaceRuns indicates an expected call of pendingWorkspaceRuns. -func (mr *MockAgentPoolControllerAutoscalingMockRecorder) pendingWorkspaceRuns(ctx, ap interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "pendingWorkspaceRuns", reflect.TypeOf((*MockAgentPoolControllerAutoscaling)(nil).pendingWorkspaceRuns), ctx, ap) -} From 7277ec40c94de23e7c9ebe168e843ab92bef5fbe Mon Sep 17 00:00:00 2001 From: Shweta Deshpande Date: Mon, 17 Nov 2025 12:11:46 -0800 Subject: [PATCH 04/14] Update function docs --- internal/controller/agentpool_controller_autoscaling.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/controller/agentpool_controller_autoscaling.go b/internal/controller/agentpool_controller_autoscaling.go index 97eba023..607c70c5 100644 --- a/internal/controller/agentpool_controller_autoscaling.go +++ b/internal/controller/agentpool_controller_autoscaling.go @@ -65,8 +65,7 @@ func matchWildcardName(wildcard string, str string) bool { } } -// pendingWorkspaceRuns returns the number of agents needed to execute current pending runs for a given agent pool. -// If there are no plan-only runs in the list of current pending runs for a workspace this functoion returns the number of workspaces. +// pendingWorkspaceRuns returns the number pending runs for a given agent pool. // This function is compatible with HCP Terraform and TFE version v202409-1 and later. func (ap *agentPoolInstance) pendingWorkspaceRuns(ctx context.Context) (int32, error) { runs := map[string]struct{}{} From b851d24b7f385e4fd583b39986cd610988054b72 Mon Sep 17 00:00:00 2001 From: Shweta Deshpande Date: Tue, 18 Nov 2025 11:19:48 -0800 Subject: [PATCH 05/14] Add changelog --- .changes/unreleased/ENHANCEMENTS-664-20251118-111910.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/unreleased/ENHANCEMENTS-664-20251118-111910.yaml diff --git a/.changes/unreleased/ENHANCEMENTS-664-20251118-111910.yaml b/.changes/unreleased/ENHANCEMENTS-664-20251118-111910.yaml new file mode 100644 index 00000000..d6479c9b --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-664-20251118-111910.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: This change provisions autoscaling of agents for speculative plan-only runs. The number of agents is determined by adding plan-only runs and workspace count +time: 2025-11-18T11:19:10.674727-08:00 +custom: + PR: "664" From 517cbcf4c62855ff3e007b58b61dd0fa379b9cc6 Mon Sep 17 00:00:00 2001 From: Shweta Deshpande Date: Wed, 19 Nov 2025 10:07:12 -0800 Subject: [PATCH 06/14] Remove unecessary interface --- .../controller/agentpool_controller_autoscaling.go | 14 +++++--------- .../agentpool_controller_autoscaling_test.go | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/internal/controller/agentpool_controller_autoscaling.go b/internal/controller/agentpool_controller_autoscaling.go index 607c70c5..7d8226cf 100644 --- a/internal/controller/agentpool_controller_autoscaling.go +++ b/internal/controller/agentpool_controller_autoscaling.go @@ -18,10 +18,6 @@ import ( appv1alpha2 "github.com/hashicorp/hcp-terraform-operator/api/v1alpha2" ) -type AgentPoolControllerAutoscaling interface { - pendingWorkspaceRuns(ctx context.Context, ap *agentPoolInstance) (int32, error) -} - // userInteractionRunStatuses contains run statuses that require user interaction. var userInteractionRunStatuses = map[tfc.RunStatus]struct{}{ tfc.RunCostEstimated: {}, @@ -67,7 +63,7 @@ func matchWildcardName(wildcard string, str string) bool { // pendingWorkspaceRuns returns the number pending runs for a given agent pool. // This function is compatible with HCP Terraform and TFE version v202409-1 and later. -func (ap *agentPoolInstance) pendingWorkspaceRuns(ctx context.Context) (int32, error) { +func pendingWorkspaceRuns(ctx context.Context, ap *agentPoolInstance) (int32, error) { runs := map[string]struct{}{} awaitingUserInteractionRuns := map[string]int{} // Track runs awaiting user interaction by status for future metrics listOpts := &tfc.RunListForOrganizationOptions{ @@ -113,7 +109,7 @@ func (ap *agentPoolInstance) pendingWorkspaceRuns(ctx context.Context) (int32, e // computeRequiredAgents is a legacy algorithm that is used to compute the number of agents needed. // It is used when the TFE version is less than v202409-1. -func (ap *agentPoolInstance) computeRequiredAgents(ctx context.Context) (int32, error) { +func computeRequiredAgents(ctx context.Context, ap *agentPoolInstance) (int32, error) { required := 0 // NOTE: // - Two maps are used here to simplify target workspace searching by ID, name, and wildcard. @@ -252,7 +248,7 @@ func (r *AgentPoolReconciler) reconcileAgentAutoscaling(ctx context.Context, ap requiredAgents, err := func() (int32, error) { if ap.tfClient.Client.IsCloud() { - return ap.pendingWorkspaceRuns(ctx) + return pendingWorkspaceRuns(ctx, ap) } tfeVersion := ap.tfClient.Client.RemoteTFEVersion() useRunsEndpoint, err := validateTFEVersion(tfeVersion) @@ -266,10 +262,10 @@ func (r *AgentPoolReconciler) reconcileAgentAutoscaling(ctx context.Context, ap // It now allows retrieving a list of runs for the organization. if useRunsEndpoint { ap.log.Info("Reconcile Agent Autoscaling", "msg", fmt.Sprintf("Proceeding with the new algorithm based on the detected TFE version %s", tfeVersion)) - return ap.pendingWorkspaceRuns(ctx) + return pendingWorkspaceRuns(ctx, ap) } ap.log.Info("Reconcile Agent Autoscaling", "msg", fmt.Sprintf("Proceeding with the legacy algorithm based to the detected TFE version %s", tfeVersion)) - return ap.computeRequiredAgents(ctx) + return computeRequiredAgents(ctx, ap) }() if err != nil { ap.log.Error(err, "Reconcile Agent Autoscaling", "msg", "Failed to get agents needed") diff --git a/internal/controller/agentpool_controller_autoscaling_test.go b/internal/controller/agentpool_controller_autoscaling_test.go index 9249069c..59a4deff 100644 --- a/internal/controller/agentpool_controller_autoscaling_test.go +++ b/internal/controller/agentpool_controller_autoscaling_test.go @@ -276,7 +276,7 @@ func TestPendingWorkspaceRuns(t *testing.T) { log: logr.Logger{}, } - count, err := ap.pendingWorkspaceRuns(context.Background()) + count, err := pendingWorkspaceRuns(context.Background(), ap) if tt.expectError { assert.Error(t, err) } else { From 455b59d1de96355ac17b2f9be198903b68b0e460 Mon Sep 17 00:00:00 2001 From: Shweta Deshpande Date: Wed, 19 Nov 2025 11:36:43 -0800 Subject: [PATCH 07/14] Update dependency --- go.mod | 1 - go.sum | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/go.mod b/go.mod index fe2e7fa1..33436431 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.25.4 require ( github.com/go-logr/logr v1.4.3 github.com/go-logr/zapr v1.3.0 - github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.7.0 github.com/hashicorp/go-slug v0.16.7 github.com/hashicorp/go-tfe v1.93.0 diff --git a/go.sum b/go.sum index b910ab54..3e5dc6b7 100644 --- a/go.sum +++ b/go.sum @@ -35,8 +35,6 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= @@ -138,7 +136,6 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -158,12 +155,10 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= @@ -171,18 +166,13 @@ golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -195,7 +185,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 7091ef0d88f02bbedd21a35e15c2e28db5178de9 Mon Sep 17 00:00:00 2001 From: Shweta Deshpande Date: Wed, 19 Nov 2025 14:31:21 -0800 Subject: [PATCH 08/14] Update mock package to latest --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 33436431..4af45cab 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/onsi/gomega v1.36.3 github.com/prometheus/client_golang v1.22.0 github.com/stretchr/testify v1.11.1 - go.uber.org/mock v0.4.0 + go.uber.org/mock v0.6.0 go.uber.org/zap v1.27.0 k8s.io/api v0.34.1 k8s.io/apimachinery v0.34.1 @@ -59,14 +59,14 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.38.0 // indirect + golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.28.0 // indirect golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/text v0.28.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/tools v0.36.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect diff --git a/go.sum b/go.sum index 3e5dc6b7..df9129cd 100644 --- a/go.sum +++ b/go.sum @@ -140,8 +140,8 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= @@ -159,8 +159,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -171,22 +171,22 @@ golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 5f1b1b80e97bb9983b5d5a8e0991f22ad82055d5 Mon Sep 17 00:00:00 2001 From: Shweta Deshpande Date: Wed, 19 Nov 2025 14:35:35 -0800 Subject: [PATCH 09/14] Update changelog --- .changes/unreleased/DEPENDENCIES-664-20251119-143451.yaml | 5 +++++ .changes/unreleased/ENHANCEMENTS-664-20251118-111910.yaml | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 .changes/unreleased/DEPENDENCIES-664-20251119-143451.yaml delete mode 100644 .changes/unreleased/ENHANCEMENTS-664-20251118-111910.yaml diff --git a/.changes/unreleased/DEPENDENCIES-664-20251119-143451.yaml b/.changes/unreleased/DEPENDENCIES-664-20251119-143451.yaml new file mode 100644 index 00000000..99dd1268 --- /dev/null +++ b/.changes/unreleased/DEPENDENCIES-664-20251119-143451.yaml @@ -0,0 +1,5 @@ +kind: DEPENDENCIES +body: 'AgentPool: This change provisions autoscaling of agents for speculative plan-only runs. The number of agents is determined by adding plan-only runs and workspace count' +time: 2025-11-19T14:34:51.090834-08:00 +custom: + PR: "664" diff --git a/.changes/unreleased/ENHANCEMENTS-664-20251118-111910.yaml b/.changes/unreleased/ENHANCEMENTS-664-20251118-111910.yaml deleted file mode 100644 index d6479c9b..00000000 --- a/.changes/unreleased/ENHANCEMENTS-664-20251118-111910.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: ENHANCEMENTS -body: This change provisions autoscaling of agents for speculative plan-only runs. The number of agents is determined by adding plan-only runs and workspace count -time: 2025-11-18T11:19:10.674727-08:00 -custom: - PR: "664" From a722a516b77d36b7fb289ede4d0619a45fbbebaf Mon Sep 17 00:00:00 2001 From: Shweta Deshpande Date: Wed, 19 Nov 2025 14:44:20 -0800 Subject: [PATCH 10/14] Add more sample data to tests --- Makefile | 2 +- .../agentpool_controller_autoscaling_test.go | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index dd6e5294..d6ce2361 100644 --- a/Makefile +++ b/Makefile @@ -154,7 +154,7 @@ test-unit: fmt vet copywrite ## Run internal/controller tests. -timeout 5m \ -count 1 \ -v \ - -run="^Test(DoNotRequeue|RequeueAfter|RequeueOnErr|FormatOutput|FinalizerBehaviors|MatchWildcardName|ValidateTFEVersion)$$" + -run="^Test(DoNotRequeue|RequeueAfter|RequeueOnErr|FormatOutput|FinalizerBehaviors|MatchWildcardName|ValidateTFEVersion|PendingWorkspaceRuns)$$" .PHONY: test-helm test-helm: ## Run Helm chart tests. diff --git a/internal/controller/agentpool_controller_autoscaling_test.go b/internal/controller/agentpool_controller_autoscaling_test.go index 59a4deff..23528e59 100644 --- a/internal/controller/agentpool_controller_autoscaling_test.go +++ b/internal/controller/agentpool_controller_autoscaling_test.go @@ -253,6 +253,53 @@ func TestPendingWorkspaceRuns(t *testing.T) { expectedCount: 2, expectError: false, }, + { + name: "plan-only runs for single workspace", + mockRuns: []*tfc.Run{ + {ID: "run1", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run2", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run3", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run4", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + }, + expectedCount: 4, + expectError: false, + }, + { + name: "plan-only and apply runs for single workspace", + mockRuns: []*tfc.Run{ + {ID: "run1", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run2", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run3", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run4", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run5", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + }, + expectedCount: 5, + expectError: false, + }, + { + name: "mix of plan-only and apply runs for single workspace", + mockRuns: []*tfc.Run{ + {ID: "run1", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run2", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run3", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run4", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run5", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + }, + expectedCount: 4, + expectError: false, + }, + { + name: "mix of plan-only and apply runs for single workspace", + mockRuns: []*tfc.Run{ + {ID: "run1", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run2", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}}, + {ID: "run3", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws3"}}, + {ID: "run4", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run5", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + }, + expectedCount: 5, + expectError: false, + }, } for _, tt := range tests { From 8ffdbec5122a9e864454335da8fe813ebf6a0784 Mon Sep 17 00:00:00 2001 From: Shweta Deshpande Date: Wed, 19 Nov 2025 15:50:27 -0800 Subject: [PATCH 11/14] Fix test name --- internal/controller/agentpool_controller_autoscaling_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/agentpool_controller_autoscaling_test.go b/internal/controller/agentpool_controller_autoscaling_test.go index 23528e59..fba61f95 100644 --- a/internal/controller/agentpool_controller_autoscaling_test.go +++ b/internal/controller/agentpool_controller_autoscaling_test.go @@ -277,7 +277,7 @@ func TestPendingWorkspaceRuns(t *testing.T) { expectError: false, }, { - name: "mix of plan-only and apply runs for single workspace", + name: "mix of plan-only and apply runs for multiple workspaces", mockRuns: []*tfc.Run{ {ID: "run1", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, {ID: "run2", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, From 20e29031ba8602e658719cf342f46cbfe831635c Mon Sep 17 00:00:00 2001 From: Shweta Deshpande Date: Wed, 19 Nov 2025 15:53:20 -0800 Subject: [PATCH 12/14] Fix test name --- .../controller/agentpool_controller_autoscaling_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/controller/agentpool_controller_autoscaling_test.go b/internal/controller/agentpool_controller_autoscaling_test.go index fba61f95..2fd2b143 100644 --- a/internal/controller/agentpool_controller_autoscaling_test.go +++ b/internal/controller/agentpool_controller_autoscaling_test.go @@ -265,7 +265,7 @@ func TestPendingWorkspaceRuns(t *testing.T) { expectError: false, }, { - name: "plan-only and apply runs for single workspace", + name: "single apply and multiple plan-only runs for single workspace", mockRuns: []*tfc.Run{ {ID: "run1", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, {ID: "run2", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, @@ -277,7 +277,7 @@ func TestPendingWorkspaceRuns(t *testing.T) { expectError: false, }, { - name: "mix of plan-only and apply runs for multiple workspaces", + name: "mix of plan-only and apply runs for single workspace", mockRuns: []*tfc.Run{ {ID: "run1", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, {ID: "run2", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, @@ -289,7 +289,7 @@ func TestPendingWorkspaceRuns(t *testing.T) { expectError: false, }, { - name: "mix of plan-only and apply runs for single workspace", + name: "mix of plan-only and apply runs for multiple workspaces", mockRuns: []*tfc.Run{ {ID: "run1", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, {ID: "run2", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}}, From 71aa03be1c274eace4f44b51dabe7cf07244f286 Mon Sep 17 00:00:00 2001 From: Shweta Deshpande Date: Wed, 19 Nov 2025 15:56:11 -0800 Subject: [PATCH 13/14] Add test case --- .../agentpool_controller_autoscaling_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/controller/agentpool_controller_autoscaling_test.go b/internal/controller/agentpool_controller_autoscaling_test.go index 2fd2b143..10ca62a8 100644 --- a/internal/controller/agentpool_controller_autoscaling_test.go +++ b/internal/controller/agentpool_controller_autoscaling_test.go @@ -300,6 +300,18 @@ func TestPendingWorkspaceRuns(t *testing.T) { expectedCount: 5, expectError: false, }, + { + name: "mix of plan-only and apply runs for two workspaces", + mockRuns: []*tfc.Run{ + {ID: "run1", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}}, + {ID: "run2", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}}, + {ID: "run3", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}}, + {ID: "run4", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}}, + {ID: "run5", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}}, + }, + expectedCount: 4, + expectError: false, + }, } for _, tt := range tests { From 98839d087a84c1de6e40772fb45fc639a016075b Mon Sep 17 00:00:00 2001 From: Shweta Deshpande Date: Thu, 20 Nov 2025 10:21:10 -0800 Subject: [PATCH 14/14] Update changelog --- ...1119-143451.yaml => ENHANCEMENTS-664-20251120-093600.yaml} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename .changes/unreleased/{DEPENDENCIES-664-20251119-143451.yaml => ENHANCEMENTS-664-20251120-093600.yaml} (77%) diff --git a/.changes/unreleased/DEPENDENCIES-664-20251119-143451.yaml b/.changes/unreleased/ENHANCEMENTS-664-20251120-093600.yaml similarity index 77% rename from .changes/unreleased/DEPENDENCIES-664-20251119-143451.yaml rename to .changes/unreleased/ENHANCEMENTS-664-20251120-093600.yaml index 99dd1268..e23655ef 100644 --- a/.changes/unreleased/DEPENDENCIES-664-20251119-143451.yaml +++ b/.changes/unreleased/ENHANCEMENTS-664-20251120-093600.yaml @@ -1,5 +1,5 @@ -kind: DEPENDENCIES +kind: ENHANCEMENTS body: 'AgentPool: This change provisions autoscaling of agents for speculative plan-only runs. The number of agents is determined by adding plan-only runs and workspace count' -time: 2025-11-19T14:34:51.090834-08:00 +time: 2025-11-20T09:36:00.142836-08:00 custom: PR: "664"