Skip to content

Commit 4b4527a

Browse files
committed
Merge branch 'main' of github.com:temporalio/temporal-worker-controller into dependabot/go_modules/internal/tests/github.com/jackc/pgx/v5-5.9.0
2 parents 1164118 + 9e35aad commit 4b4527a

31 files changed

Lines changed: 1245 additions & 396 deletions

.github/workflows/helm-validate.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,27 @@ jobs:
7474
- name: Template CRDs chart
7575
run: helm template test-release helm/temporal-worker-controller-crds
7676

77+
helm-check-rbac:
78+
name: Check Helm RBAC wasn't manually updated
79+
runs-on: ubuntu-latest
80+
steps:
81+
- uses: actions/checkout@v4
82+
83+
- uses: actions/setup-go@v5
84+
with:
85+
go-version-file: go.mod
86+
87+
- name: Regenerate and diff
88+
run: make manifests && git diff --exit-code helm/temporal-worker-controller/templates/rbac.yaml
89+
7790
helm-validate-succeed:
7891
name: All Helm Validations Succeed
7992
needs:
8093
- helm-lint
8194
- helm-template
8295
- helm-lint-crds
8396
- helm-template-crds
97+
- helm-check-rbac
8498
runs-on: ubuntu-latest
8599
if: always()
86100
env:

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM golang:1.25 AS builder
1+
FROM golang:1.26 AS builder
22
ARG TARGETOS
33
ARG TARGETARCH
44

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ help: ## Display this help.
153153
manifests: controller-gen ## Generate ClusterRole and CustomResourceDefinition objects.
154154
GOWORK=off GO111MODULE=on $(CONTROLLER_GEN) rbac:roleName=manager-role crd:allowDangerousTypes=true,maxDescLen=0,generateEmbeddedObjectMeta=true paths=./api/... paths=./internal/... paths=./cmd/... \
155155
output:crd:artifacts:config=helm/temporal-worker-controller-crds/templates
156+
python3 hack/sync-rbac-rules.py
156157

157158
.PHONY: generate
158159
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Temporal's [Worker Versioning](https://docs.temporal.io/production-deployment/wo
1818
📦 **Automatic version management** - Registers versions with Temporal, manages routing rules, and tracks version lifecycle
1919
🎯 **Smart traffic routing** - New workflows automatically get routed to your target worker version
2020
🛡️ **Progressive rollouts** - Catch incompatible changes early with small traffic percentages before they spread
21-
**Easy rollbacks** - Instantly route traffic back to a previous version if issues are detected
21+
**Easy rollbacks** - Instantly route traffic back to a previous version if issues are detected
2222
📈 **Per-version autoscaling** - Attach HPAs or other custom scalers to each versioned Deployment via [`WorkerResourceTemplate`](docs/worker-resource-templates.md)
2323

2424
## Quick Example
@@ -145,6 +145,7 @@ The Temporal Worker Controller eliminates this operational overhead by automatin
145145

146146
| Document | Description |
147147
|-------------------------------------------------------------|-----------------------------------------------------------------------|
148+
| [Releases](docs/release.md) | How we version and release the controller and Helm Chart |
148149
| [Migration Guide](docs/migration-to-versioned.md) | Step-by-step guide for migrating from traditional deployments |
149150
| [Reversion Guide](docs/migration-to-unversioned.md) | Step-by-step guide for migrating back to unversioned deployment |
150151
| [CD Rollouts](docs/cd-rollouts.md) | Helm, kubectl, ArgoCD, and Flux integration for steady-state rollouts |

api/v1alpha1/temporalconnection_types.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ type TemporalConnectionSpec struct {
2727
HostPort string `json:"hostPort"`
2828

2929
// MutualTLSSecretRef is the name of the Secret that contains the TLS certificate and key
30-
// for mutual TLS authentication. The secret must be `type: kubernetes.io/tls` and exist
31-
// in the same Kubernetes namespace as the TemporalConnection resource.
30+
// for mutual TLS authentication. The secret must be `type: kubernetes.io/tls` or
31+
// `type: Opaque` and exist in the same Kubernetes namespace as the TemporalConnection
32+
// resource. Opaque secrets are useful when bundling tls.crt, tls.key, and ca.crt into
33+
// a single secret (e.g. multi-file cert-manager outputs).
3234
//
3335
// More information about creating a TLS secret:
3436
// https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets

api/v1alpha1/temporalworker_webhook.go

Lines changed: 14 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package v1alpha1
77
import (
88
"context"
99
"fmt"
10-
"time"
1110

1211
"github.com/temporalio/temporal-worker-controller/internal/defaults"
1312
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -19,10 +18,6 @@ import (
1918
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
2019
)
2120

22-
const (
23-
maxTemporalWorkerDeploymentNameLen = 63
24-
)
25-
2621
func (r *TemporalWorkerDeployment) SetupWebhookWithManager(mgr ctrl.Manager) error {
2722
return ctrl.NewWebhookManagedBy(mgr).
2823
For(r).
@@ -85,71 +80,37 @@ func (r *TemporalWorkerDeployment) validateForUpdateOrCreate(ctx context.Context
8580
}
8681

8782
func validateForUpdateOrCreate(old, new *TemporalWorkerDeployment) (admission.Warnings, error) {
88-
var allErrs field.ErrorList
89-
90-
if len(new.GetName()) > maxTemporalWorkerDeploymentNameLen {
91-
allErrs = append(allErrs,
92-
field.Invalid(field.NewPath("metadata.name"), new.GetName(), fmt.Sprintf("cannot be more than %d characters", maxTemporalWorkerDeploymentNameLen)),
93-
)
94-
}
95-
96-
allErrs = append(allErrs, validateRolloutStrategy(new.Spec.RolloutStrategy)...)
97-
83+
allErrs := validateRolloutStrategy(new.Spec.RolloutStrategy)
9884
if len(allErrs) > 0 {
9985
return nil, newInvalidErr(new, allErrs)
10086
}
101-
10287
return nil, nil
10388
}
10489

90+
// validateRolloutStrategy checks constraints that the CRD schema cannot enforce:
91+
// rampPercentage must be strictly increasing across steps, and gate.input and
92+
// gate.inputFrom are mutually exclusive (gate.input is an unstructured JSON field
93+
// invisible to CEL). All other rollout constraints are enforced by the CRD CEL rules.
10594
func validateRolloutStrategy(s RolloutStrategy) []*field.Error {
10695
var allErrs []*field.Error
10796

10897
if s.Strategy == UpdateProgressive {
109-
rolloutSteps := s.Steps
110-
if len(rolloutSteps) == 0 {
111-
allErrs = append(allErrs,
112-
field.Invalid(field.NewPath("spec.rollout.steps"), rolloutSteps, "steps are required for Progressive rollout"),
113-
)
114-
}
11598
var lastRamp int
116-
for i, s := range rolloutSteps {
117-
// Check duration >= 30s
118-
if s.PauseDuration.Duration < 30*time.Second {
99+
for i, step := range s.Steps {
100+
if step.RampPercentage <= lastRamp {
119101
allErrs = append(allErrs,
120-
field.Invalid(field.NewPath(fmt.Sprintf("spec.rollout.steps[%d].pauseDuration", i)), s.PauseDuration.Duration.String(), "pause duration must be at least 30s"),
102+
field.Invalid(field.NewPath(fmt.Sprintf("spec.rollout.steps[%d].rampPercentage", i)), step.RampPercentage, "rampPercentage must increase between each step"),
121103
)
122104
}
123-
124-
// Check ramp value greater than last
125-
if s.RampPercentage <= lastRamp {
126-
allErrs = append(allErrs,
127-
field.Invalid(field.NewPath(fmt.Sprintf("spec.rollout.steps[%d].rampPercentage", i)), s.RampPercentage, "rampPercentage must increase between each step"),
128-
)
129-
}
130-
lastRamp = s.RampPercentage
105+
lastRamp = step.RampPercentage
131106
}
132107
}
133108

134-
// Validate gate input fields
135-
if s.Gate != nil {
136-
gate := s.Gate
137-
if gate.Input != nil && gate.InputFrom != nil {
138-
allErrs = append(allErrs,
139-
field.Invalid(field.NewPath("spec.rollout.gate"), "input & inputFrom",
140-
"only one of input or inputFrom may be set"),
141-
)
142-
}
143-
if gate.InputFrom != nil {
144-
cm := gate.InputFrom.ConfigMapKeyRef
145-
sec := gate.InputFrom.SecretKeyRef
146-
if (cm == nil && sec == nil) || (cm != nil && sec != nil) {
147-
allErrs = append(allErrs,
148-
field.Invalid(field.NewPath("spec.rollout.gate.inputFrom"), gate.InputFrom,
149-
"exactly one of configMapKeyRef or secretKeyRef must be set"),
150-
)
151-
}
152-
}
109+
if s.Gate != nil && s.Gate.Input != nil && s.Gate.InputFrom != nil {
110+
allErrs = append(allErrs,
111+
field.Invalid(field.NewPath("spec.rollout.gate"), "input & inputFrom",
112+
"only one of input or inputFrom may be set"),
113+
)
153114
}
154115

155116
return allErrs

api/v1alpha1/temporalworker_webhook_test.go

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,6 @@ func TestTemporalWorkerDeployment_ValidateCreate(t *testing.T) {
2727
"valid temporal worker deployment": {
2828
obj: testhelpers.MakeTWDWithName("valid-worker", ""),
2929
},
30-
"temporal worker deployment with name too long": {
31-
obj: testhelpers.MakeTWDWithName("this-is-a-very-long-temporal-worker-deployment-name-that-exceeds-the-maximum-allowed-length-of-sixty-three-characters", ""),
32-
errorMsg: "cannot be more than 63 characters",
33-
},
3430
"invalid object type": {
3531
obj: &corev1.Pod{
3632
ObjectMeta: metav1.ObjectMeta{
@@ -39,14 +35,6 @@ func TestTemporalWorkerDeployment_ValidateCreate(t *testing.T) {
3935
},
4036
errorMsg: "expected a TemporalWorkerDeployment",
4137
},
42-
"missing rollout steps": {
43-
obj: testhelpers.ModifyObj(testhelpers.MakeTWDWithName("prog-rollout-missing-steps", ""), func(obj *temporaliov1alpha1.TemporalWorkerDeployment) *temporaliov1alpha1.TemporalWorkerDeployment {
44-
obj.Spec.RolloutStrategy.Strategy = temporaliov1alpha1.UpdateProgressive
45-
obj.Spec.RolloutStrategy.Steps = nil
46-
return obj
47-
}),
48-
errorMsg: "spec.rollout.steps: Invalid value: null: steps are required for Progressive rollout",
49-
},
5038
"ramp value for step <= previous step": {
5139
obj: testhelpers.ModifyObj(testhelpers.MakeTWDWithName("prog-rollout-decreasing-ramps", ""), func(obj *temporaliov1alpha1.TemporalWorkerDeployment) *temporaliov1alpha1.TemporalWorkerDeployment {
5240
obj.Spec.RolloutStrategy.Strategy = temporaliov1alpha1.UpdateProgressive
@@ -62,18 +50,6 @@ func TestTemporalWorkerDeployment_ValidateCreate(t *testing.T) {
6250
}),
6351
errorMsg: "[spec.rollout.steps[2].rampPercentage: Invalid value: 9: rampPercentage must increase between each step, spec.rollout.steps[4].rampPercentage: Invalid value: 50: rampPercentage must increase between each step]",
6452
},
65-
"pause duration < 30s": {
66-
obj: testhelpers.ModifyObj(testhelpers.MakeTWDWithName("prog-rollout-decreasing-ramps", ""), func(obj *temporaliov1alpha1.TemporalWorkerDeployment) *temporaliov1alpha1.TemporalWorkerDeployment {
67-
obj.Spec.RolloutStrategy.Strategy = temporaliov1alpha1.UpdateProgressive
68-
obj.Spec.RolloutStrategy.Steps = []temporaliov1alpha1.RolloutStep{
69-
{10, metav1.Duration{Duration: time.Minute}},
70-
{25, metav1.Duration{Duration: 10 * time.Second}},
71-
{50, metav1.Duration{Duration: time.Minute}},
72-
}
73-
return obj
74-
}),
75-
errorMsg: `spec.rollout.steps[1].pauseDuration: Invalid value: "10s": pause duration must be at least 30s`,
76-
},
7753
}
7854

7955
for name, tc := range tests {
@@ -110,11 +86,6 @@ func TestTemporalWorkerDeployment_ValidateUpdate(t *testing.T) {
11086
oldObj: nil,
11187
newObj: testhelpers.MakeTWDWithName("valid-worker", ""),
11288
},
113-
"update with name too long": {
114-
oldObj: nil,
115-
newObj: testhelpers.MakeTWDWithName("this-is-a-very-long-temporal-worker-deployment-name-that-exceeds-the-maximum-allowed-length-of-sixty-three-characters", ""),
116-
errorMsg: "cannot be more than 63 characters",
117-
},
11889
}
11990

12091
for name, tc := range tests {
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
2+
//
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2024 Datadog, Inc.
4+
5+
package v1alpha1
6+
7+
// Integration tests for CRD-level CEL validation rules on TemporalWorkerDeployment.
8+
//
9+
// These tests hit a real kube-apiserver (via envtest) so they verify that the
10+
// x-kubernetes-validations blocks in the generated CRD manifest are syntactically
11+
// valid and semantically correct. The webhook Go code is NOT involved here — we are
12+
// testing what the API server enforces regardless of whether the webhook is enabled.
13+
14+
import (
15+
"strings"
16+
"time"
17+
18+
. "github.com/onsi/ginkgo/v2"
19+
. "github.com/onsi/gomega"
20+
corev1 "k8s.io/api/core/v1"
21+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+
)
23+
24+
var _ = Describe("TemporalWorkerDeployment CRD CEL validation", func() {
25+
var ns string
26+
27+
BeforeEach(func() {
28+
ns = makeTestNamespace("twd-cel")
29+
})
30+
31+
// baseTWD returns a minimal valid TWD in the given namespace.
32+
baseTWD := func(name string) *TemporalWorkerDeployment {
33+
return &TemporalWorkerDeployment{
34+
ObjectMeta: metav1.ObjectMeta{
35+
Name: name,
36+
Namespace: ns,
37+
},
38+
Spec: TemporalWorkerDeploymentSpec{
39+
Template: corev1.PodTemplateSpec{
40+
Spec: corev1.PodSpec{
41+
Containers: []corev1.Container{{Name: "worker", Image: "worker:latest"}},
42+
},
43+
},
44+
RolloutStrategy: RolloutStrategy{Strategy: UpdateAllAtOnce},
45+
WorkerOptions: WorkerOptions{
46+
TemporalConnectionRef: TemporalConnectionReference{Name: "my-connection"},
47+
TemporalNamespace: "default",
48+
},
49+
},
50+
}
51+
}
52+
53+
It("accepts a valid TWD", func() {
54+
Expect(k8sClient.Create(ctx, baseTWD("valid-worker"))).To(Succeed())
55+
})
56+
57+
It("rejects name longer than 63 characters", func() {
58+
twd := baseTWD(strings.Repeat("a", 64))
59+
err := k8sClient.Create(ctx, twd)
60+
Expect(err).To(HaveOccurred())
61+
Expect(err.Error()).To(ContainSubstring("name cannot be more than 63 characters"))
62+
})
63+
64+
It("rejects Progressive strategy with no steps", func() {
65+
twd := baseTWD("prog-no-steps")
66+
twd.Spec.RolloutStrategy = RolloutStrategy{Strategy: UpdateProgressive}
67+
err := k8sClient.Create(ctx, twd)
68+
Expect(err).To(HaveOccurred())
69+
Expect(err.Error()).To(ContainSubstring("steps are required for Progressive rollout"))
70+
})
71+
72+
It("rejects more than 20 Progressive steps", func() {
73+
steps := make([]RolloutStep, 21)
74+
for i := range steps {
75+
steps[i] = RolloutStep{
76+
RampPercentage: i + 1,
77+
PauseDuration: metav1.Duration{Duration: time.Minute},
78+
}
79+
}
80+
twd := baseTWD("prog-too-many-steps")
81+
twd.Spec.RolloutStrategy = RolloutStrategy{Strategy: UpdateProgressive, Steps: steps}
82+
err := k8sClient.Create(ctx, twd)
83+
Expect(err).To(HaveOccurred())
84+
Expect(err.Error()).To(ContainSubstring("Too many"))
85+
})
86+
87+
It("rejects a Progressive step with pauseDuration less than 30s", func() {
88+
twd := baseTWD("short-pause")
89+
twd.Spec.RolloutStrategy = RolloutStrategy{
90+
Strategy: UpdateProgressive,
91+
Steps: []RolloutStep{
92+
{RampPercentage: 50, PauseDuration: metav1.Duration{Duration: 10 * time.Second}},
93+
},
94+
}
95+
err := k8sClient.Create(ctx, twd)
96+
Expect(err).To(HaveOccurred())
97+
Expect(err.Error()).To(ContainSubstring("pause duration must be at least 30s"))
98+
})
99+
100+
It("rejects gate.inputFrom with both configMapKeyRef and secretKeyRef set", func() {
101+
twd := baseTWD("bad-gate-inputfrom")
102+
twd.Spec.RolloutStrategy = RolloutStrategy{
103+
Strategy: UpdateAllAtOnce,
104+
Gate: &GateWorkflowConfig{
105+
WorkflowType: "my-gate",
106+
InputFrom: &GateInputSource{
107+
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
108+
LocalObjectReference: corev1.LocalObjectReference{Name: "my-cm"},
109+
Key: "key",
110+
},
111+
SecretKeyRef: &corev1.SecretKeySelector{
112+
LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"},
113+
Key: "key",
114+
},
115+
},
116+
},
117+
}
118+
err := k8sClient.Create(ctx, twd)
119+
Expect(err).To(HaveOccurred())
120+
Expect(err.Error()).To(ContainSubstring("exactly one of configMapKeyRef or secretKeyRef must be set"))
121+
})
122+
})

0 commit comments

Comments
 (0)