Skip to content

Commit d07c257

Browse files
🚀 Speculative agent autoscaling (#664)
* Fix issue with speculative runs not scaling agents * Add mocks and unit test * Remove unnecessary mock and fix logging * Update function docs * Add changelog * Remove unecessary interface * Update dependency * Update mock package to latest * Update changelog * Add more sample data to tests * Fix test name * Fix test name * Add test case * Update changelog
1 parent f4952ed commit d07c257

File tree

6 files changed

+188
-23
lines changed

6 files changed

+188
-23
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: ENHANCEMENTS
2+
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'
3+
time: 2025-11-20T09:36:00.142836-08:00
4+
custom:
5+
PR: "664"

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ test-unit: fmt vet copywrite ## Run internal/controller tests.
154154
-timeout 5m \
155155
-count 1 \
156156
-v \
157-
-run="^Test(DoNotRequeue|RequeueAfter|RequeueOnErr|FormatOutput|FinalizerBehaviors|MatchWildcardName|ValidateTFEVersion)$$"
157+
-run="^Test(DoNotRequeue|RequeueAfter|RequeueOnErr|FormatOutput|FinalizerBehaviors|MatchWildcardName|ValidateTFEVersion|PendingWorkspaceRuns)$$"
158158

159159
.PHONY: test-helm
160160
test-helm: ## Run Helm chart tests.

go.mod

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/onsi/gomega v1.36.3
1313
github.com/prometheus/client_golang v1.22.0
1414
github.com/stretchr/testify v1.11.1
15+
go.uber.org/mock v0.6.0
1516
go.uber.org/zap v1.27.0
1617
k8s.io/api v0.34.1
1718
k8s.io/apimachinery v0.34.1
@@ -58,14 +59,14 @@ require (
5859
go.uber.org/multierr v1.11.0 // indirect
5960
go.yaml.in/yaml/v2 v2.4.2 // indirect
6061
go.yaml.in/yaml/v3 v3.0.4 // indirect
61-
golang.org/x/net v0.38.0 // indirect
62+
golang.org/x/net v0.43.0 // indirect
6263
golang.org/x/oauth2 v0.28.0 // indirect
6364
golang.org/x/sync v0.17.0 // indirect
64-
golang.org/x/sys v0.32.0 // indirect
65-
golang.org/x/term v0.30.0 // indirect
66-
golang.org/x/text v0.23.0 // indirect
65+
golang.org/x/sys v0.35.0 // indirect
66+
golang.org/x/term v0.34.0 // indirect
67+
golang.org/x/text v0.28.0 // indirect
6768
golang.org/x/time v0.12.0 // indirect
68-
golang.org/x/tools v0.31.0 // indirect
69+
golang.org/x/tools v0.36.0 // indirect
6970
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
7071
google.golang.org/protobuf v1.36.5 // indirect
7172
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect

go.sum

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
140140
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
141141
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
142142
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
143+
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
144+
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
143145
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
144146
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
145147
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
@@ -157,8 +159,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
157159
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
158160
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
159161
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
160-
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
161-
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
162+
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
163+
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
162164
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
163165
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
164166
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -169,22 +171,22 @@ golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
169171
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
170172
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
171173
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
172-
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
173-
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
174-
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
175-
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
174+
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
175+
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
176+
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
177+
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
176178
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
177179
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
178-
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
179-
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
180+
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
181+
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
180182
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
181183
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
182184
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
183185
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
184186
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
185187
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
186-
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
187-
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
188+
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
189+
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
188190
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
189191
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
190192
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

internal/controller/agentpool_controller_autoscaling.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func matchWildcardName(wildcard string, str string) bool {
6161
}
6262
}
6363

64-
// pendingWorkspaceRuns returns the number of workspaces with pending runs for a given agent pool.
64+
// pendingWorkspaceRuns returns the number pending runs for a given agent pool.
6565
// This function is compatible with HCP Terraform and TFE version v202409-1 and later.
6666
func pendingWorkspaceRuns(ctx context.Context, ap *agentPoolInstance) (int32, error) {
6767
runs := map[string]struct{}{}
@@ -74,7 +74,7 @@ func pendingWorkspaceRuns(ctx context.Context, ap *agentPoolInstance) (int32, er
7474
PageNumber: initPageNumber,
7575
},
7676
}
77-
77+
planOnlyRunCount := 0
7878
for {
7979
runsList, err := ap.tfClient.Client.Runs.ListForOrganization(ctx, ap.instance.Spec.Organization, listOpts)
8080
if err != nil {
@@ -87,6 +87,11 @@ func pendingWorkspaceRuns(ctx context.Context, ap *agentPoolInstance) (int32, er
8787
awaitingUserInteractionRuns[string(run.Status)]++
8888
continue
8989
}
90+
// Count plan-only runs separately so agents can scale up and execute runs parallely
91+
if run.PlanOnly {
92+
planOnlyRunCount++
93+
continue
94+
}
9095
runs[run.Workspace.ID] = struct{}{}
9196
}
9297
if runsList.NextPage == 0 {
@@ -97,8 +102,9 @@ func pendingWorkspaceRuns(ctx context.Context, ap *agentPoolInstance) (int32, er
97102

98103
// TODO:
99104
// Add metric(s) for runs awaiting user interaction
100-
101-
return int32(len(runs)), nil
105+
totalPendingRuns := len(runs) + planOnlyRunCount
106+
ap.log.Info("Runs", "msg", fmt.Sprintf("Workspaces: %+v Plan-only runs: %d Total pending runs: %d", runs, planOnlyRunCount, totalPendingRuns))
107+
return int32(totalPendingRuns), nil
102108
}
103109

104110
// computeRequiredAgents is a legacy algorithm that is used to compute the number of agents needed.
@@ -266,7 +272,7 @@ func (r *AgentPoolReconciler) reconcileAgentAutoscaling(ctx context.Context, ap
266272
r.Recorder.Eventf(&ap.instance, corev1.EventTypeWarning, "AutoscaleAgentPoolDeployment", "Autoscaling failed: %v", err.Error())
267273
return err
268274
}
269-
ap.log.Info("Reconcile Agent Autoscaling", "msg", fmt.Sprintf("%d workspaces have pending runs", requiredAgents))
275+
ap.log.Info("Reconcile Agent Autoscaling", "msg", fmt.Sprintf("%d agents are required", requiredAgents))
270276

271277
currentReplicas, err := r.getAgentDeploymentReplicas(ctx, ap)
272278
if err != nil {

internal/controller/agentpool_controller_autoscaling_test.go

Lines changed: 153 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,25 @@
44
package controller
55

66
import (
7+
"context"
8+
"errors"
79
"fmt"
10+
"testing"
811
"time"
912

13+
"github.com/go-logr/logr"
1014
tfc "github.com/hashicorp/go-tfe"
15+
"github.com/hashicorp/go-tfe/mocks"
1116
appv1alpha2 "github.com/hashicorp/hcp-terraform-operator/api/v1alpha2"
1217
"github.com/hashicorp/hcp-terraform-operator/internal/pointer"
1318
. "github.com/onsi/ginkgo/v2"
1419
. "github.com/onsi/gomega"
20+
gomock "go.uber.org/mock/gomock"
1521
corev1 "k8s.io/api/core/v1"
16-
"k8s.io/apimachinery/pkg/api/errors"
22+
k8sapierrors "k8s.io/apimachinery/pkg/api/errors"
1723
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
25+
"github.com/stretchr/testify/assert"
1826
)
1927

2028
var _ = Describe("Agent Pool controller", Ordered, func() {
@@ -77,7 +85,7 @@ var _ = Describe("Agent Pool controller", Ordered, func() {
7785
Expect(k8sClient.Delete(ctx, instance)).To(Succeed())
7886
Eventually(func() bool {
7987
err := k8sClient.Get(ctx, namespacedName, instance)
80-
return errors.IsNotFound(err)
88+
return k8sapierrors.IsNotFound(err)
8189
}).Should(BeTrue())
8290
})
8391

@@ -194,3 +202,146 @@ var _ = Describe("Agent Pool controller", Ordered, func() {
194202
})
195203
})
196204
})
205+
206+
func TestPendingWorkspaceRuns(t *testing.T) {
207+
tests := []struct {
208+
name string
209+
mockRuns []*tfc.Run
210+
mockErr error
211+
expectedCount int32
212+
expectError bool
213+
}{
214+
{
215+
name: "returns error from client",
216+
mockErr: errors.New("api error"),
217+
expectedCount: 0,
218+
expectError: true,
219+
},
220+
{
221+
name: "counts plan-only runs",
222+
mockRuns: []*tfc.Run{
223+
{ID: "run1", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
224+
{ID: "run2", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}},
225+
},
226+
expectedCount: 2,
227+
expectError: false,
228+
},
229+
{
230+
name: "skips user interaction runs",
231+
mockRuns: []*tfc.Run{
232+
{ID: "run1", PlanOnly: false, Status: tfc.RunPlanned, Workspace: &tfc.Workspace{ID: "ws1"}},
233+
{ID: "run2", PlanOnly: false, Status: tfc.RunPolicyOverride, Workspace: &tfc.Workspace{ID: "ws2"}},
234+
},
235+
expectedCount: 0,
236+
expectError: false,
237+
},
238+
{
239+
name: "counts normal pending runs",
240+
mockRuns: []*tfc.Run{
241+
{ID: "run1", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
242+
{ID: "run2", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}},
243+
},
244+
expectedCount: 2,
245+
expectError: false,
246+
},
247+
{
248+
name: "mix of plan-only and normal runs",
249+
mockRuns: []*tfc.Run{
250+
{ID: "run1", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
251+
{ID: "run2", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}},
252+
},
253+
expectedCount: 2,
254+
expectError: false,
255+
},
256+
{
257+
name: "plan-only runs for single workspace",
258+
mockRuns: []*tfc.Run{
259+
{ID: "run1", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
260+
{ID: "run2", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
261+
{ID: "run3", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
262+
{ID: "run4", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
263+
},
264+
expectedCount: 4,
265+
expectError: false,
266+
},
267+
{
268+
name: "single apply and multiple plan-only runs for single workspace",
269+
mockRuns: []*tfc.Run{
270+
{ID: "run1", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
271+
{ID: "run2", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
272+
{ID: "run3", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
273+
{ID: "run4", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
274+
{ID: "run5", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
275+
},
276+
expectedCount: 5,
277+
expectError: false,
278+
},
279+
{
280+
name: "mix of plan-only and apply runs for single workspace",
281+
mockRuns: []*tfc.Run{
282+
{ID: "run1", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
283+
{ID: "run2", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
284+
{ID: "run3", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
285+
{ID: "run4", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
286+
{ID: "run5", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
287+
},
288+
expectedCount: 4,
289+
expectError: false,
290+
},
291+
{
292+
name: "mix of plan-only and apply runs for multiple workspaces",
293+
mockRuns: []*tfc.Run{
294+
{ID: "run1", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
295+
{ID: "run2", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}},
296+
{ID: "run3", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws3"}},
297+
{ID: "run4", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
298+
{ID: "run5", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
299+
},
300+
expectedCount: 5,
301+
expectError: false,
302+
},
303+
{
304+
name: "mix of plan-only and apply runs for two workspaces",
305+
mockRuns: []*tfc.Run{
306+
{ID: "run1", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws1"}},
307+
{ID: "run2", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}},
308+
{ID: "run3", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}},
309+
{ID: "run4", PlanOnly: true, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}},
310+
{ID: "run5", PlanOnly: false, Status: tfc.RunPlanning, Workspace: &tfc.Workspace{ID: "ws2"}},
311+
},
312+
expectedCount: 4,
313+
expectError: false,
314+
},
315+
}
316+
317+
for _, tt := range tests {
318+
t.Run(tt.name, func(t *testing.T) {
319+
ctrl := gomock.NewController(t)
320+
defer ctrl.Finish()
321+
322+
mockRuns := mocks.NewMockRuns(ctrl)
323+
mockRuns.EXPECT().
324+
ListForOrganization(gomock.Any(), "test-org", gomock.Any()).
325+
Return(&tfc.OrganizationRunList{Items: tt.mockRuns, PaginationNextPrev: &tfc.PaginationNextPrev{NextPage: 0}}, tt.mockErr)
326+
327+
ap := &agentPoolInstance{
328+
tfClient: HCPTerraformClient{Client: &tfc.Client{Runs: mockRuns}},
329+
instance: appv1alpha2.AgentPool{
330+
Spec: appv1alpha2.AgentPoolSpec{
331+
Name: "test-pool",
332+
Organization: "test-org",
333+
},
334+
},
335+
log: logr.Logger{},
336+
}
337+
338+
count, err := pendingWorkspaceRuns(context.Background(), ap)
339+
if tt.expectError {
340+
assert.Error(t, err)
341+
} else {
342+
assert.NoError(t, err)
343+
assert.Equal(t, tt.expectedCount, count)
344+
}
345+
})
346+
}
347+
}

0 commit comments

Comments
 (0)