Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/unreleased/FEATURES-622-20250717-145101.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: FEATURES
body: Added a new field `allowedWorkspaces` to `AgentPool` to allow to set scope workspace at the Agent Pools level
time: 2025-07-17T14:51:01.914242+09:00
custom:
PR: "622"
5 changes: 5 additions & 0 deletions api/v1alpha2/agentpool_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ type AgentPoolSpec struct {
//+optional
AgentDeploymentAutoscaling *AgentDeploymentAutoscaling `json:"autoscaling,omitempty"`

//AllowedWorkspaces is a list of HCP Terraform Workspaces ID which
// are scoped to this agent pool.
//+optional
AllowedWorkspaces *[]string `json:"allowedWorkspaces,omitempty"`

// The Deletion Policy specifies the behavior of the custom resource and its associated agent pool when the custom resource is deleted.
// - `retain`: When you delete the custom resource, the operator will remove only the custom resource.
// The HCP Terraform agent pool will be retained. The managed tokens will remain active on the HCP Terraform side; however, the corresponding secrets and managed agents will be removed.
Expand Down
9 changes: 9 additions & 0 deletions api/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -8109,6 +8109,13 @@ spec:
type: object
minItems: 1
type: array
allowedWorkspaces:
description: |-
AllowedWorkspaces is a list of HCP Terraform Workspaces ID which
are scoped to this agent pool.
items:
type: string
type: array
autoscaling:
description: Agent deployment settings
properties:
Expand Down
7 changes: 7 additions & 0 deletions config/crd/bases/app.terraform.io_agentpools.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8106,6 +8106,13 @@ spec:
type: object
minItems: 1
type: array
allowedWorkspaces:
description: |-
AllowedWorkspaces is a list of HCP Terraform Workspaces ID which
are scoped to this agent pool.
items:
type: string
type: array
autoscaling:
description: Agent deployment settings
properties:
Expand Down
1 change: 1 addition & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ _Appears in:_
| `agentTokens` _[AgentToken](#agenttoken) array_ | List of the agent tokens to generate. |
| `agentDeployment` _[AgentDeployment](#agentdeployment)_ | Agent deployment settings |
| `autoscaling` _[AgentDeploymentAutoscaling](#agentdeploymentautoscaling)_ | Agent deployment settings |
| `allowedWorkspaces` _string_ | AllowedWorkspaces is a list of HCP Terraform Workspaces ID which<br />are scoped to this agent pool. |
| `deletionPolicy` _[AgentPoolDeletionPolicy](#agentpooldeletionpolicy)_ | The Deletion Policy specifies the behavior of the custom resource and its associated agent pool when the custom resource is deleted.<br />- `retain`: When you delete the custom resource, the operator will remove only the custom resource.<br /> The HCP Terraform agent pool will be retained. The managed tokens will remain active on the HCP Terraform side; however, the corresponding secrets and managed agents will be removed.<br />- `destroy`: The operator will attempt to remove the managed HCP Terraform agent pool.<br /> On success, the managed agents and the corresponding secret with tokens will be removed along with the custom resource.<br /> On failure, the managed agents will be scaled down to 0, and the managed tokens, along with the corresponding secret, will be removed. The operator will continue attempting to remove the agent pool until it succeeds.<br />Default: `retain`. |


Expand Down
33 changes: 32 additions & 1 deletion internal/controller/agentpool_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,21 @@ func (r *AgentPoolReconciler) updateStatus(ctx context.Context, ap *agentPoolIns

func (r *AgentPoolReconciler) createAgentPool(ctx context.Context, ap *agentPoolInstance) (*tfc.AgentPool, error) {
options := tfc.AgentPoolCreateOptions{
Name: &ap.instance.Spec.Name,
Name: &ap.instance.Spec.Name,
OrganizationScoped: tfc.Bool(true),
}

if ap.instance.Spec.AllowedWorkspaces != nil {
options.AllowedWorkspaces = []*tfc.Workspace{}
if len(*ap.instance.Spec.AllowedWorkspaces) > 0 {
options.OrganizationScoped = tfc.Bool(false)
for _, workspace := range *ap.instance.Spec.AllowedWorkspaces {
ap.log.Info("Reconcile Agent Pool", "msg", fmt.Sprintf("creating allowed workspace %s to the agent pool", workspace))
options.AllowedWorkspaces = append(options.AllowedWorkspaces, &tfc.Workspace{ID: workspace})
}
}
}

agentPool, err := ap.tfClient.Client.AgentPools.Create(ctx, ap.instance.Spec.Organization, options)
if err != nil {
return nil, err
Expand All @@ -208,6 +221,24 @@ func (r *AgentPoolReconciler) updateAgentPool(ctx context.Context, ap *agentPool
if agentPool.Name != spec.Name {
options.Name = tfc.String(spec.Name)
}
optionsAllow := tfc.AgentPoolAllowedWorkspacesUpdateOptions{
AllowedWorkspaces: []*tfc.Workspace{},
}
if spec.AllowedWorkspaces != nil {
if len(*ap.instance.Spec.AllowedWorkspaces) > 0 {
options.OrganizationScoped = tfc.Bool(false)
for _, workspace := range *ap.instance.Spec.AllowedWorkspaces {
ap.log.Info("Reconcile Agent Pool", "msg", fmt.Sprintf("Updating allowed workspace %s to the agent pool", workspace))
optionsAllow.AllowedWorkspaces = append(optionsAllow.AllowedWorkspaces, &tfc.Workspace{ID: workspace})
}
_, err := ap.tfClient.Client.AgentPools.UpdateAllowedWorkspaces(ctx, ap.instance.Status.AgentPoolID, optionsAllow)
if err != nil {
return nil, err
}
}
} else {
options.OrganizationScoped = tfc.Bool(true)
}

return ap.tfClient.Client.AgentPools.Update(ctx, ap.instance.Status.AgentPoolID, options)
}
Expand Down
97 changes: 97 additions & 0 deletions internal/controller/agentpool_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,104 @@ var _ = Describe("Agent Pool controller", Ordered, func() {
// VALIDATE AGENT DEPLOYMENT ATTRIBUTES
validateAgentPoolDeployment(ctx, instance)
})
It("can create an agent pool with allowed workspaces", func() {
// CREATE A NEW AGENT POOL
createTestAgentPool(instance)
// VALIDATE SPEC AGAINST STATUS
validateAgentPoolTestStatus(ctx, instance)
// VALIDATE AGENT TOKENS
validateAgentPoolTestTokens(ctx, instance)

Expect(k8sClient.Get(ctx, namespacedName, instance)).Should(Succeed())
Expect(instance.Spec.AgentDeployment).To(BeNil())

workspaceInstance := testWorkspace("test-workspace-pool", "default", instance.Spec.Name)
createWorkspace(workspaceInstance)

// ADD AllowedWorkspaces
instance.Spec.AllowedWorkspaces = &[]string{workspaceInstance.Status.WorkspaceID}
Expect(k8sClient.Update(ctx, instance)).Should(Succeed())
Expect(k8sClient.Get(ctx, namespacedName, instance)).Should(Succeed())

agentPoolTestGenerationsMatch(instance)

Expect(instance.Spec.AllowedWorkspaces).ToNot(BeNil())
Expect(*instance.Spec.AllowedWorkspaces).To(HaveLen(1))
Expect(instance.Spec.AllowedWorkspaces).To(Equal(&[]string{workspaceInstance.Status.WorkspaceID}))

// VALIDATE AGENT DEPLOYMENT ATTRIBUTES
deleteWorkspace(workspaceInstance)
})
It("can update an agent pool allowed workspaces", func() {
// CREATE A NEW AGENT POOL
createTestAgentPool(instance)

// CREATE 2 WORKSPACE
workspaceInstance := testWorkspace("test-workspace-dl-pool", "default", instance.Spec.Name)
workspaceInstance2 := testWorkspace("test-workspace-dl-pool2", "default", instance.Spec.Name)
workspaceInstance2.Spec.AgentPool = nil
workspaceInstance2.Spec.ExecutionMode = "remote"
createWorkspace(workspaceInstance)
createWorkspace(workspaceInstance2)

// VALIDATE SPEC AGAINST STATUS
validateAgentPoolTestStatus(ctx, instance)

Expect(k8sClient.Get(ctx, namespacedName, instance)).Should(Succeed())
Expect(instance.Spec.AgentDeployment).To(BeNil())

instance.Spec.AllowedWorkspaces = &[]string{workspaceInstance.Status.WorkspaceID}
Expect(k8sClient.Update(ctx, instance)).Should(Succeed())
Expect(k8sClient.Get(ctx, namespacedName, instance)).Should(Succeed())
agentPoolTestGenerationsMatch(instance)
// ADD AllowedWorkspaces
instance.Spec.AllowedWorkspaces = &[]string{
workspaceInstance.Status.WorkspaceID,
workspaceInstance2.Status.WorkspaceID,
}
Expect(k8sClient.Update(ctx, instance)).Should(Succeed())
Expect(k8sClient.Get(ctx, namespacedName, instance)).Should(Succeed())
agentPoolTestGenerationsMatch(instance)

Expect(instance.Spec.AllowedWorkspaces).ToNot(BeNil())
Expect(*instance.Spec.AllowedWorkspaces).To(HaveLen(2))
Expect(instance.Spec.AllowedWorkspaces).To(Equal(&[]string{
workspaceInstance.Status.WorkspaceID,
workspaceInstance2.Status.WorkspaceID,
}))

deleteWorkspace(workspaceInstance)
deleteWorkspace(workspaceInstance2)
})
It("can remove from an agent pool allowed workspaces", func() {
// CREATE A NEW AGENT POOL
createTestAgentPool(instance)
// CREATE A WORKSPACE
workspaceInstance := testWorkspace("test-workspace-dl-pool", "default", instance.Spec.Name)
createWorkspace(workspaceInstance)

// VALIDATE SPEC AGAINST STATUS
validateAgentPoolTestStatus(ctx, instance)

Expect(k8sClient.Get(ctx, namespacedName, instance)).Should(Succeed())
Expect(instance.Spec.AgentDeployment).To(BeNil())

instance.Spec.AllowedWorkspaces = &[]string{workspaceInstance.Status.WorkspaceID}
Expect(k8sClient.Update(ctx, instance)).Should(Succeed())
Expect(k8sClient.Get(ctx, namespacedName, instance)).Should(Succeed())
agentPoolTestGenerationsMatch(instance)
workspaceInstance.Spec.AgentPool = nil
workspaceInstance.Spec.ExecutionMode = "remote"
updateWorkspace(workspaceInstance)
// ADD AllowedWorkspaces
instance.Spec.AllowedWorkspaces = nil
Expect(k8sClient.Update(ctx, instance)).Should(Succeed())
Expect(k8sClient.Get(ctx, namespacedName, instance)).Should(Succeed())
agentPoolTestGenerationsMatch(instance)

Expect(instance.Spec.AllowedWorkspaces).To(BeNil())
deleteWorkspace(workspaceInstance)
})
It("can delete agent deployments", func() {
// CREATE A NEW AGENT POOL
createTestAgentPool(instance)
Expand Down
15 changes: 15 additions & 0 deletions internal/controller/workspace_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,21 @@ func createWorkspace(instance *appv1alpha2.Workspace) {
Expect(instance.Status.WorkspaceID).Should(HavePrefix("ws-"))
}

func updateWorkspace(instance *appv1alpha2.Workspace) {
namespacedName := getNamespacedName(instance)

// Create a new Kubernetes workspace object
Expect(k8sClient.Update(ctx, instance)).Should(Succeed())
// Wait until the controller finishes the reconciliation
Eventually(func() bool {
Expect(k8sClient.Get(ctx, namespacedName, instance)).Should(Succeed())
return instance.Status.ObservedGeneration == instance.Generation
}).Should(BeTrue())

// The Kubernetes workspace object should have Status.WorkspaceID with the valid workspace ID
Expect(instance.Status.WorkspaceID).Should(HavePrefix("ws-"))
}

func deleteWorkspace(instance *appv1alpha2.Workspace) {
namespacedName := getNamespacedName(instance)

Expand Down
Loading