Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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-628-20250925-091657.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: FEATURES
body: '`AgentToken`: Introduce a new controller that manages tokens in arbitrary agent pools.'
time: 2025-09-25T09:16:57.344633+02:00
custom:
PR: "628"
3 changes: 2 additions & 1 deletion .github/pr-labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ controller:
- any:
- changed-files:
- any-glob-to-any-file:
- 'controllers/*.go'
- 'internal/controller/*.go'
- 'cmd/*.go'

crd:
- any:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/helm-end-to-end-tfc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ jobs:
--set operator.image.tag=${{ env.DOCKER_METADATA_OUTPUT_VERSION }} \
--set operator.syncPeriod=30s \
--set controllers.agentPool.workers=5 \
--set controllers.agentToken.workers=5 \
--set controllers.module.workers=5 \
--set controllers.project.workers=5 \
--set controllers.workspace.workers=5
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/helm-end-to-end-tfe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ jobs:
--set operator.tfeAddress=${{ secrets.TFE_ADDRESS }} \
--set operator.syncPeriod=30s \
--set controllers.agentPool.workers=5 \
--set controllers.agentToken.workers=5 \
--set controllers.module.workers=5 \
--set controllers.project.workers=5 \
--set controllers.workspace.workers=5
Expand Down
9 changes: 9 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,13 @@ resources:
kind: Project
path: github.com/hashicorp/hcp-terraform-operator/api/v1alpha2
version: v1alpha2
- api:
crdVersion: v1
namespaced: true
controller: true
domain: terraform.io
group: app
kind: AgentToken
path: github.com/hashicorp/hcp-terraform-operator/api/v1alpha2
version: v1alpha2
version: "3"
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Kubernetes Operator allows managing HCP Terraform / Terraform Enterprise resourc
The Operator can manage the following types of resources:

- `AgentPool` manages [HCP Terraform Agent Pools](https://developer.hashicorp.com/terraform/cloud-docs/agents/agent-pools), [HCP Terraform Agent Tokens](https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/api-tokens#agent-api-tokens) and can perform TFC agent scaling
- `AgentToken` manages [HCP Terraform Agent Tokens](https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/api-tokens#agent-api-tokens)
- `Module` implements [API-driven Run Workflows](https://developer.hashicorp.com/terraform/cloud-docs/run/api)
- `Project` manages [HCP Terraform Projects](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/organize-workspaces-with-projects)
- `Workspace` manages [HCP Terraform Workspaces](https://developer.hashicorp.com/terraform/cloud-docs/workspaces)
Expand Down Expand Up @@ -56,6 +57,7 @@ General usage documentation can be found [here](./docs/usage.md).
Controllers usage guides:

- [AgentPool](./docs/agentpool.md)
- [AgentToken](./docs/agenttoken.md)
- [Module](./docs/module.md)
- [Project](./docs/project.md)
- [Workspace](./docs/workspace.md)
Expand Down Expand Up @@ -110,6 +112,7 @@ If you encounter any issues with the Operator there are a number of ways how to

```console
$ kubectl get agentpool <NAME>
$ kubectl get agenttoken <NAME>
$ kubectl get module <NAME>
$ kubectl get project <NAME>
$ kubectl get workspace <NAME>
Expand All @@ -119,6 +122,7 @@ If you encounter any issues with the Operator there are a number of ways how to

```console
$ kubectl describe agentpool <NAME>
$ kubectl describe agenttoken <NAME>
$ kubectl describe module <NAME>
$ kubectl describe project <NAME>
$ kubectl describe workspace <NAME>
Expand Down
6 changes: 3 additions & 3 deletions api/v1alpha2/agentpool_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const (
// In `spec` only the field `Name` is allowed, the rest are used in `status`.
// More infromation:
// - https://developer.hashicorp.com/terraform/cloud-docs/agents
type AgentToken struct {
type AgentAPIToken struct {
// Agent Token name.
//
//+kubebuilder:validation:MinLength:=1
Expand Down Expand Up @@ -135,7 +135,7 @@ type AgentPoolSpec struct {
//
//+kubebuilder:validation:MinItems:=1
//+optional
AgentTokens []*AgentToken `json:"agentTokens,omitempty"`
AgentTokens []*AgentAPIToken `json:"agentTokens,omitempty"`

// Agent deployment settings
//+optional
Expand Down Expand Up @@ -179,7 +179,7 @@ type AgentPoolStatus struct {
// List of the agent tokens generated by the controller.
//
//+optional
AgentTokens []*AgentToken `json:"agentTokens,omitempty"`
AgentTokens []*AgentAPIToken `json:"agentTokens,omitempty"`
// Name of the agent deployment generated by the controller.
//
//+optional
Expand Down
12 changes: 6 additions & 6 deletions api/v1alpha2/agentpool_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestValidateAgentPoolSpecAgentToken(t *testing.T) {
successCases := map[string]AgentPool{
"HasOnlyName": {
Spec: AgentPoolSpec{
AgentTokens: []*AgentToken{
AgentTokens: []*AgentAPIToken{
{
Name: "this",
},
Expand All @@ -24,7 +24,7 @@ func TestValidateAgentPoolSpecAgentToken(t *testing.T) {
},
"HasMultipleTokens": {
Spec: AgentPoolSpec{
AgentTokens: []*AgentToken{
AgentTokens: []*AgentAPIToken{
{
Name: "this",
},
Expand All @@ -47,7 +47,7 @@ func TestValidateAgentPoolSpecAgentToken(t *testing.T) {
errorCases := map[string]AgentPool{
"HasID": {
Spec: AgentPoolSpec{
AgentTokens: []*AgentToken{
AgentTokens: []*AgentAPIToken{
{
Name: "this",
ID: "this",
Expand All @@ -57,7 +57,7 @@ func TestValidateAgentPoolSpecAgentToken(t *testing.T) {
},
"HasCreatedAt": {
Spec: AgentPoolSpec{
AgentTokens: []*AgentToken{
AgentTokens: []*AgentAPIToken{
{
Name: "this",
CreatedAt: pointer.PointerOf(int64(1984)),
Expand All @@ -67,7 +67,7 @@ func TestValidateAgentPoolSpecAgentToken(t *testing.T) {
},
"HasLastUsedAt": {
Spec: AgentPoolSpec{
AgentTokens: []*AgentToken{
AgentTokens: []*AgentAPIToken{
{
Name: "this",
LastUsedAt: pointer.PointerOf(int64(1984)),
Expand All @@ -77,7 +77,7 @@ func TestValidateAgentPoolSpecAgentToken(t *testing.T) {
},
"HasDuplicateName": {
Spec: AgentPoolSpec{
AgentTokens: []*AgentToken{
AgentTokens: []*AgentAPIToken{
{
Name: "this",
},
Expand Down
113 changes: 113 additions & 0 deletions api/v1alpha2/agenttoken_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package v1alpha2

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// The Management Policy defines how the controller will manage tokens in the specified Agent Pool.
// - `merge` — the controller will manage its tokens alongside any existing tokens in the pool, without modifying or deleting tokens it does not own.
// - `owner` — the controller assumes full ownership of all agent tokens in the pool, managing and potentially modifying or deleting all tokens, including those not created by it.
type AgentTokenManagementPolicy string

const (
AgentTokenManagementPolicyMerge AgentTokenManagementPolicy = "merge"
AgentTokenManagementPolicyOwner AgentTokenManagementPolicy = "owner"
)

// The Deletion Policy defines how managed tokens and Kubernetes Secrets should be handled when the custom resource is deleted.
// - `retain`: When the custom resource is deleted, the operator will remove only the resource itself.
// The managed HCP Terraform Agent tokens will remain active on the HCP Terraform side, and the corresponding Kubernetes Secret will not be modified.
// - `destroy`: The operator will attempt to delete the managed HCP Terraform Agent tokens and remove the corresponding Kubernetes Secret.
type AgentTokenDeletionPolicy string

const (
AgentTokenDeletionPolicyRetain AgentTokenDeletionPolicy = "retain"
AgentTokenDeletionPolicyDestroy AgentTokenDeletionPolicy = "destroy"
)

// AgentTokenSpec defines the desired state of AgentToken.
type AgentTokenSpec struct {
// Organization name where the Workspace will be created.
// More information:
// - https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/organizations
//
//+kubebuilder:validation:MinLength:=1
Organization string `json:"organization"`
// API Token to be used for API calls.
Token Token `json:"token"`
// The Deletion Policy defines how managed tokens and Kubernetes Secrets should be handled when the custom resource is deleted.
// - `retain`: When the custom resource is deleted, the operator will remove only the resource itself.
// The managed HCP Terraform Agent tokens will remain active on the HCP Terraform side, and the corresponding Kubernetes Secret will not be modified.
// - `destroy`: The operator will attempt to delete the managed HCP Terraform Agent tokens and remove the corresponding Kubernetes Secret.
// Default: `retain`.
//
//+kubebuilder:validation:Enum:=retain;destroy
//+kubebuilder:default=retain
//+optional
DeletionPolicy AgentTokenDeletionPolicy `json:"deletionPolicy,omitempty"`
// The Agent Pool name or ID where the tokens will be managed.
AgentPool AgentPoolRef `json:"agentPool"`
// The Management Policy defines how the controller will manage tokens in the specified Agent Pool.
// - `merge` — the controller will manage its tokens alongside any existing tokens in the pool, without modifying or deleting tokens it does not own.
// - `owner` — the controller assumes full ownership of all agent tokens in the pool, managing and potentially modifying or deleting all tokens, including those not created by it.
// Default: `merge`.
//
//+kubebuilder:validation:Enum:=merge;owner
//+kubebuilder:default=merge
//+optional
ManagementPolicy AgentTokenManagementPolicy `json:"managementPolicy,omitempty"`
// List of the HCP Terraform Agent tokens to manage.
//
//+kubebuilder:validation:MinItems:=1
AgentTokens []AgentAPIToken `json:"agentTokens"`
// secretName specifies the name of the Kubernetes Secret
// where the HCP Terraform Agent tokens are stored.
//
//+kubebuilder:validation:MinLength:=1
SecretName string `json:"secretName"`
}

// AgentTokenStatus defines the observed state of AgentToken.
type AgentTokenStatus struct {
// Real world state generation.
ObservedGeneration int64 `json:"observedGeneration"`
// Agent Pool where tokens are managed by the controller.
AgentPool *AgentPoolRef `json:"agentPool,omitempty"`
// List of the agent tokens managed by the controller.
//
//+optional
AgentTokens []*AgentAPIToken `json:"agentTokens,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
//+kubebuilder:printcolumn:name="Agent Pool Name",type=string,JSONPath=`.status.agentPool.name`
//+kubebuilder:printcolumn:name="Agent Pool ID",type=string,JSONPath=`.status.agentPool.id`
//+kubebuilder:metadata:labels="app.terraform.io/crd-schema-version=v25.9.0"

// AgentToken manages HCP Terraform Agent Tokens.
// More information:
// - https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/api-tokens#agent-api-tokens
type AgentToken struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec AgentTokenSpec `json:"spec"`
Status AgentTokenStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// AgentTokenList contains a list of AgentToken.
type AgentTokenList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []AgentToken `json:"items"`
}

func init() {
SchemeBuilder.Register(&AgentToken{}, &AgentTokenList{})
}
63 changes: 63 additions & 0 deletions api/v1alpha2/agenttoken_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package v1alpha2

import (
"fmt"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
)

func (t *AgentToken) ValidateSpec() error {
var allErrs field.ErrorList

allErrs = append(allErrs, t.validateSpecAgentTokens()...)

if len(allErrs) == 0 {
return nil
}

return apierrors.NewInvalid(
schema.GroupKind{Group: "", Kind: "AgentToken"},
t.Name,
allErrs,
)
}

func (t *AgentToken) validateSpecAgentTokens() field.ErrorList {
allErrs := field.ErrorList{}
atn := make(map[string]int)

for i, at := range t.Spec.AgentTokens {
f := field.NewPath("spec").Child(fmt.Sprintf("agentTokens[%d]", i))

if at.ID != "" {
allErrs = append(allErrs, field.Forbidden(
f.Child("id"),
"id is not allowed in the spec"),
)
}
if at.CreatedAt != nil {
allErrs = append(allErrs, field.Forbidden(
f.Child("createdAt"),
"createdAt is not allowed in the spec"),
)
}
if at.LastUsedAt != nil {
allErrs = append(allErrs, field.Forbidden(
f.Child("lastUsedAt"),
"lastUsedAt is not allowed in the spec"),
)
}

if _, ok := atn[at.Name]; ok {
allErrs = append(allErrs, field.Duplicate(f.Child("name"), at.Name))
}
atn[at.Name] = i
}

return allErrs
}
Loading
Loading