Skip to content

Commit

Permalink
Feat workspaces trigger patterns
Browse files Browse the repository at this point in the history
  • Loading branch information
baptman21 committed Jan 20, 2025
1 parent 2d94cfa commit 19e3dac
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 9 deletions.
15 changes: 15 additions & 0 deletions api/v1alpha2/workspace_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,21 @@ type WorkspaceSpec struct {
//+kubebuilder:validation:MinItems:=1
//+optional
TeamAccess []*TeamAccess `json:"teamAccess,omitempty"`
// File triggers allow you to queue runs in Terraform Cloud when files in your VCS repository change.
//
//+optional
//+kubebuilder:default:=false
FileTriggersEnabled bool `json:"fileTriggersEnabled"`
// The list of pattern triggers that will queue runs in Terraform Cloud when files in your VCS repository change.
//
//+kubebuilder:validation:MinItems:=1
//+optional
TriggerPatterns []string `json:"triggerPatterns,omitempty"`
// The list of pattern prefixes that will queue runs in Terraform Cloud when files in your VCS repository change.
//
//+kubebuilder:validation:MinItems:=1
//+optional
TriggerPrefixes []string `json:"triggerPrefixes,omitempty"`
// The version of Terraform to use for this workspace.
// If not specified, the latest available version will be used.
// Must match pattern: `^\\d{1}\\.\\d{1,2}\\.\\d{1,2}$`
Expand Down
46 changes: 46 additions & 0 deletions api/v1alpha2/workspace_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func (w *Workspace) ValidateSpec() error {
allErrs = append(allErrs, w.validateSpecRunTriggers()...)
allErrs = append(allErrs, w.validateSpecSSHKey()...)
allErrs = append(allErrs, w.validateSpecProject()...)
allErrs = append(allErrs, w.validateSpecFileTriggers()...)
allErrs = append(allErrs, w.validateSpecTerraformVariables()...)
allErrs = append(allErrs, w.validateSpecEnvironmentVariables()...)
allErrs = append(allErrs, w.validateSpecDeletionPolicy()...)
Expand Down Expand Up @@ -590,6 +591,51 @@ func (w *Workspace) validateSpecDeletionPolicy() field.ErrorList {
return allErrs
}

func (w *Workspace) validateSpecFileTriggers() field.ErrorList {
allErrs := field.ErrorList{}
spec := w.Spec

f := field.NewPath("spec")

if len(spec.TriggerPatterns) > 0 && len(spec.TriggerPrefixes) > 0 {
allErrs = append(allErrs, field.Invalid(
f,
"",
"only one of the field TriggerPatterns or TriggerPrefixes is allowed"),
)
}

f = field.NewPath("spec").Child("fileTriggerEnabled")

if !spec.FileTriggersEnabled && len(spec.TriggerPatterns) > 0 {
allErrs = append(allErrs, field.Invalid(
f,
"",
"TriggerPatterns requires FileTriggersEnabled set to true"),
)
}

if !spec.FileTriggersEnabled && len(spec.TriggerPrefixes) > 0 {
allErrs = append(allErrs, field.Invalid(
f,
"",
"TriggerPrefixes requires FileTriggersEnabled set to true"),
)
}

f = field.NewPath("spec").Child("workingDirectory")

if spec.WorkingDirectory == "" && len(spec.TriggerPrefixes) > 0 {
allErrs = append(allErrs, field.Invalid(
f,
"",
"TriggerPrefixes requires a non-empty WorkingDirectory"),
)
}

return allErrs
}

// TODO:Validation
//
// + Tags duplicate: spec.tags[]
Expand Down
63 changes: 63 additions & 0 deletions api/v1alpha2/workspace_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1089,3 +1089,66 @@ func TestValidateSpecDeletionPolicy(t *testing.T) {
})
}
}

func TestValidateWorkspaceSpecFileTriggers(t *testing.T) {
t.Parallel()

successCases := map[string]Workspace{
"HasOnlyTriggerPatterns": {
Spec: WorkspaceSpec{
FileTriggersEnabled: true,
TriggerPatterns: []string{"path/*/workspace/*"},
},
},
"HasOnlyTriggerPrefixes": {
Spec: WorkspaceSpec{
FileTriggersEnabled: true,
WorkingDirectory: "path/",
TriggerPrefixes: []string{"path/to/workspace/"},
},
},
}

for n, c := range successCases {
t.Run(n, func(t *testing.T) {
if errs := c.validateSpecFileTriggers(); len(errs) != 0 {
t.Errorf("Unexpected validation errors: %v", errs)
}
})
}

errorCases := map[string]Workspace{
"TriggerPrefixesWithoutWorkingDirectory": {
Spec: WorkspaceSpec{
FileTriggersEnabled: true,
TriggerPrefixes: []string{"path/to/workspace/"},
},
},
"BothTriggerOptions": {
Spec: WorkspaceSpec{
FileTriggersEnabled: true,
WorkingDirectory: "path/",
TriggerPatterns: []string{"path/*/workspace/*"},
TriggerPrefixes: []string{"path/to/workspace/"},
},
},
"TriggerPatternsWithoutFileTriggersEnabled": {
Spec: WorkspaceSpec{
TriggerPatterns: []string{"path/*/workspace/*"},
},
},
"TriggerPrefixesWithoutFileTriggersEnabled": {
Spec: WorkspaceSpec{
TriggerPrefixes: []string{"path/to/workspace/"},
},
},
}

for n, c := range errorCases {
t.Run(n, func(t *testing.T) {
if errs := c.validateSpecFileTriggers(); len(errs) == 0 {
t.Error("Unexpected failure, at least one error is expected")
}
})
}
}
10 changes: 10 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 @@ -205,6 +205,11 @@ spec:
- https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings#execution-mode
pattern: ^(agent|local|remote)$
type: string
fileTriggersEnabled:
default: false
description: File triggers allow you to queue runs in Terraform Cloud
when files in your VCS repository change.
type: boolean
name:
description: Workspace name.
minLength: 1
Expand Down Expand Up @@ -691,6 +696,20 @@ spec:
required:
- secretKeyRef
type: object
triggerPatterns:
description: The list of pattern triggers that will queue runs in
Terraform Cloud when files in your VCS repository change.
items:
type: string
minItems: 1
type: array
triggerPrefixes:
description: The list of pattern prefixes that will queue runs in
Terraform Cloud when files in your VCS repository change.
items:
type: string
minItems: 1
type: array
versionControl:
description: |-
Settings for the workspace's VCS repository, enabling the UI/VCS-driven run workflow.
Expand Down
19 changes: 19 additions & 0 deletions config/crd/bases/app.terraform.io_workspaces.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,11 @@ spec:
- https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings#execution-mode
pattern: ^(agent|local|remote)$
type: string
fileTriggersEnabled:
default: false
description: File triggers allow you to queue runs in Terraform Cloud
when files in your VCS repository change.
type: boolean
name:
description: Workspace name.
minLength: 1
Expand Down Expand Up @@ -688,6 +693,20 @@ spec:
required:
- secretKeyRef
type: object
triggerPatterns:
description: The list of pattern triggers that will queue runs in
Terraform Cloud when files in your VCS repository change.
items:
type: string
minItems: 1
type: array
triggerPrefixes:
description: The list of pattern prefixes that will queue runs in
Terraform Cloud when files in your VCS repository change.
items:
type: string
minItems: 1
type: array
versionControl:
description: |-
Settings for the workspace's VCS repository, enabling the UI/VCS-driven run workflow.
Expand Down
3 changes: 3 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,9 @@ _Appears in:_
| `runTasks` _[WorkspaceRunTask](#workspaceruntask) array_ | Run tasks allow HCP Terraform to interact with external systems at specific points in the HCP Terraform run lifecycle.<br />More information:<br /> - https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings/run-tasks |
| `tags` _[Tag](#tag) array_ | Workspace tags are used to help identify and group together workspaces.<br />Tags must be one or more characters; can include letters, numbers, colons, hyphens, and underscores; and must begin and end with a letter or number. |
| `teamAccess` _[TeamAccess](#teamaccess) array_ | HCP Terraform workspaces can only be accessed by users with the correct permissions.<br />You can manage permissions for a workspace on a per-team basis.<br />When a workspace is created, only the owners team and teams with the "manage workspaces" permission can access it,<br />with full admin permissions. These teams' access can't be removed from a workspace.<br />More information:<br /> - https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings/access |
| `fileTriggersEnabled` _boolean_ | File triggers allow you to queue runs in Terraform Cloud when files in your VCS repository change. |
| `triggerPatterns` _string array_ | The list of pattern triggers that will queue runs in Terraform Cloud when files in your VCS repository change. |
| `triggerPrefixes` _string array_ | The list of pattern prefixes that will queue runs in Terraform Cloud when files in your VCS repository change. |
| `terraformVersion` _string_ | The version of Terraform to use for this workspace.<br />If not specified, the latest available version will be used.<br />Must match pattern: `^\\d\{1\}\\.\\d\{1,2\}\\.\\d\{1,2\}$`<br />More information:<br /> - https://www.terraform.io/cloud-docs/workspaces/settings#terraform-version |
| `workingDirectory` _string_ | The directory where Terraform will execute, specified as a relative path from the root of the configuration directory.<br />More information:<br /> - https://www.terraform.io/cloud-docs/workspaces/settings#terraform-working-directory |
| `environmentVariables` _[Variable](#variable) array_ | Terraform Environment variables for all plans and applies in this workspace.<br />Variables defined within a workspace always overwrite variables from variable sets that have the same type and the same key.<br />More information:<br /> - https://developer.hashicorp.com/terraform/cloud-docs/workspaces/variables<br /> - https://developer.hashicorp.com/terraform/cloud-docs/workspaces/variables#environment-variables |
Expand Down
33 changes: 24 additions & 9 deletions internal/controller/workspace_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,16 @@ func (r *WorkspaceReconciler) updateStatus(ctx context.Context, w *workspaceInst
func (r *WorkspaceReconciler) createWorkspace(ctx context.Context, w *workspaceInstance) (*tfc.Workspace, error) {
spec := w.instance.Spec
options := tfc.WorkspaceCreateOptions{
Name: tfc.String(spec.Name),
AllowDestroyPlan: tfc.Bool(spec.AllowDestroyPlan),
AutoApply: tfc.Bool(applyMethodToBool(spec.ApplyMethod)),
Description: tfc.String(spec.Description),
ExecutionMode: tfc.String(spec.ExecutionMode),
TerraformVersion: tfc.String(spec.TerraformVersion),
WorkingDirectory: tfc.String(spec.WorkingDirectory),
Name: tfc.String(spec.Name),
AllowDestroyPlan: tfc.Bool(spec.AllowDestroyPlan),
AutoApply: tfc.Bool(applyMethodToBool(spec.ApplyMethod)),
Description: tfc.String(spec.Description),
ExecutionMode: tfc.String(spec.ExecutionMode),
FileTriggersEnabled: tfc.Bool(spec.FileTriggersEnabled),
TriggerPatterns: spec.TriggerPatterns,
TriggerPrefixes: spec.TriggerPrefixes,
TerraformVersion: tfc.String(spec.TerraformVersion),
WorkingDirectory: tfc.String(spec.WorkingDirectory),
}

if spec.ExecutionMode == "agent" {
Expand All @@ -275,7 +278,6 @@ func (r *WorkspaceReconciler) createWorkspace(ctx context.Context, w *workspaceI
Identifier: tfc.String(spec.VersionControl.Repository),
Branch: tfc.String(spec.VersionControl.Branch),
}
options.FileTriggersEnabled = tfc.Bool(false)
options.SpeculativeEnabled = tfc.Bool(spec.VersionControl.SpeculativePlans)
}

Expand Down Expand Up @@ -361,6 +363,20 @@ func (r *WorkspaceReconciler) updateWorkspace(ctx context.Context, w *workspaceI
updateOptions.ExecutionMode = tfc.String(spec.ExecutionMode)
}

if workspace.FileTriggersEnabled != spec.FileTriggersEnabled {
updateOptions.FileTriggersEnabled = tfc.Bool(spec.FileTriggersEnabled)
}

triggerPatternsDiff := triggerPatternsDifference(getWorkspaceTriggerPatterns(workspace), getTriggerPatterns(&w.instance))
if len(triggerPatternsDiff) != 0 {
updateOptions.TriggerPatterns = spec.TriggerPatterns
}

triggerPrefixesDiff := triggerPrefixesDifference(getWorkspaceTriggerPrefixes(workspace), getTriggerPrefixes(&w.instance))
if len(triggerPrefixesDiff) != 0 {
updateOptions.TriggerPrefixes = spec.TriggerPrefixes
}

if spec.RemoteStateSharing != nil {
if workspace.GlobalRemoteState != spec.RemoteStateSharing.AllWorkspaces {
updateOptions.GlobalRemoteState = tfc.Bool(spec.RemoteStateSharing.AllWorkspaces)
Expand Down Expand Up @@ -399,7 +415,6 @@ func (r *WorkspaceReconciler) updateWorkspace(ctx context.Context, w *workspaceI
Identifier: tfc.String(spec.VersionControl.Repository),
Branch: tfc.String(spec.VersionControl.Branch),
}
updateOptions.FileTriggersEnabled = tfc.Bool(false)

if workspace.SpeculativeEnabled != spec.VersionControl.SpeculativePlans {
updateOptions.SpeculativeEnabled = tfc.Bool(spec.VersionControl.SpeculativePlans)
Expand Down
57 changes: 57 additions & 0 deletions internal/controller/workspace_controller_trigger_patterns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package controller

import (
tfc "github.com/hashicorp/go-tfe"
appv1alpha2 "github.com/hashicorp/hcp-terraform-operator/api/v1alpha2"
)

// HELPERS

// getTriggerPatterns return a map that maps consist of all trigger patterns defined in a object specification
// and values 'true' to simulate the Set structure
func getTriggerPatterns(instance *appv1alpha2.Workspace) map[string]bool {
patterns := make(map[string]bool)

if len(instance.Spec.TriggerPatterns) == 0 {
return patterns
}

for _, t := range instance.Spec.TriggerPatterns {
patterns[string(t)] = true
}

return patterns
}

// getWorkspaceTriggerPatterns return a map that maps consist of all trigger patterns assigned to workspace
// and values 'true' to simulate the Set structure
func getWorkspaceTriggerPatterns(workspace *tfc.Workspace) map[string]bool {
patterns := make(map[string]bool)

if len(workspace.TriggerPatterns) == 0 {
return patterns
}

for _, t := range workspace.TriggerPatterns {
patterns[t] = true
}

return patterns
}

// triggerPatternsDifference returns the list of trigger patterns that consists of the elements of leftTriggerPatterns
// which are not elements of rightTriggerPatterns
func triggerPatternsDifference(leftTriggerPatterns, rightTriggerPatterns map[string]bool) []string {
var d []string

for t := range leftTriggerPatterns {
if !rightTriggerPatterns[t] {
d = append(d, t)
}
}

return d
}
Loading

0 comments on commit 19e3dac

Please sign in to comment.